From ececf833892da7739c2ed4ba3670b5ccd0b84bf8 Mon Sep 17 00:00:00 2001 From: David Yoder Date: Tue, 22 Oct 2024 14:32:46 -0400 Subject: [PATCH] initial commit --- Hudu-ADDS-Documentation.ps1 | 528 ++++++++++++++++++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 Hudu-ADDS-Documentation.ps1 diff --git a/Hudu-ADDS-Documentation.ps1 b/Hudu-ADDS-Documentation.ps1 new file mode 100644 index 0000000..a10eed5 --- /dev/null +++ b/Hudu-ADDS-Documentation.ps1 @@ -0,0 +1,528 @@ +# Based on the script by lwhitelock, which is based on the original script by Kelvin Tegelaar https://github.com/KelvinTegelaar/AutomaticDocumentation +##################################################################### +# +# Active Directory Details to Hudu +# +# $HuduAPIKey must be supplied by RMM script + +# $HuduBaseDomain is the base domain of your Hudu instance (without a trailing /) +# and must be supplied by RMM script + +# $CompanyName must exactly match the name of the company in Hudu +# and must be supplied by RMM script + +# This is the name of the Hudu Asset Layout you want to use. +$HuduAssetLayoutName = "Active Directory" +##################################################################### + +#Get the Hudu API Module if not installed +if (Get-Module -ListAvailable -Name HuduAPI) { + Import-Module HuduAPI + } else { + Install-Module HuduAPI -Force + Import-Module HuduAPI + } + +#Set Hudu logon information +New-HuduAPIKey $HuduAPIKey +New-HuduBaseUrl $HuduBaseDomain + +Function Get-RegistryValue +{ + # Gets the specified registry value or $Null if it is missing + [CmdletBinding()] + Param + ( + [String] $path, + [String] $name, + [String] $ComputerName + ) + + If($ComputerName -eq $env:computername -or $ComputerName -eq "LocalHost") + { + $key = Get-Item -LiteralPath $path -EA 0 + If($key) + { + Return $key.GetValue($name, $Null) + } + Else + { + Return $Null + } + } + + #path needed here is different for remote registry access + $path1 = $path.SubString( 6 ) + $path2 = $path1.Replace( '\', '\\' ) + + $registry = $null + try + { + ## use the Remote Registry service + $registry = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey( + [Microsoft.Win32.RegistryHive]::LocalMachine, + $ComputerName ) + } + catch + { + #$e = $error[ 0 ] + #3.06, remove the verbose message as it confised some people + #wv "Could not open registry on computer $ComputerName ($e)" + } + + $val = $null + If( $registry ) + { + $key = $registry.OpenSubKey( $path2 ) + If( $key ) + { + $val = $key.GetValue( $name ) + $key.Close() + } + + $registry.Close() + } + + Return $val +} + +Function GetBasicDCInfo { + Param + ( + [Parameter( Mandatory = $true )] + [String] $dn ## distinguishedName of a DC + ) + + $DCName = $dn.SubString( 0, $dn.IndexOf( '.' ) ) + $SrvName = $dn.SubString( $dn.IndexOf( '.' ) + 1 ) + + $Results = Get-ADDomainController -Identity $DCName -Server $SrvName -EA 0 + + If($? -and $Null -ne $Results) + { + $GC = $Results.IsGlobalCatalog.ToString() + $ReadOnly = $Results.IsReadOnly.ToString() + $IPv4Address = $Results.IPv4Address -join ", " + $IPv6Address = $Results.IPv6Address -join ", " + $ServerOS = $Results.OperatingSystem + $tmp = Get-RegistryValue "HKLM:\software\microsoft\windows nt\currentversion" "installationtype" $DCName + If( $null -eq $tmp ) { $ServerCore = 'Unknown' } + ElseIf( $tmp -eq 'Server Core') { $ServerCore = 'Yes' } + Else { $ServerCore = 'No' } + } + Else + { + $GC = 'Unable to retrieve status' + $ReadOnly = $GC + $ServerOS = $GC + $ServerCore = $GC + $IPv4Address = $GC + $IPv6Address = $GC + } + + $obj = [PSCustomObject] @{ + DCName = $DCName + GC = $GC + ReadOnly = $ReadOnly + ServerOS = $ServerOS + ServerCore = $ServerCore + IPv4Address = $IPv4Address + IPv6Address = $IPv6Address + } + + Return $obj +} + +Function GetTimeServerRegistryKeys { + Param + ( + [String] $DCName + ) + + $AnnounceFlags = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" "AnnounceFlags" $DCName + If( $null -eq $AnnounceFlags ) + { + ## DCName can't be contacted or DCName is an appliance with no registry + $AnnounceFlags = 'n/a' + $MaxNegPhaseCorrection = 'n/a' + $MaxPosPhaseCorrection = 'n/a' + $NtpServer = 'n/a' + $NtpType = 'n/a' + $SpecialPollInterval = 'n/a' + $VMICTimeProviderEnabled = 'n/a' + $NTPSource = 'Cannot retrieve data from registry' + } + Else + { + $MaxNegPhaseCorrection = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" "MaxNegPhaseCorrection" $DCName + $MaxPosPhaseCorrection = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" "MaxPosPhaseCorrection" $DCName + $NtpServer = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" "NtpServer" $DCName + $NtpType = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" "Type" $DCName + $SpecialPollInterval = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient" "SpecialPollInterval" $DCName + $VMICTimeProviderEnabled = Get-RegistryValue "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\VMICTimeProvider" "Enabled" $DCName + $NTPSource = Invoke-Command -ComputerName $DCName {w32tm /query /computer:$DCName /source} + } + + If( $VMICTimeProviderEnabled -eq 'n/a' ) + { + $VMICEnabled = 'n/a' + } + ElseIf( $VMICTimeProviderEnabled -eq 0 ) + { + $VMICEnabled = 'Disabled' + } + Else + { + $VMICEnabled = 'Enabled' + } + + $obj = [PSCustomObject] @{ + DCName = $DCName.Substring(0, $_.IndexOf( '.')) + TimeSource = $NTPSource + AnnounceFlags = $AnnounceFlags + MaxNegPhaseCorrection = $MaxNegPhaseCorrection + MaxPosPhaseCorrection = $MaxPosPhaseCorrection + NtpServer = $NtpServer + NtpType = $NtpType + SpecialPollInterval = $SpecialPollInterval + VMICTimeProvider = $VMICEnabled + } + Return $obj +} + +function Get-WinADForestInformation { + $Data = @{ } + $ForestInformation = $(Get-ADForest) + $Data.Forest = $ForestInformation + $Data.RootDSE = $(Get-ADRootDSE -Properties *) + $Data.ForestName = $ForestInformation.Name + $Data.ForestNameDN = $Data.RootDSE.defaultNamingContext + $Data.Domains = $ForestInformation.Domains + $Data.ForestInformation = @{ + 'Forest Name' = $ForestInformation.Name + 'Root Domain' = $ForestInformation.RootDomain + 'Forest Functional Level' = $ForestInformation.ForestMode + '# of Domains' = ($ForestInformation.Domains).Count + 'Sites Count' = ($ForestInformation.Sites).Count + 'Forest Domains' = ($ForestInformation.Domains) -join ", " + 'Sites' = ($ForestInformation.Sites) -join ", " + } + + $Data.UPNSuffixes = Invoke-Command -ScriptBlock { + $UPNSuffixList = [PSCustomObject] @{ + "Primary UPN" = $ForestInformation.RootDomain + "UPN Suffixes" = $ForestInformation.UPNSuffixes -join "," + } + return $UPNSuffixList + } + + $Data.GlobalCatalogs = $ForestInformation.GlobalCatalogs + $Data.SPNSuffixes = $ForestInformation.SPNSuffixes + + $Data.Sites = Invoke-Command -ScriptBlock { + $Sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites | Sort-Object + $SiteData = foreach ($Site in $Sites) { + [PSCustomObject] @{ + "Site Name" = $site.Name + "Subnets" = ($site.Subnets | Sort-Object) -join ", " + "Servers" = ($Site.Servers) -join ", " + } + } + Return $SiteData + } + + + $Data.FSMO = Invoke-Command -ScriptBlock { + [PSCustomObject] @{ + "Domain" = $ForestInformation.RootDomain + "Role" = 'Domain Naming Master' + "Holder" = $ForestInformation.DomainNamingMaster + } + + [PSCustomObject] @{ + "Domain" = $ForestInformation.RootDomain + "Role" = 'Schema Master' + "Holder" = $ForestInformation.SchemaMaster + } + + foreach ($Domain in $ForestInformation.Domains) { + $DomainFSMO = Get-ADDomain $Domain | Select-Object PDCEmulator, RIDMaster, InfrastructureMaster + + [PSCustomObject] @{ + "Domain" = $Domain + "Role" = 'PDC Emulator' + "Holder" = $DomainFSMO.PDCEmulator + } + + + [PSCustomObject] @{ + "Domain" = $Domain + "Role" = 'Infrastructure Master' + "Holder" = $DomainFSMO.InfrastructureMaster + } + + [PSCustomObject] @{ + "Domain" = $Domain + "Role" = 'RID Master' + "Holder" = $DomainFSMO.RIDMaster + } + + } + + Return $FSMO + } + + $Data.OptionalFeatures = Invoke-Command -ScriptBlock { + $OptionalFeatures = $(Get-ADOptionalFeature -Filter * ) + $Optional = @{ + 'Recycle Bin Enabled' = '' + 'Privileged Access Management Feature Enabled' = '' + } + ### Fix Optional Features + foreach ($Feature in $OptionalFeatures) { + if ($Feature.Name -eq 'Recycle Bin Feature') { + if ("$($Feature.EnabledScopes)" -eq '') { + $Optional.'Recycle Bin Enabled' = $False + } + else { + $Optional.'Recycle Bin Enabled' = $True + } + } + if ($Feature.Name -eq 'Privileged Access Management Feature') { + if ("$($Feature.EnabledScopes)" -eq '') { + $Optional.'Privileged Access Management Feature Enabled' = $False + } + else { + $Optional.'Privileged Access Management Feature Enabled' = $True + } + } + } + return $Optional + ### Fix optional features + } + return $Data +} + +$TableHeader = "" +$Whitespace = "
" +$TableStyling = "" +$passwordToc = "
" +$passwordpolicyNice = $passwordToc + $TableHeader + ($passwordpolicyheader -replace $TableStyling) + ($passwordpolicyraw -replace $TableStyling) + $Whitespace + +$adminsraw = Get-ADGroupMember "Domain Admins" | Select-Object SamAccountName, Name | convertto-html -Fragment | Select-Object -Skip 1 +$adminsToc = "
" +$adminsnice = $adminsToc + $TableHeader + ($adminsraw -replace $TableStyling) + $Whitespace + +$TotalUsers = (Get-AdUser -filter *).count +$EnabledUsers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $true }).count +$DisabledUSers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $false }).count +$DomainAdminUsers = (Get-ADGroupMember -Identity "Domain Admins").count +$EnterpriseAdminUsers = (Get-ADGroupMember -Identity "Enterprise Admins").count +$SchemaAdminUsers = (Get-ADGroupMember -Identity "Schema Admins").count +$AdminCountUsers = (Get-ADUser -LDAPFilter "(admincount=1)").count +$UsersCountObj = [PSCustomObject] @{ + 'Total' = $TotalUsers + 'Enabled' = $EnabledUsers + 'Disabled' = $DisabledUSers + 'Domain Admins' = $DomainAdminUsers + 'Enterprise Admins' = $EnterpriseAdminUsers + 'Schema Admins' = $SchemaAdminUsers + 'AdminCount users' = $AdminCountUsers +} + +$userTotalsRaw = $UsersCountObj | convertto-html -Fragment | Select-Object -Skip 1 +$userTotalsToc = "
" +$userTotalsNice = $userTotalsToc + $TableHeader + ($userTotalsRaw -replace $TableStyling) + $Whitespace + +$currentDate = Get-Date -Format "dddd dd/MM/yyyy HH:mm K" +$toc = '

+ FOREST SUMMARY  |   + SITE SUMMARY  |   + DOMAIN SUMMARY  |   + DOMAIN CONTROLLERS  |   + NTP CONFIGURATION  |   + FSMO ROLES  |   + OPTIONAL FEATURES  |   + UPN SUFFIXES  |   + DEFAULT PASSWORD POLICIES  |   + USER COUNT  |   + DOMAIN ADMINS +


' + +# Setup the fields for the Asset +$AssetFields = @{ + + 'last_updated' = $currentDate + 'toc' = $toc + 'forest_name' = $RawAD.ForestName + 'forest_summary' = $ForestNice + 'site_summary' = $SiteNice + 'domain_summary' = $DomainsNice + 'domain_controllers' = $DCNice + 'ntp_configuration' = $DCNTPconfigNice + 'fsmo_roles' = $FSMONice + 'optional_features' = $OptionalNice + 'upn_suffixes' = $UPNNice + 'default_password_policies' = $passwordpolicyNice + 'domain_admins' = $adminsnice + 'user_count' = $userTotalsNice + } + +# Checking if the FlexibleAsset exists. If not, create a new one. +$Layout = Get-HuduAssetLayouts -name $HuduAssetLayoutName + +if (!$Layout) { + +$AssetLayoutFields = @( + @{ + label = 'Last Updated' + field_type = 'Text' + show_in_list = 'true' + position = 1 + }, + @{ + label = 'Table of Contents' + field_type = 'RichText' + show_in_list = 'false' + position = 2 + }, + @{ + label = 'Forest Name' + field_type = 'Text' + show_in_list = 'true' + position = 3 + }, + @{ + label = 'Forest Summary' + field_type = 'RichText' + show_in_list = 'false' + position = 4 + }, + @{ + label = 'Site Summary' + field_type = 'RichText' + show_in_list = 'false' + position = 5 + }, + @{ + label = 'Domain Summary' + field_type = 'RichText' + show_in_list = 'false' + position = 6 + }, + @{ + label = 'Domain Controllers' + field_type = 'RichText' + show_in_list = 'false' + position = 7 + }, + @{ + label = 'NTP Configuration' + field_type = 'RichText' + show_in_list = 'false' + position = 8 + }, + @{ + label = 'FSMO Roles' + field_type = 'RichText' + show_in_list = 'false' + position = 9 + }, + @{ + label = 'Optional Features' + field_type = 'RichText' + show_in_list = 'false' + position = 10 + }, + @{ + label = 'UPN Suffixes' + field_type = 'RichText' + show_in_list = 'false' + position = 11 + }, + @{ + label = 'Default Password Policies' + field_type = 'RichText' + show_in_list = 'false' + position = 12 + }, + @{ + label = 'User Count' + field_type = 'RichText' + show_in_list = 'false' + position = 13 + }, + @{ + label = 'Domain Admins' + field_type = 'RichText' + show_in_list = 'false' + position = 14 + } + ) + + Write-Host "Creating New Asset Layout" + #$NewLayout = New-HuduAssetLayout -name $HuduAssetLayoutName -icon "fas fa-sitemap" -color "#00adef" -icon_color "#000000" -include_passwords $false -include_photos $false -include_comments $false -include_files $false -fields $AssetLayoutFields + New-HuduAssetLayout -name $HuduAssetLayoutName -icon "fas fa-sitemap" -color "#00adef" -icon_color "#000000" -include_passwords $false -include_photos $false -include_comments $false -include_files $false -fields $AssetLayoutFields + $Layout = Get-HuduAssetLayouts -name $HuduAssetLayoutName +} + + +$Company = Get-HuduCompanies -name $CompanyName +if ($company) { + #Upload data to Hudu + $Asset = Get-HuduAssets -name $RawAD.ForestName -companyid $company.id -assetlayoutid $layout.id + + #If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload. + if (!$Asset) { + Write-Host "Creating new Asset" + $Asset = New-HuduAsset -name $RawAD.ForestName -company_id $company.id -asset_layout_id $layout.id -fields $AssetFields + } + else { + Write-Host "Updating Asset" + $Asset = Set-HuduAsset -asset_id $Asset.id -name $RawAD.ForestName -company_id $company.id -asset_layout_id $layout.id -fields $AssetFields + } + +} else { + Write-Host "$CompanyName was not found in Hudu" +}
", "" + +$RawAD = Get-WinADForestInformation + +$ForestRawInfo = new-object PSCustomObject -property $RawAD.ForestInformation | convertto-html -Fragment | Select-Object -Skip 1 +$ForestToc = "
" +$ForestNice = $ForestToc + $TableHeader + ($ForestRawInfo -replace $TableStyling) + $Whitespace + +$SiteRawInfo = $RawAD.Sites | Select-Object 'Site Name', Servers, Subnets | ConvertTo-Html -Fragment | Select-Object -Skip 1 +$SiteHeader = "

AD Forest Physical Structure.

" +$SiteNice = $SiteHeader + $TableHeader + ($SiteRawInfo -replace $TableStyling) + $Whitespace + +$DomainsRawInfo = $(Get-WinADForestInformation).Domains | ForEach-Object { Get-ADDomain $_ | Select-Object Name, NetBIOSName, DomainMode } | ConvertTo-Html -Fragment | Select-Object -Skip 1 +$DomainsHeader = "

AD Forest Logical Structure.

" +$DomainsNice = $DomainsHeader + $TableHeader + ($DomainsRawInfo -replace $TableStyling) + $Whitespace + +$OptionalRawFeatures = new-object PSCustomObject -property $RawAD.OptionalFeatures | convertto-html -Fragment | Select-Object -Skip 1 +$OptionalFeaturesToc = "
" +$OptionalNice = $OptionalFeaturesToc + $TableHeader + ($OptionalRawFeatures -replace $TableStyling) + $Whitespace + +$UPNRawFeatures = $RawAD.UPNSuffixes | convertto-html -Fragment -as list| Select-Object -Skip 1 +$UPNToc = "
" +$UPNNice = $UPNToc + $TableHeader + ($UPNRawFeatures -replace $TableStyling) + $Whitespace + +$DCRawFeatures = $RawAD.GlobalCatalogs| Sort-Object | ForEach-Object { GetBasicDCInfo $_ } | convertto-html -Fragment | Select-Object -Skip 1 +$DCToc = "
" +$DCNice = $DCToc + $TableHeader + ($DCRawFeatures -replace $TableStyling) + $Whitespace + +$DCRawNTPconfig = $RawAD.GlobalCatalogs | Sort-Object | ForEach-Object { (GetTimeServerRegistryKeys $_) } | convertto-html -Fragment | Select-Object -Skip 1 +$NTPToc = "
" +$DCNTPconfigNice = $NTPToc + $TableHeader + ($DCRawNTPconfig -replace $TableStyling) + $Whitespace + +$FSMORawFeatures = $RawAD.FSMO | convertto-html -Fragment | Select-Object -Skip 1 +$FSMOToc = "
" +$FSMONice = $FSMOToc + $TableHeader + ($FSMORawFeatures -replace $TableStyling) + $Whitespace + +#$ForestFunctionalLevel = $RawAD.RootDSE.forestFunctionality +#$DomainFunctionalLevel = $RawAD.RootDSE.domainFunctionality +#$domaincontrollerMaxLevel = $RawAD.RootDSE.domainControllerFunctionality + +$passwordpolicyraw = Get-ADDefaultDomainPasswordPolicy | Select-Object ComplexityEnabled, PasswordHistoryCount, LockoutDuration, LockoutThreshold, MaxPasswordAge, MinPasswordAge | convertto-html -Fragment -As List | Select-Object -skip 1 +$passwordpolicyheader = "
PolicySetting