ADAuditTasks.psm1
#Region '.\Classes\1.ADAuditTasksUser.ps1' 0 class ADAuditTasksUser { [string]$UserName [string]$FirstName [string]$LastName [string]$Name [string]$UPN [string]$LastSignIn [string]$Enabled [string]$LastSeen [string]$OrgUnit [string]$Title [string]$Manager [string]$Department [bool]$AccessRequired [bool]$NeedMailbox ADAuditTasksUser( [string]$UserName, [string]$FirstName, [string]$LastName, [string]$Name, [string]$UPN, [string]$LastSignIn, [string]$Enabled, [string]$LastSeen, [string]$OrgUnit, [string]$Title, [string]$Manager, [string]$Department, [bool]$AccessRequired, [bool]$NeedMailbox ) { $this.UserName = $UserName $this.FirstName = $FirstName $this.LastName = $LastName $this.Name = $Name $this.UPN = $UPN $this.LastSignIn = ([DateTime]::FromFileTime($LastSignIn)) $this.Enabled = $Enabled $this.LastSeen = $( switch (([DateTime]::FromFileTime($LastSeen))) { # Over 90 Days { ($_ -lt (Get-Date).Adddays( - (90))) } { '3+ months'; break } # Over 60 Days { ($_ -lt (Get-Date).Adddays( - (60))) } { '2+ months'; break } # Over 90 Days { ($_ -lt (Get-Date).Adddays( - (30))) } { '1+ month'; break } default { 'Recently' } } # End Switch ) # End LastSeen $this.OrgUnit = $OrgUnit -replace '^.*?,(?=[A-Z]{2}=)' $this.Title = $Title $this.Manager = $( switch ($Manager) { # Over 90 Days { if ($_) { return $true } } { "$((Get-ADUser -Identity $Manager).Name)"; break } # Over 60 Days default { 'NotFound' } } ) # End Manager $this.AccessRequired = $AccessRequired $this.NeedMailbox = $NeedMailbox $this.Department = $Department } } #EndRegion '.\Classes\1.ADAuditTasksUser.ps1' 65 #Region '.\Classes\2.ADAuditTasksComputer.ps1' 0 class ADAuditTasksComputer { [string]$ComputerName [string]$DNSHostName [bool]$Enabled [string]$IPv4Address [string]$IPv6Address [string]$OperatingSystem [string]$LastLogon [string]$Created [string]$Modified [string]$Description [string]$OrgUnit [string]$KerberosEncryptionType [string]$SPNs [string]$GroupMemberships #Computername for Group Membership Search [string]$LastSeen # Constructor 1 ADAuditTasksComputer( [string]$ComputerName, [string]$DNSHostName, [bool]$Enabled, [string]$IPv4Address, [string]$IPv6Address, [string]$OperatingSystem, [long]$LastLogon, [datetime]$Created, [string]$Modified, [string]$Description, [string]$OrgUnit, [string]$KerberosEncryptionType, [string]$SPNs, [string]$GroupMemberships, [long]$LastSeen ) { #Begin Contructor 1 $this.ComputerName = $ComputerName $this.DNSHostName = $DNSHostName $this.Enabled = $Enabled $this.IPv4Address = $IPv4Address $this.IPv6Address = $IPv6Address $this.OperatingSystem = $OperatingSystem $this.LastLogon = ([DateTime]::FromFileTime($LastLogon)) $this.Created = $Created $this.Modified = $Modified $this.Description = $Description $this.OrgUnit = $(($OrgUnit -replace '^.*?,(?=[A-Z]{2}=)') -replace ",", ">") $this.KerberosEncryptionType = $(($KerberosEncryptionType | Select-Object -ExpandProperty $_) -replace ", ", " | ") $this.SPNs = $SPNs $this.GroupMemberships = $(Get-ADGroupMemberof -SamAccountName $GroupMemberships -AccountType ADComputer) $this.LastSeen = $( switch (([DateTime]::FromFileTime($LastSeen))) { # Over 90 Days { ($_ -lt (Get-Date).Adddays( - (90))) } { '3+ months'; break } # Over 60 Days { ($_ -lt (Get-Date).Adddays( - (60))) } { '2+ months'; break } # Over 90 Days { ($_ -lt (Get-Date).Adddays( - (30))) } { '1+ month'; break } default { 'Recently' } } # End Switch ) # End LastSeen }# End Constuctor 1 } #EndRegion '.\Classes\2.ADAuditTasksComputer.ps1' 63 #Region '.\Private\Build-ADAuditTasksComputer.ps1' 0 function Build-ADAuditTasksComputer { param ( $ADComputer ) return [ADAuditTasksComputer]::new( $ADComputer.Name, $ADComputer.DNSHostName, $ADComputer.Enabled, $ADComputer.IPv4Address, $ADComputer.IPv6Address, $ADComputer.OperatingSystem, $ADComputer.lastLogonTimestamp, $ADComputer.Created, $ADComputer.whenChanged, $ADComputer.Description, $ADComputer.DistinguishedName, $(($ADComputer.KerberosEncryptionType).Value.tostring()), ($ADComputer.servicePrincipalName -join " | "), $ADComputer.Name, $ADComputer.lastLogonTimestamp ) } #EndRegion '.\Private\Build-ADAuditTasksComputer.ps1' 25 #Region '.\Private\Build-ADAuditTasksUser.ps1' 0 function Build-ADAuditTasksUser { #Not Finished param ( [Microsoft.ActiveDirectory.Management.ADUser[]]$ADExport ) $Script:LogString += Write-AuditLog -Message "Begin ADAUditTasksUser object creation." $Export = @() foreach ($item in $ADExport) { $Export += [ADAuditTasksUser]::new( $($item.SamAccountName), $($item.GivenName), $($item.Surname), $($item.Name), $($item.UserPrincipalName), $($item.LastLogonTimeStamp), $($item.Enabled), $($item.LastLogonTimeStamp), $($item.DistinguishedName), $($item.Title), $($item.Manager), $($item.Department), $false, $false ) } if ($null -ne $Export) { $Script:LogString += Write-AuditLog -Message "The ADAUditTasksUser object was built successfully." return $Export } } #EndRegion '.\Private\Build-ADAuditTasksUser.ps1' 31 #Region '.\Private\Build-DirectoryPath.ps1' 0 function Build-DirectoryPath { param ( $DirectoryPath ) $AttachmentFolderPathCheck = Test-Path -Path $DirectoryPath If (!($AttachmentFolderPathCheck)) { $Script:LogString += Write-AuditLog -Message "Would you like to create the directory $($DirectoryPath)?" -Severity Warning Try { # If not present then create the dir New-Item -ItemType Directory $DirectoryPath -Force -ErrorAction Stop | Out-Null $Script:LogString += Write-AuditLog -Message $("Directory: " + $DirectoryPath + "was created.") } Catch { $Script:LogString += Write-AuditLog -Message $("Directory: " + $DirectoryPath + "was not created.") -Severity Error $Script:LogString += Write-AuditLog -Message "End Log" throw $_.Exception } # Log creation of output directory $outputMsg = "$("Output Folder created at: `n" + $DirectoryPath)" $Script:LogString += Write-AuditLog -Message $outputMsg } else { $Script:LogString += Write-AuditLog -Message $("Directory: " + $DirectoryPath + " exists already and will be used.") } } #EndRegion '.\Private\Build-DirectoryPath.ps1' 27 #Region '.\Private\Build-MacIdOUIList.ps1' 0 function Build-MacIdOUIList { Write-AuditLog -Message "Retrieving MACID OUI list from https://standards-oui.ieee.org/oui/oui.csv" try { $ouiobject = Invoke-RestMethod https://standards-oui.ieee.org/oui/oui.csv | ConvertFrom-Csv -ErrorAction Stop Write-AuditLog -Message "Successfully downloaded the OUI list!" return $ouiobject } catch { Write-Warning "List not downloaded. Continuing without MACID OUI list." -WarningAction Continue } } #EndRegion '.\Private\Build-MacIdOUIList.ps1' 12 #Region '.\Private\Build-NetScanObject.ps1' 0 function Build-NetScanObject { param( $NetScanObject, [switch]$IncludeNoPing ) $ouiobject = Build-MacIdOUIList $Script:LogString += Write-AuditLog -Message "Begin NetScan object creation." switch ($IncludeNoPing) { $true { $scan = $NetSCanObject } Default { $scan = $NetSCanObject | Where-Object { $_.Ping -eq $true } } } $Export = @() foreach ($Item in $scan) { $portsenabled = ($item.PSObject.Properties | Where-Object { $_.Value -eq $true -and $_.name -ne "Ping" }).Name -join " | " $portsenabled = $portsenabled.Replace("Port ", "") $SaveErrorPref = $Script:ErrorActionPreference $Script:ErrorActionPreference = 'SilentlyContinue' $macid = ((arp -a "$($item.ComputerName)" | Select-String '([0-9a-f]{2}-){5}[0-9a-f]{2}').Matches.Value).Replace("-", ":") $macpop = $macid.replace(":", "") $macsubstr = $macpop.Substring(0, 6) $org = ($ouiobject | Where-Object { $_.assignment -eq $macsubstr })."Organization Name" $Script:ErrorActionPreference = $SaveErrorPref if ($org) { [string]$ManufacturerName = $org } else { [string]$ManufacturerName = "NotFound" } $hash = [ordered]@{ ComputerName = $Item.ComputerName "IP/DNS" = $Item."IP/DNS" Ping = $Item.Ping MacID = $macid ManufacturerName = $ManufacturerName PortsEnabled = $portsenabled } # End Ordered Hash table New-Object -TypeName PSCustomObject -Property $hash -OutVariable PSObject | Out-Null $Export += $PSObject } # End foreach scan if ($Export) { $Script:LogString += Write-AuditLog -Message "NetScan object created!" return $Export } else { throw "The ExportObject was Blank" } } #EndRegion '.\Private\Build-NetScanObject.ps1' 52 #Region '.\Private\Build-ReportArchive.ps1' 0 function Build-ReportArchive { [CmdletBinding()] <# .SYNOPSIS Exports data to a CSV file, archives the CSV file and a log file in a zip file, and returns the path to the zip file. .DESCRIPTION The Build-ReportArchive function exports data to a CSV file, archives the CSV file and a log file in a zip file, and returns the path to the zip file. The function takes four parameters: $Export (the data to export), $csv (the name of the CSV file to create), $zip (the name of the zip file to create), and $log (the name of the log file to create). The function writes information about the export and archive process to the log file, and any errors that occur are also logged. .PARAMETER Export Specifies the data to export. .PARAMETER csv Specifies the name of the CSV file to create. .PARAMETER zip Specifies the name of the zip file to create. .PARAMETER log Specifies the name of the log file to create. .INPUTS The function accepts data as input from the pipeline. .OUTPUTS The function returns the path to the zip file that contains the archived CSV and log files. .EXAMPLE PS C:\> $Export = Get-ADUser -Filter * PS C:\> $CsvFile = "C:\Temp\ExportedData.csv" PS C:\> $ZipFile = "C:\Temp\ExportedData.zip" PS C:\> $LogFile = "C:\Temp\ExportedData.log" PS C:\> Build-ReportArchive -Export $Export -csv $CsvFile -zip $ZipFile -log $LogFile In this example, the Build-ReportArchive function is used to export all AD users to a CSV file, archive the CSV file and a log file in a zip file, and return the path to the zip file. The exported data is passed as input to the function using the $Export parameter, and the names of the CSV, zip, and log files are specified using the $csv, $zip, and $log parameters, respectively. .NOTES This function requires PowerShell 5.0 or later. .LINK https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.archive/compress-archive #> # Define function parameters with help messages param ( [Parameter( HelpMessage = 'Active Directory User Enabled or not. Default $true', Position = 0, ValueFromPipelineByPropertyName = $true )]$Export, [Parameter( HelpMessage = 'CSV File Name', Position = 1, ValueFromPipelineByPropertyName = $true )][string]$csv, [Parameter( HelpMessage = 'Zip File Name', Position = 2, ValueFromPipelineByPropertyName = $true )][string]$zip, [Parameter( HelpMessage = 'Log File Name', Position = 3, ValueFromPipelineByPropertyName = $true )][string]$log ) # Initialize variables begin { $ExportFile = $Export } # Process each object in the pipeline process { try { # Export data to CSV file $ExportFile | Export-Csv $csv -NoTypeInformation -Encoding utf8 -ErrorVariable ExportErr -ErrorAction Stop } catch { # Write error to log and re-throw error $Script:LogString += Write-AuditLog -Message "Failed to export CSV: $csv" -Severity Error throw $ExportErr } # Get SHA-256 hash of the CSV file and write to log $Sha256Hash = (Get-FileHash $csv).Hash $Script:LogString += Write-AuditLog -Message "Exported CSV SHA256 hash: " $Script:LogString += Write-AuditLog -Message "$($Sha256Hash)" # Write information about the export directory and file path to log $Script:LogString += Write-AuditLog -Message "Directory: $AttachmentFolderPath" $Script:LogString += Write-AuditLog -Message "FilePath: $zip" $Script:LogString += Write-AuditLog -Message "End Log" $Script:LogString | Export-Csv $log -NoTypeInformation -Encoding utf8 } # Clean up and archive files end { try { Compress-Archive -Path $csv, $log -DestinationPath $zip -CompressionLevel Optimal -ErrorAction Stop Remove-Item $csv, $log -Force return [string[]]$zip } catch { throw $_.Exception } } } # End Function #EndRegion '.\Private\Build-ReportArchive.ps1' 101 #Region '.\Private\Get-AdExtendedRight.ps1' 0 Function Get-AdExtendedRight([Microsoft.ActiveDirectory.Management.ADObject] $ADObject) { <# .SYNOPSIS Gets the extended rights granted to a specified Active Directory object. .DESCRIPTION The Get-AdExtendedRight function returns a list of dangerous extended rights granted to a specified Active Directory object. The function checks each access control entry in the object's security descriptor to see if it grants an extended right, and if so, it maps the object type of the access control entry to a name of a dangerous extended attribute. .PARAMETER ADObject Specifies the Active Directory object to get extended rights for. .INPUTS The function accepts an Active Directory object as input. .OUTPUTS The function returns a list of dangerous extended rights granted to the specified Active Directory object. The output is an array of PowerShell objects that contain the following properties: - Actor: The security principal that has been granted the extended right. - CanActOnThePermissionof: The name and distinguished name of the Active Directory object that the extended right has been granted to. - WithExtendedRight: The name of the dangerous extended right that has been granted. .EXAMPLE PS C:\> $ADObject = Get-ADUser -Identity "jdoe" PS C:\> $ER = Get-AdExtendedRight -ADObject $ADObject PS C:\> $ER Actor : CONTOSO\ITAdmin CanActOnThePermissionof : jdoe (CN=John Doe,OU=Users,DC=contoso,DC=com) WithExtendedRight : Manage-SID-History Actor : CONTOSO\ITAdmin CanActOnThePermissionof : jdoe (CN=John Doe,OU=Users,DC=contoso,DC=com) WithExtendedRight : User-Force-Change-Password .DESCRIPTION In this example, the Get-AdExtendedRight function is used to get the dangerous extended rights that have been granted to the Active Directory user "jdoe". The function returns an array of two PowerShell objects that contain information about the extended rights that have been granted. .NOTES This function requires the Active Directory module to be installed on the local computer. .LINK https://docs.microsoft.com/en-us/windows/win32/ad/active-directory-extended-rights #> # Initialize an empty array to store extended rights $ExportER = @() # Loop through each access control entry in the object's security descriptor Foreach ($Access in $ADObject.ntsecurityDescriptor.Access) { # Ignore deny permissions, well-known identities, and inherited permissions if ($Access.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny) { continue } if ($Access.IdentityReference -eq "NT AUTHORITY\SYSTEM") { continue } if ($Access.IdentityReference -eq "NT AUTHORITY\SELF") { continue } if ($Access.IsInherited) { continue } # Check if the access control entry grants an extended right if ($Access.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight) { # Initialize an empty string to store the name of the extended right $Right = "" # Map the object type of the access control entry to a name of a dangerous extended attribute # (see https://technet.microsoft.com/en-us/library/ff405676.aspx) switch ($Access.ObjectType) { "00299570-246d-11d0-a768-00aa006e0529" { $Right = "User-Force-Change-Password" } "45ec5156-db7e-47bb-b53f-dbeb2d03c40" { $Right = "Reanimate-Tombstones" } "bf9679c0-0de6-11d0-a285-00aa003049e2" { $Right = "Self-Membership" } "ba33815a-4f93-4c76-87f3-57574bff8109" { $Right = "Manage-SID-History" } "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" { $Right = "DS-Replication-Get-Changes-All" } } # If the access control entry grants a dangerous extended right, add it to the array if ($Right -ne "") { $Rights = [ordered]@{ Actor = $($Access.IdentityReference) CanActOnThePermissionof = "$($ADObject.name)" + " " + "($($ADObject.DistinguishedName))" WithExtendedRight = $Right } $ExportER += New-Object -TypeName PSObject -Property $Rights #"$($Access.IdentityReference) can act on the permission of $($ADObject.name) ($($ADObject.DistinguishedName)) with extended right: $Right" } } } # Return the array of dangerous extended rights return $ExportER } # End Function #EndRegion '.\Private\Get-AdExtendedRight.ps1' 78 #Region '.\Private\Get-ADGroupMemberof.ps1' 0 function Get-ADGroupMemberof { <# .SYNOPSIS Gets the names of the groups that a user or computer is a member of. .DESCRIPTION The Get-ADGroupMemberof function gets the names of the groups that a user or computer is a member of. The function takes two parameters: $SamAccountName (the name of the user or computer) and $AccountType (the type of account, either ADUser or ADComputer). The function uses a switch statement to determine whether to get the groups that a user or computer is a member of, and returns a string containing the names of the groups. .PARAMETER SamAccountName Specifies the name of the user or computer to get the group membership for. .PARAMETER AccountType Specifies the type of account, either ADUser or ADComputer. The default value is ADUser. .OUTPUTS The function returns a string containing the names of the groups that the specified user or computer is a member of. .EXAMPLE PS C:\> Get-ADGroupMemberof -SamAccountName "jdoe" -AccountType "ADUser" In this example, the Get-ADGroupMemberof function is used to get the names of the groups that the user "jdoe" is a member of. The type of account is specified using the $AccountType parameter. .NOTES This function requires the ActiveDirectory PowerShell module. .LINK https://docs.microsoft.com/en-us/powershell/module/activedirectory/ #> [CmdletBinding()] # Define function parameters param ( [string]$SamAccountName, [ValidateSet("ADUser", "ADComputer")] [string]$AccountType = "ADUser" ) # Process the account name and type process { switch ($AccountType) { "ADComputer" { # Get the groups that the computer is a member of $GroupStringArray = ((Get-ADComputer -Identity $SamAccountName -Properties memberof).memberof | Get-ADGroup | Select-Object name | Sort-Object name).name $GroupString = $GroupStringArray -join " | " } Default { # Get the groups that the user is a member of $GroupStringArray = ((Get-ADUser -Identity $SamAccountName -Properties memberof).memberof | Get-ADGroup | Select-Object name | Sort-Object name).name $GroupString = $GroupStringArray -join " | " } } # Return a string containing the names of the groups return $GroupString } } # End Function #EndRegion '.\Private\Get-ADGroupMemberof.ps1' 51 #Region '.\Private\Group-UpdateByProduct.ps1' 0 function Group-UpdateByProduct { param( [Parameter(Mandatory = $true)] [System.Collections.ArrayList]$AllUpdates, [string[]]$OSList ) $Updates = @{} foreach ($OS in $OSList) { $Updates[$OS] = $AllUpdates | Where-Object { $_.Product -like "$OS*" } } return $Updates } #EndRegion '.\Private\Group-UpdateByProduct.ps1' 16 #Region '.\Private\Initialize-ModuleEnv.ps1' 0 function Initialize-ModuleEnv { <# .SYNOPSIS Initializes the environment by installing required PowerShell modules. .DESCRIPTION This function installs PowerShell modules required by the script. It can install public or pre-release versions of the module, and it supports installation for all users or current user. .PARAMETER PublicModuleNames An array of module names to be installed. Required when using the Public parameter set. .PARAMETER PublicRequiredVersions An array of required module versions to be installed. Required when using the Public parameter set. .PARAMETER PrereleaseModuleNames An array of pre-release module names to be installed. Required when using the Prerelease parameter set. .PARAMETER PrereleaseRequiredVersions An array of required pre-release module versions to be installed. Required when using the Prerelease parameter set. .PARAMETER Scope The scope of the module installation. Possible values are "AllUsers" and "CurrentUser". .PARAMETER ImportModuleNames The specific modules you'd like to import from the installed package to streamline imports. Example "Microsoft.Graph.Authentication","Microsoft.Graph.Identity.SignIns" .EXAMPLE Initialize-ModuleEnv -PublicModuleNames "PSnmap", "Microsoft.Graph" -PublicRequiredVersions "1.3.1","1.23.0" -Scope AllUsers This example installs the PSnmap and Microsoft.Graph modules in the AllUsers scope with the specified versions. .EXAMPLE $params1 = @{ PublicModuleNames = "PSnmap","Microsoft.Graph" PublicRequiredVersions = "1.3.1","1.23.0" ImportModuleNames = "Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.SignIns" Scope = "CurrentUser" } Initialize-ModuleEnv @params1 This example installs Microsoft.Graph and Pester Modules in the CurrentUser scope with the specified versions. It will attempt to only import Microsoft.Graph Modules matching the names in the "ImportModulesNames" array. .EXAMPLE $params2 = @{ PrereleaseModuleNames = "Sampler", "Pester" PrereleaseRequiredVersions = "2.1.5", "4.10.1" Scope = "CurrentUser" } Initialize-ModuleEnv @params2 This example installs the PreRelease Sampler and Pester Modules in the CurrentUser scope with the specified versions. Double check https://www.powershellgallery.com/packages/<ModuleName>/<ModuleVersionNumber> to verify if the "-PreRelease" switch is needed. .INPUTS None .OUTPUTS None .NOTES Author: DrIOSx #> [CmdletBinding(DefaultParameterSetName = "Public")] param ( [Parameter(ParameterSetName = "Public", Mandatory)] [string[]]$PublicModuleNames, [Parameter(ParameterSetName = "Public", Mandatory)] [string[]]$PublicRequiredVersions, [Parameter(ParameterSetName = "Prerelease", Mandatory)] [string[]]$PrereleaseModuleNames, [Parameter(ParameterSetName = "Prerelease", Mandatory)] [string[]]$PrereleaseRequiredVersions, [ValidateSet( "AllUsers", "CurrentUser" )] [string]$Scope, [string[]]$ImportModuleNames = $null ) if ($global:MaximumFunctionCount -lt 8192) { $global:MaximumFunctionCount = 8192 } if ($Scope -eq "AllUsers") { switch (Test-IsAdmin) { $false { $Script:LogString += Write-AuditLog -Message "You must be an administrator to install in the `'AllUsers`' scope." -Severity Error $Script:LogString += Write-AuditLog -Message "If you intended to install the module only for this user, select the `'CurrentUser`' scope." -Severity Error throw "Elevation required for `'AllUsers`' scope" } Default { $Script:LogString += Write-AuditLog -Message "You have sufficient privileges to install to the `'AllUsers`' scope." } } } if ($PSCmdlet.ParameterSetName -eq "Public") { $modules = $PublicModuleNames $versions = $PublicRequiredVersions $prerelease = $false } elseif ($PSCmdlet.ParameterSetName -eq "Prerelease") { $modules = $PrereleaseModuleNames $versions = $PrereleaseRequiredVersions $prerelease = $true } foreach ($module in $modules) { $name = $module $version = $versions[$modules.IndexOf($module)] $installedModule = Get-Module -Name $name -ListAvailable switch (($null -eq $ImportModuleNames)) { $false { $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name } $Script:LogString += Write-AuditLog -Message "Attempting to selecively install module/s:" } Default { $SelectiveImports = $null $Script:LogString += Write-AuditLog -Message "Selective imports were not specified. All functions and commands will be imported." } } # Get Module Object switch ($prerelease) { $true { $message = "The PreRelease module $name version $version is not installed. Would you like to install it?" $throwmsg = "You must install the PreRelease module $name version $version to continue" } Default { $message = "The $name module version $version is not installed. Would you like to install it?" $throwmsg = "You must install the $name module version $version to continue." } } if (!$installedModule) { # Install Required Module $Script:LogString += Write-AuditLog -Message $message -Severity Warning try { $Script:LogString += Write-AuditLog -Message "Installing $name module/s version $version -AllowPrerelease:$prerelease." $SaveVerbosePreference = $script:VerbosePreference Install-Module $name -Scope $Scope -RequiredVersion $version -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference $Script:LogString += Write-AuditLog -Message "$name module successfully installed!" if ($SelectiveImports) { foreach ($Mod in $SelectiveImports) { $name = $Mod $Script:LogString += Write-AuditLog -Message "Selectively importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference $Script:LogString += Write-AuditLog -Message "Successfully imported the $name module." } } else { $Script:LogString += Write-AuditLog -Message "Importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference $Script:LogString += Write-AuditLog -Message "Successfully imported the $name module." } } catch { $Script:LogString += Write-AuditLog -Message $throwmsg -Severity Error throw $_.Exception } } else { try { if ($SelectiveImports) { foreach ($Mod in $SelectiveImports) { $name = $Mod $Script:LogString += Write-AuditLog -Message "The $name module was found to be installed." $Script:LogString += Write-AuditLog -Message "Selectively importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference $Script:LogString += Write-AuditLog -Message "Successfully imported the $name module." } } else { $Script:LogString += Write-AuditLog -Message "The $name module was found to be installed." $Script:LogString += Write-AuditLog -Message "Importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference $Script:LogString += Write-AuditLog -Message "Successfully imported the $name module." } } catch { $Script:LogString += Write-AuditLog -Message $throwmsg -Severity Error throw $_.Exception } } } } #EndRegion '.\Private\Initialize-ModuleEnv.ps1' 180 #Region '.\Private\Install-ADModule.ps1' 0 function Install-ADModule { # Setup Variables $SaveVerbosePreference = $script:VerbosePreference $script:VerbosePreference = 'SilentlyContinue' Get-CimInstance -Class Win32_OperatingSystem -ErrorAction Stop -OutVariable OS -Verbose:$false | Out-Null $script:VerbosePreference = $SaveVerbosePreference $OSName = ($OS).Name.Split('|')[0] $OSBuildNumber = $($OS.BuildNumber) $OSVersion = $($OS.Version) try { $SaveVerbosePreference = $script:VerbosePreference Import-Module ActiveDirectory -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference $Script:LogString += Write-AuditLog -Message "The ActiveDirectory Module was successfully imported." $Script:LogString += Write-AuditLog -Message "OS: $OSName Build: $OSBuildNumber, Version: $OSVersion" } catch { if (!(Test-IsAdmin)) { $Script:LogString += Write-AuditLog -Message "You must be run the script as an administrator to install ActiveDirectory module!" $Script:LogString += Write-AuditLog -Message "Once you've installed the module, susequent runs will not need elevation!" throw "Installation requires elevation." } if (($OSBuildNumber -lt 17763) -and ($OSName -notmatch "Windows Server") ) { # Exit Function if windows version is less than Windows 10 October 2018 (1809) $Script:LogString += Write-AuditLog -Message "Get installation instructions and download Remote Server Administration Tools (RSAT):" $Script:LogString += Write-AuditLog -Message "https://www.microsoft.com/en-us/download/details.aspx?id=45520" throw "Install the appropriate RSAT module for $OSName Build: $OSBuildNumber, Version: $OSVersion." } # Write-AuditLog Warning (-WarningAction Inquire) $Script:LogString += Write-AuditLog -Message "The ActiveDirectory module is not installed, would you like attempt to install it?" -Severity Warning try { $Script:LogString += Write-AuditLog -Message "Potentially compatible OS: $OSName Build: $OSBuildNumber, Version: $OSVersion." $Script:LogString += Write-AuditLog -Message "Installing ActiveDirectory Module." # Run the command to install AD module based on OS if ($OSName -match "Windows Server") { # If Windows Server $Script:LogString += Write-AuditLog -Message "OS matched `"Windows Server`"." $Script:LogString += Write-AuditLog -Message "Importing ServerManager Module." Import-Module ServerManager -ErrorAction Stop $Script:LogString += Write-AuditLog -Message "Using Install-WindowsFeature RSAT-AD-PowerShell -IncludeAllSubFeature to install ActiveDirectory Module." Install-WindowsFeature RSAT-AD-PowerShell -IncludeAllSubFeature -ErrorAction Stop } else { # If Windows Client $Script:LogString += Write-AuditLog -Message "OperatingSystem: $OSName is not like `"Windows Server`" and" $Script:LogString += Write-AuditLog -Message "OSBuild: $OSBuildNumber is greater than 17763 (Windows 10 October 2018 (1809) Update)." $Script:LogString += Write-AuditLog -Message "Retrieving RSAT.ActiveDirectory Feature using Get-WindowsCapability -Online" Get-WindowsCapability -Online | ` Where-Object { $_.Name -like "Rsat.ActiveDirectory*" } -ErrorAction Stop -OutVariable ADRSATModule | Out-Null $RSATModuleName = $($ADRSATModule.Name) $Script:LogString += Write-AuditLog -Message "Installing $RSATModuleName features." Add-WindowsCapability -Online -Name $RSATModuleName -ErrorAction Stop } } catch { $Script:LogString += Write-AuditLog -Message "The ActiveDirectory module failed to install." throw $_.Exception } # End Region try/catch ActiveDirectory import finally { try { $Script:LogString += Write-AuditLog -Message "Attempting to import the ActiveDirectory module." $SaveVerbosePreference = $script:VerbosePreference Import-Module ActiveDirectory -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference $Script:LogString += Write-AuditLog -Message "The ActiveDirectory module was imported!" } catch { $Script:LogString += Write-AuditLog -Message "The ActiveDirectory module failed to import." throw $_.Exception } } } # End Import Catch } #EndRegion '.\Private\Install-ADModule.ps1' 75 #Region '.\Private\Read-FileContent.ps1' 0 function Read-FileContent { param( [string]$FilePath ) return (Get-Content -Path $FilePath -Raw) } #EndRegion '.\Private\Read-FileContent.ps1' 7 #Region '.\Private\Show-OSUpdateSection.ps1' 0 function Show-OSUpdateSection { param( $osUpdates ) $sectionHtml = "" foreach ($os in $osUpdates) { $sectionHtml += @" <h3>$($os.Title)</h3> "@ $groupedUpdates = $os.Updates | Group-Object -Property Article foreach ($group in $groupedUpdates) { $firstUpdate = $group.Group[0] $tableId = "table_" + (New-Guid).ToString() $arrowId = "arrow_" + (New-Guid).ToString() $sectionHtml += @" <h4 onclick='toggleTable("$tableId", "$arrowId")' style='cursor:pointer;'><span id='$arrowId' class='arrow'>â–¶</span><span class='kb-number'>KB$($group.Name)</span> - Max Severity: $($firstUpdate.'Max Severity') - <a href='$($firstUpdate.ArticleUrl)' target='_parent'>Article URL</a> | Type: $($firstUpdate.Download) - <a href='$($firstUpdate.'Download Url')' target='_parent'>Download URL</a></h4> <table id='$tableId' style='display:none;'> <tr> <th onclick='onHeaderClick("$tableId", 0)'>Release Date</th> <th onclick='onHeaderClick("$tableId", 1)'>Impact</th> <th onclick='onHeaderClick("$tableId", 2)'>Build Number</th> <th onclick='onHeaderClick("$tableId", 3)'>Details</th> <th onclick='onHeaderClick("$tableId", 4)'>Details URL</th> <th onclick='onHeaderClick("$tableId", 5)'>Base Score</th> </tr> "@ foreach ($update in $group.Group) { $sectionHtml += @" <tr> <td>$($update.'Release Date')</td> <td>$($update.Impact)</td> <td>$($update.'Build Number')</td> <td>$($update.Details)</td> <td><a href='$($update.'Details Url' -replace "(?<=https://)(.*)//", '$1/')' target='_parent'>Link</a></td> <td>$($update.'Base Score')</td> </tr> "@ } $sectionHtml += @" </table> "@ } } return $sectionHtml } #EndRegion '.\Private\Show-OSUpdateSection.ps1' 50 #Region '.\Private\Test-IsAdmin.ps1' 0 function Test-IsAdmin { <# .SYNOPSIS Checks if the current user is an administrator on the machine. .DESCRIPTION This private function returns a Boolean value indicating whether the current user has administrator privileges on the machine. It does this by creating a new WindowsPrincipal object, passing in a WindowsIdentity object representing the current user, and then checking if that principal is in the Administrator role. .INPUTS None. .OUTPUTS Boolean. Returns True if the current user is an administrator, and False otherwise. .EXAMPLE PS C:\> Test-IsAdmin True #> # Create a new WindowsPrincipal object for the current user and check if it is in the Administrator role (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } #EndRegion '.\Private\Test-IsAdmin.ps1' 23 #Region '.\Private\Write-AuditLog.ps1' 0 function Write-AuditLog { <# .SYNOPSIS Writes an audit log entry with a specified message and severity level. .DESCRIPTION The Write-AuditLog function writes an audit log entry to the console, providing information about the time, the version of the module and the function, the PowerShell version, whether the user is an administrator, the user's domain and username, the computer name, the severity level, and the specified message. .PARAMETER Message Specifies the message to include in the audit log entry. This parameter is mandatory. .PARAMETER Severity Specifies the severity level of the audit log entry. Valid values are 'Information', 'Warning', and 'Error'. The default value is 'Information'. .OUTPUTS Returns a pscustomobject representing the audit log entry with the following properties: - Time: The date and time when the log entry was created. - PSVersion: The version of PowerShell. - IsAdmin: Whether the user is an administrator. - User: The domain and username of the user who invoked the function. - HostName: The name of the computer where the function was invoked. - InvokedBy: The name and version of the module and the function. - Severity: The severity level of the audit log entry. - Message: The message included in the audit log entry. .EXAMPLE Write-AuditLog -Message "Successful login." -Severity Information This example writes an audit log entry with the message "Successful login" and the severity level 'Information'. .NOTES This function is intended to be used for auditing purposes to keep track of events happening in a PowerShell script or module. #> [OutputType([pscustomobject])] [CmdletBinding()] # Define the parameters of the function. param( [Parameter( Mandatory = $true, HelpMessage = 'Input a Message string.', Position = 0 )] [ValidateNotNullOrEmpty()] [string]$Message, [Parameter( HelpMessage = 'Information, Warning or Error.', Position = 1 )] [ValidateNotNullOrEmpty()] [ValidateSet('Information', 'Warning', 'Error')] [string]$Severity = 'Information' ) # Switch statement to determine what action to take based on the severity parameter. switch ($Severity) { 'Warning' { Write-Warning $Message -WarningAction Inquire } 'Error' { Write-Error $Message } Default { Write-Verbose $Message } } # Set the error action preference to silently continue. $ErrorActionPreference = "SilentlyContinue" # Define variables to hold information about the command that was invoked. $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*' $FuncName = (Get-PSCallStack)[1].Command $ModuleVer = $MyInvocation.MyCommand.Version.ToString() # Set the error action preference to continue. $ErrorActionPreference = "Continue" # Return a custom object containing the specified properties with their values. return [pscustomobject]@{ Time = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss')) PSVersion = ($PSVersionTable.PSVersion).ToString() PSEdition = ($PSVersionTable.PSEdition).ToString() IsAdmin = $(Test-IsAdmin) User = "$Env:USERDOMAIN\$Env:USERNAME" HostName = $Env:COMPUTERNAME InvokedBy = $( $ModuleName + "/" + $FuncName + '.v' + $ModuleVer ) Severity = $Severity Message = $Message } } #EndRegion '.\Private\Write-AuditLog.ps1' 79 #Region '.\Public\Get-ADActiveUserAudit.ps1' 0 function Get-ADActiveUserAudit { <# .SYNOPSIS Gets active but stale AD User accounts that haven't logged in within the last 90 days by default. .DESCRIPTION Audit's Active Directory taking "days" as the input for how far back to check for a user's last sign in. Output can be piped to a csv manually, or automatically to C:\temp\ADActiveUserAudit or a specified path in "AttachmentFolderPath" using the -Report Switch. Any user account that is enabled and not signed in over 90 days is a candidate for removal. .EXAMPLE PS C:\> Get-ADActiveUserAudit .EXAMPLE PS C:\> Get-ADActiveUserAudit -Report -Verbose .EXAMPLE PS C:\> Get-ADActiveUserAudit -Enabled $false -DaysInactive 30 -AttachmentFolderPath "C:\temp\MyNewFolderName" -Report -Verbose .PARAMETER Report Add report output as csv to DirPath directory. .PARAMETER AttachmentFolderPath Default path is C:\temp\ADActiveUserAudit. This is the folder where attachments are going to be saved. .PARAMETER Enabled If "$false", will also search disabled users. .PARAMETER DaysInactive How far back in days to look for sign ins. Outside of this window, users are considered "Inactive" .NOTES Outputs to C:\temp\ADActiveUserAudit by default. For help type: help Get-ADActiveUserAudit -ShowWindow #> [OutputType([ADAuditTasksUser])] [CmdletBinding()] param ( [Parameter( HelpMessage = 'Active Directory User Enabled or not. Default $true', Position = 0, ValueFromPipelineByPropertyName = $true )] [bool]$Enabled = $true, [Parameter( HelpMessage = 'Days back to check for recent sign in. Default: 90 days', Position = 1, ValueFromPipelineByPropertyName = $true )] [int]$DaysInactive = 90, [Parameter( HelpMessage = 'Enter output folder path. Default: C:\temp\ADActiveUserAudit', Position = 2, ValueFromPipeline = $true )] [string]$AttachmentFolderPath = "C:\temp\ADActiveUserAudit", [Parameter( HelpMessage = 'Switch to export output to a csv and zipped to Directory C:\temp. Default: $false', Position = 3, ValueFromPipelineByPropertyName = $true )] [switch]$Report ) begin { #Create logging object $Script:LogString = @() #Begin Logging $Script:LogString += Write-AuditLog -Message "Begin Log" $Script:LogString += Write-AuditLog -Message "###############################################" $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*' ### ActiveDirectory Module Install try { Install-ADModule -ErrorAction Stop -Verbose } catch { throw $_.Exception } ### End ADModule Install # Create Directory Path if it does not exist. Build-DirectoryPath -DirectoryPath $AttachmentFolderPath # Gather ADUser Properties to search for. $propsArray = "SamAccountName", "GivenName", "Surname", "Name", "UserPrincipalName", "LastLogonTimeStamp", "Enabled", "LastLogonTimeStamp", "DistinguishedName", "Title", "Manager", "Department" # Log the properties being retrieved. $Script:LogString += Write-AuditLog -Message "###############################################" $Script:LogString += Write-AuditLog -Message "Retrieving the following ADUser properties: " $Script:LogString += Write-AuditLog -Message "$($propsArray -join " | ")" # Establish timeframe to review. $time = (Get-Date).Adddays( - ($DaysInactive)) # Log the search criteria. $Script:LogString += Write-AuditLog -Message "Searching for users who have not signed in within the last $DaysInactive days." $Script:LogString += Write-AuditLog -Message "Where property Enabled = $Enabled" # Pause for 2 seconds to avoid potential race conditions. Start-Sleep 2 } process { # Get Users Get-ADUser -Filter { LastLogonTimeStamp -lt $time -and Enabled -eq $Enabled } ` -Properties $propsArray -OutVariable ADExport | Out-Null # Create custom object for the output $Export = Build-ADAuditTasksUser -ADExport $ADExport } # End Process end { # Log success message. $Script:LogString += Write-AuditLog -Message "The $ScriptFunctionName Export was successful." # Log output object properties. $Script:LogString += Write-AuditLog -Message "There are $($Export.Count) objects listed with the following properties: " $Script:LogString += Write-AuditLog -Message "$(($Export | Get-Member -MemberType property ).Name -join " | ")" # Export to csv and zip, if requested. if ($Report) { # Add Datetime to filename. $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($env:USERDNSDOMAIN)" # Create FileNames. $csv = "$ExportFileName.csv" $zip = "$ExportFileName.zip" $log = "$ExportFileName.AuditLog.csv" # Call the Build-ReportArchive function to create the archive. Build-ReportArchive -Export $Export -csv $csv -zip $zip -log $log -ErrorAction SilentlyContinue -ErrorVariable BuildErr } else { # Log message indicating that the function is returning the output object. $Script:LogString += Write-AuditLog -Message "Returning output object." Start-Sleep 2 return $Export } } } #EndRegion '.\Public\Get-ADActiveUserAudit.ps1' 133 #Region '.\Public\Get-ADHostAudit.ps1' 0 function Get-ADHostAudit { <# .SYNOPSIS Active Directory Server and Workstation Audit with Report export option (Can also be piped to CSV if Report isn't specified). .DESCRIPTION Audit's Active Directory taking "days" as the input for how far back to check for a device's last sign in. Output can be piped to a csv manually, or automatically to C:\temp\ADHostAudit or a specified path in "AttachmentFolderPath" using the -Report Switch. Use the Tab key to cycle through the -HostType Parameter. .EXAMPLE PS C:\> Get-ADHostAudit -HostType WindowsServers -Report -Verbose .EXAMPLE PS C:\> Get-ADHostAudit -HostType WindowsWorkstations -Report -Verbose .EXAMPLE PS C:\> Get-ADHostAudit -HostType "Non-Windows" -Report -Verbose .EXAMPLE PS C:\> Get-ADHostAudit -OSType "2008" -DirPath "C:\Temp\" -Report -Verbose .PARAMETER HostType Select from WindowsServers, WindowsWorkstations or Non-Windows. .PARAMETER OSType Search an OS String. There is no need to add wildcards. .PARAMETER DaystoConsiderAHostInactive How far back in days to look for sign ins. Outside of this window, hosts are considered "Inactive" .PARAMETER Report Add report output as csv to DirPath directory. .PARAMETER AttachmentFolderPath Default path is C:\temp\ADHostAudit. This is the folder where attachments are going to be saved. .PARAMETER Enabled If "$false", will also search disabled computers. .NOTES Outputs to C:\temp\ADHostAudit by default. For help type: help Get-ADHostAudit -ShowWindow #> [OutputType([pscustomobject])] [CmdletBinding(DefaultParameterSetName = 'HostType')] param ( [ValidateSet("WindowsServers", "WindowsWorkstations", "Non-Windows")] [Parameter( ParameterSetName = 'HostType', Mandatory = $true, Position = 0, HelpMessage = 'Name filter attached to users.', ValueFromPipeline = $true )] [string]$HostType, [Parameter( Mandatory = $true, ParameterSetName = 'OSType', Position = 0, HelpMessage = 'Enter a Specific OS Name or first few letters of the OS to Search for in ActiveDirectory', ValueFromPipeline = $true )] [string]$OSType, [Parameter( Position = 1, HelpMessage = 'How many days back to consider an AD Computer last sign in as active', ValueFromPipelineByPropertyName = $true )] [int]$DaystoConsiderAHostInactive = 90, [Parameter( Position = 2, HelpMessage = 'Switch to output to directory specified in DirPath parameter', ValueFromPipelineByPropertyName = $true )] [switch]$Report, [Parameter( Position = 3, HelpMessage = 'Enter the working directory you wish the report to save to. Default creates C:\temp' )] [string]$AttachmentFolderPath = 'C:\temp\ADHostAudit', [Parameter( HelpMessage = 'Search for Enabled or Disabled hosts', ValueFromPipelineByPropertyName = $true )] [bool]$Enabled = $true ) begin { # Create logging object $Script:LogString = @() # Begin Logging $Script:LogString += Write-AuditLog -Message "Begin Log" $Script:LogString += Write-AuditLog -Message "###############################################" # Get the name of the script function $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*' # Check if the Active Directory module is installed and install it if necessary try { Install-ADModule -ErrorAction Stop -Verbose } catch { throw $_.Exception } ### End ADModule Install # Calculate the time that is considered a host inactive $time = (Get-Date).Adddays( - ($DaystoConsiderAHostInactive)) # Check if the attachment folder exists and create it if it does not Build-DirectoryPath -DirectoryPath $AttachmentFolderPath # Determine the host type and set the appropriate search criteria switch ($PsCmdlet.ParameterSetName) { 'HostType' { if ($HostType -eq "WindowsWorkstations") { $FileSuffix = "Workstations" $Script:LogString += Write-AuditLog -Message "###############################################" $Script:LogString += Write-AuditLog -Message "Searching Windows Workstations......" Start-Sleep 2 } elseif ($HostType -eq "Non-Windows") { $POSIX = $true $FileSuffix = "Non-Windows" $Script:LogString += Write-AuditLog -Message "###############################################" $Script:LogString += Write-AuditLog -Message "Searching Non-Windows Computer Objects......" Start-Sleep 2 } elseif ($HostType -eq "WindowsServers") { $OSPicked = "*Server*" $FileSuffix = "Servers" $Script:LogString += Write-AuditLog -Message "###############################################" $Script:LogString += Write-AuditLog -Message "Searching Windows Servers......" Start-Sleep 2 } } 'OSType' { $OSPicked = '*' + $OSType + '*' $FileSuffix = $OSType $Script:LogString += Write-AuditLog -Message "###############################################" $Script:LogString += Write-AuditLog -Message "Searching OSType $OsType......" Start-Sleep 2 } } # Set the properties to retrieve for the host objects $propsArray = ` "Created", ` "Description", ` "DNSHostName", ` "Enabled", ` "IPv4Address", ` "IPv6Address", ` "KerberosEncryptionType", ` "lastLogonTimestamp", ` "Name", ` "OperatingSystem", ` "DistinguishedName", ` "servicePrincipalName", ` "whenChanged" } # End Begin process { # Log the search criteria $Script:LogString += Write-AuditLog -Message "Searching computers that have logged in within the last $DaystoConsiderAHostInactive days." $Script:LogString += Write-AuditLog -Message "Where property Enabled = $Enabled" Start-Sleep 2 # Determine the Active Directory computers to include in the report if ($OSPicked) { $Script:LogString += Write-AuditLog -Message "And Operating System is like: $OSPicked." Get-ADComputer -Filter { (LastLogonTimeStamp -gt $time) -and (Enabled -eq $Enabled) -and (OperatingSystem -like $OSPicked) }` -Properties $propsArray | Select-Object $propsArray -OutVariable ADComps | Out-Null } elseif ($POSIX) { $Script:LogString += Write-AuditLog -Message "And Operating System is: Non-Windows(POSIX)." Get-ADComputer -Filter { OperatingSystem -notlike "*windows*" -and OperatingSystem -notlike "*server*" -and Enabled -eq $Enabled -and lastlogontimestamp -gt $time }` -Properties $propsArray | Select-Object $propsArray -OutVariable ADComps | Out-Null } else { $Script:LogString += Write-AuditLog -Message "And Operating System is -like `"*windows*`" -and Operating System -notlike `"*server*`" (Workstations)." Get-ADComputer -Filter { OperatingSystem -like "*windows*" -and OperatingSystem -notlike "*server*" -and Enabled -eq $Enabled -and lastlogontimestamp -gt $time } ` -Properties $propsArray | Select-Object $propsArray -OutVariable ADComps | Out-Null } # Create a new object for each Active Directory computer with the selected properties and store the results in an array $ADCompExport = foreach ($item in $ADComps) { Build-ADAuditTasksComputer -ADComputer $item } # End foreach Item in ADComps # Convert the objects to PSCustomObjects and store the results in an array # Convert the objects to PSCustomObjects and store the results in an array $Export = @() foreach ($Comp in $ADCompExport) { $hash = [ordered]@{ DNSHostName = $Comp.DNSHostName ComputerName = $Comp.ComputerName Enabled = $Comp.Enabled IPv4Address = $Comp.IPv4Address IPv6Address = $Comp.IPv6Address OperatingSystem = $Comp.OperatingSystem LastLogon = $Comp.LastLogon LastSeen = $Comp.LastSeen Created = $Comp.Created Modified = $Comp.Modified Description = $Comp.Description GroupMemberships = $Comp.GroupMemberships OrgUnit = $Comp.OrgUnit KerberosEncryptionType = $Comp.KerberosEncryptionType SPNs = $Comp.SPNs } # End Ordered Hash table New-Object -TypeName PSCustomObject -Property $hash -OutVariable PSObject | Out-Null $Export += $PSObject } # End foreach Comp in ADCompExport } # End Process end { # If there the export is not empty if ($Export) { # Create a message that lists the properties that were exported $ExportMembers = "Export: $(($Export | Get-Member -MemberType noteproperty ).Name -join " | ")" # Log a successful export message and list the exported properties and the number of objects exported $Script:LogString += Write-AuditLog -Message "The $ScriptFunctionName Export was successful." $Script:LogString += Write-AuditLog -Message "There are $($Export.Count) objects listed with the following properties: " $Script:LogString += Write-AuditLog -Message "$ExportMembers" # If the -Report switch is used, create a report archive and log the output if ($Report) { # Add Datetime to filename $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($env:USERDNSDOMAIN)" # Create FileNames $csv = "$ExportFileName.$FileSuffix.csv" $zip = "$ExportFileName.$FileSuffix.zip" $log = "$ExportFileName.$FileSuffix.AuditLog.csv" Build-ReportArchive -Export $Export -csv $csv -zip $zip -log $log -ErrorVariable BuildErr } # If the -Report switch is not used, return the output object else { $Script:LogString += Write-AuditLog -Message "Returning output object." Start-Sleep 2 return $Export } } else { # If there is no output, log message and create an audit log file $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($env:USERDNSDOMAIN)" $log = "$ExportFileName.$FileSuffix.AuditLog.csv" $Script:LogString += Write-AuditLog "There is no output for the specified host type $FileSuffix" $Script:LogString | Export-Csv $log -NoTypeInformation -Encoding utf8 # If the -Report switch is not used, return null if (-not $Report) { return $null } else { return $log } } } # End End } #EndRegion '.\Public\Get-ADHostAudit.ps1' 238 #Region '.\Public\Get-ADUserLogonAudit.ps1' 0 function Get-ADUserLogonAudit { <# .SYNOPSIS Retrieves the most recent LastLogon timestamp for a specified Active Directory user account from all domain controllers and outputs it as a DateTime object. .DESCRIPTION This function takes a SamAccountName input parameter for a specific user account and retrieves the most recent LastLogon timestamp for that user from all domain controllers in the Active Directory environment. It then returns the LastLogon timestamp as a DateTime object. The function also checks the availability of each domain controller before querying it, and writes an audit log with a list of available and unavailable domain controllers. .PARAMETER SamAccountName Specifies the SamAccountName of the user account to be checked for the most recent LastLogon timestamp. .INPUTS A SamAccountName string representing the user account to be checked. .OUTPUTS A DateTime object representing the most recent LastLogon timestamp for the specified user account. .EXAMPLE Get-ADUserLogonAudit -SamAccountName "jdoe" Retrieves the most recent LastLogon timestamp for the user account with the SamAccountName "jdoe" from all domain controllers in the Active Directory environment. .NOTES This function is designed to be run on the primary domain controller, but it can be run on any domain controller in the environment. It requires the Active Directory PowerShell module and appropriate permissions to read user account data. The function may take some time to complete if the Active Directory environment is large or the domain controllers are geographically distributed. #> [CmdletBinding()] [OutputType([datetime])] param ( [Alias("Identity", "UserName", "Account")] [Parameter( Mandatory = $true, HelpMessage = 'Enter the SamAccountName', ValueFromPipeline = $true )] $SamAccountName ) process { #Create logging object $Script:LogString = @() #Begin Logging $Script:LogString += Write-AuditLog -Message "Begin Log" $Script:LogString += Write-AuditLog -Message "###############################################" # Check if the Active Directory module is installed and install it if necessary try { Install-ADModule -ErrorAction Stop -Verbose } catch { throw $_.Exception } ### End ADModule Install #Get all domain controllers $DomainControllers = Get-ADDomainController -Filter { Name -like "*" } $Comps = $DomainControllers.name #Create a hash table to store the parameters for Get-ADObject command $Params = @{} $Params.ComputerName = @() #Create a hash table to store domain controllers that are not available for queries $NoRemoteAccess = @{} $NoRemoteAccess.NoRemoteAccess = @() #Loop through all domain controllers to check for remote access foreach ($comp in $comps) { $testRemoting = Test-WSMan -ComputerName $comp -ErrorAction SilentlyContinue if ($null -ne $testRemoting ) { $params.ComputerName += $comp } else { $NoRemoteAccess.NoRemoteAccess += $comp } } #Write audit logs for domain controllers that are available for queries if ($params.ComputerName) { $Script:LogString += Write-AuditLog -Message "The following DC's were available for WSMan:" $Script:LogString += Write-AuditLog -Message "$($params.ComputerName)" } #Write audit logs for domain controllers that are not available for queries if ($NoRemoteAccess.NoRemoteAccess) { $Script:LogString += Write-AuditLog -Message "The following DC's were unavailable and weren't included:" $Script:LogString += Write-AuditLog -Message "$($NoRemoteAccess.NoRemoteAccess)" } #Get the AD user object based on the given SamAccountName $user = Get-ADUser -Identity $SamAccountName #Initialize a variable to store the latest lastLogon time $time = 0 #Initialize an array to store DateTime objects from all domain controllers $dt = @() #Loop through all domain controllers to get the lastLogon time of the user foreach ($dc in $params.ComputerName) { $user | Get-ADObject -Server $dc -Properties lastLogon -OutVariable usertime -ErrorAction SilentlyContinue | Out-Null if ($usertime.LastLogon -gt $time) { $time = $usertime.LastLogon } $dt += [DateTime]::FromFileTime($time) } #Sort the array of DateTime objects in descending order and return the latest DateTime object return ($dt | Sort-Object -Descending)[0] } } #EndRegion '.\Public\Get-ADUserLogonAudit.ps1' 99 #Region '.\Public\Get-ADUserPrivilegeAudit.ps1' 0 function Get-ADUserPrivilegeAudit { <# .SYNOPSIS Produces three object outputs: PrivilegedGroups, AdExtendedRights, and possible service accounts. .DESCRIPTION The Get-ADUserPrivilegeAudit function produces reports on privileged groups, AD extended rights, and possible service accounts. If the -Report switch is used, the reports will be created in the specified folder. To instantiate variables with the objects, provide three objects on the left side of the assignment: Example: $a,$b,$c = Get-ADUserPrivilegeAudit -Verbose The objects will be populated with privileged groups, AD extended rights, and possible service accounts, respectively. .EXAMPLE Get-ADUserPrivilegeAudit -Verbose Gets the reports as three separate objects. To instantiate variables with the objects, provide three objects on the left side of the assignment: Example: $a,$b,$c = Get-ADUserPrivilegeAudit -Verbose The objects will be populated with privileged groups, AD extended rights, and possible service accounts, respectively. .EXAMPLE Get-ADUserPrivilegeAudit -Report -Verbose Returns three reports to the default folder, C:\temp\ADUserPrivilegeAudit, in a single zip file. .PARAMETER AttachmentFolderPath Specifies the path of the folder where you want to save attachments. The default path is C:\temp\ADUserPrivilegeAudit. .PARAMETER Report Adds report output as CSV to the directory specified by AttachmentFolderPath. .NOTES This function requires the ActiveDirectory module. #> [CmdletBinding()] param ( # Input parameter: output folder path for generated reports [Parameter( HelpMessage = ' Enter output folder path. Default: C:\temp\ADUserPrivilegeAudit ', Position = 0, ValueFromPipeline = $true )] [string]$AttachmentFolderPath = 'C:\temp\ADUserPrivilegeAudit', # Input parameter: switch to export output to a CSV and zip to the specified directory [Parameter( HelpMessage = 'Switch to export output to a CSV and zipped to Directory C:\temp\ADUserPrivilegeAudit Default: $false', Position = 1, ValueFromPipelineByPropertyName = $true )] [switch]$Report ) begin { # Create logging object $Script:LogString = @() # Begin Logging $Script:LogString += Write-AuditLog -Message "Begin Log" $Script:LogString += Write-AuditLog -Message "###############################################" # Get name of the function $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*' # Check if ActiveDirectory module is installed ### ActiveDirectory Module Install try { Install-ADModule -ErrorAction Stop -Verbose } catch { throw $_.Exception } ### End ADModule Install # Create output directory if it does not already exist Build-DirectoryPath -DirectoryPath $AttachmentFolderPath # Create Privilege Groups Array. $AD_PrivilegedGroups = @( 'Enterprise Admins', 'Schema Admins', 'Domain Admins', 'Administrators', 'Cert Publishers', 'Account Operators', 'Server Operators', 'Backup Operators', 'Print Operators', 'DnsAdmins', 'DnsUpdateProxy', 'DHCP Administrators' ) # Time Variables $time90 = (Get-Date).Adddays( - (90)) $time60 = (Get-Date).Adddays( - (60)) $time30 = (Get-Date).Adddays( - (30)) # Create Arrays $members = @() $ADUsers = @() # AD Groups to search for. $Script:LogString += Write-AuditLog -Message "###############################################" $Script:LogString += Write-AuditLog -Message "Retriving info from the following priveledged groups: " $Script:LogString += Write-AuditLog -Message "$($AD_PrivilegedGroups -join " | ")" Start-Sleep 2 } process { # Iterate through each group in $AD_PrivilegedGroups foreach ($group in $AD_PrivilegedGroups) { # Clear the GroupMember variable and retrieve all members of the current group Clear-Variable GroupMember -ErrorAction SilentlyContinue Get-ADGroupMember -Identity $group -Recursive -OutVariable GroupMember | Out-Null # Select the desired properties for each member and add custom properties to the output $GroupMember | Select-Object SamAccountName, Name, ObjectClass, ` @{N = 'PriviledgedGroup'; E = { $group } }, ` @{N = 'Enabled'; E = { (Get-ADUser -Identity $_.samaccountname).Enabled } }, ` @{N = 'PasswordNeverExpires'; E = { (Get-ADUser -Identity $_.samaccountname -Properties PasswordNeverExpires).PasswordNeverExpires } }, ` @{N = 'LastLogin'; E = { [DateTime]::FromFileTime((Get-ADUser -Identity $_.samaccountname -Properties lastLogonTimestamp).lastLogonTimestamp) } }, ` @{N = 'LastSeen'; E = { switch ([DateTime]::FromFileTime((Get-ADUser -Identity $_.samaccountname -Properties lastLogonTimestamp).lastLogonTimestamp)) { # Over 90 Days { ($_ -lt $time90) } { '3+ months'; break } # Over 60 Days { ($_ -lt $time60) } { '2+ months'; break } # Over 90 Days { ($_ -lt $time30) } { '1+ month'; break } default { 'Recently' } } } }, ` @{N = 'OrgUnit'; E = { $_.DistinguishedName -replace '^.*?,(?=[A-Z]{2}=)' } }, ` @{N = 'GroupMemberships'; E = { Get-ADGroupMemberof -SamAccountName $_.samaccountname } }, ` Title, ` @{N = 'Manager'; E = { (Get-ADUser -Identity $_.manager).Name } }, ` @{N = 'SuspectedSvcAccount'; E = { # Check if the account is a suspected service account based on PasswordNeverExpires or servicePrincipalName if (((Get-ADUser -Identity $_.samaccountname -Properties PasswordNeverExpires).PasswordNeverExpires) -or (((Get-ADUser -Identity $_.samaccountname -Properties servicePrincipalName).servicePrincipalName) -ne $null) ) { return $true } else { return $false } } # End Expression }, # End Named Expression SuspectedSvcAccount Department, AccessRequired, NeedMailbox -OutVariable members | Out-Null # Add the member objects to $ADUsers array $ADUsers += $members } # Create an array to store the output objects $Export = @() # Iterate through each member in $ADUsers and create a custom object with desired properties foreach ($User in $ADUsers) { $hash = [ordered]@{ PriviledgedGroup = $User.PriviledgedGroup SamAccountName = $User.SamAccountName Name = $User.Name ObjectClass = $User.ObjectClass LastLogin = $User.LastLogin LastSeen = $User.LastSeen GroupMemberships = $User.GroupMemberships Title = $User.Title Manager = $User.Manager Department = $User.Department OrgUnit = $User.OrgUnit Enabled = $User.Enabled PasswordNeverExpires = $User.PasswordNeverExpires SuspectedSvcAccount = $User.SuspectedSvcAccount AccessRequired = $false NeedMailbox = $true } New-Object -TypeName PSCustomObject -Property $hash -OutVariable PSObject | Out-Null $Export += $PSObject } # Log success message for $ScriptFunctionName export $Script:LogString += Write-AuditLog -Message "The $ScriptFunctionName Export was successful." # Log count and properties of objects in $Export $Script:LogString += Write-AuditLog -Message "There are $($Export.Count) objects listed with the following properties: " $Script:LogString += Write-AuditLog -Message "$(($Export | Get-Member -MemberType noteproperty ).Name -join " | ")" # Get PDC $dc = (Get-ADDomainController -Discover -DomainName $env:USERDNSDOMAIN -Service PrimaryDC).Name # Get DN of AD Root. $rootou = (Get-ADRootDSE).defaultNamingContext # Get AD objects from the PDC for the root ou. #TODO Check $Allobjects = Get-ADObject -Server $dc -SearchBase $rootou -SearchScope subtree -LDAPFilter ` "(&(objectclass=user)(objectcategory=person))" -Properties ntSecurityDescriptor -ResultSetSize $null # Create $Export2 object by looping through all objects in $Allobjects and retrieving extended rights $Export2 = Foreach ($ADObject in $Allobjects) { Get-AdExtendedRight $ADObject } # Log success message for extended permissions export $Script:LogString += Write-AuditLog -Message "The Extended Permissions Export was successful." # Log count and properties of objects in $Export2 $Script:LogString += Write-AuditLog -Message "There are $($Export2.Count) objects listed with the following properties: " $Script:LogString += Write-AuditLog -Message "$(($Export2 | Get-Member -MemberType noteproperty ).Name -join " | ")" # Export Delegated access, allowed protocols, and Destination Services by filtering for relevant properties $Export3 = Get-ADObject -Filter { (msDS-AllowedToDelegateTo -like '*') -or (UserAccountControl -band 0x0080000) -or (UserAccountControl -band 0x1000000) } ` -prop samAccountName, msDS-AllowedToDelegateTo, servicePrincipalName, userAccountControl | ` Select-Object DistinguishedName, ObjectClass, samAccountName, ` @{N = 'servicePrincipalName'; E = { $_.servicePrincipalName -join " | " } }, ` @{N = 'DelegationStatus'; E = { if ($_.UserAccountControl -band 0x80000) { 'AllServices' }else { 'SpecificServices' } } }, ` @{N = 'AllowedProtocols'; E = { if ($_.UserAccountControl -band 0x1000000) { 'Any' }else { 'Kerberos' } } }, ` @{N = 'DestinationServices'; E = { $_.'msDS-AllowedToDelegateTo' } } # Log success message for delegated permissions export $Script:LogString += Write-AuditLog -Message "The delegated permissions Export was successful." # Log count and properties of objects in $Export3 $Script:LogString += Write-AuditLog -Message "There are $($Export3.Count) objects listed with the following properties: " $Script:LogString += Write-AuditLog -Message "$(($Export3 | Get-Member -MemberType noteproperty ).Name -join " | ")" } end { if ($Report) { # Add Datetime to filename $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($env:USERDNSDOMAIN)" # Create FileNames $csv1 = "$ExportFileName.csv" $csv2 = "$ExportFileName.ExtendedPermissions.csv" $csv3 = "$ExportFileName.PossibleServiceAccounts.csv" $zip1 = "$ExportFileName.zip" $log = "$ExportFileName.AuditLog.csv" # Export results to CSV files $Export | Export-Csv $csv1 $Export2 | Export-Csv $csv2 $Export3 | Export-Csv $csv3 # Compute SHA256 hash for each CSV file $csv1Sha256Hash = (Get-FileHash $csv1).Hash $csv2Sha256Hash = (Get-FileHash $csv2).Hash $csv3Sha256Hash = (Get-FileHash $csv3).Hash # Log SHA256 hash for each CSV file $Script:LogString += Write-AuditLog -Message "Exported CSV $csv1 SHA256 hash: " $Script:LogString += Write-AuditLog -Message "$($csv1Sha256Hash)" $Script:LogString += Write-AuditLog -Message "Exported CSV $csv2 SHA256 hash: " $Script:LogString += Write-AuditLog -Message "$($csv2Sha256Hash)" $Script:LogString += Write-AuditLog -Message "Exported CSV $csv3 SHA256 hash: " $Script:LogString += Write-AuditLog -Message "$($csv3Sha256Hash)" # Log directory path and ZIP file path $Script:LogString += Write-AuditLog -Message "Directory: $AttachmentFolderPath" $Script:LogString += Write-AuditLog -Message "Returning string filepath of: " $Script:LogString += Write-AuditLog -Message "FilePath: $zip1" # Export audit log to CSV file $Script:LogString | Export-Csv $log -NoTypeInformation -Encoding utf8 # Compress CSV files and audit log into a ZIP file Compress-Archive $csv1, $csv2, $csv3, $log -DestinationPath $zip1 -CompressionLevel Optimal # Remove CSV and audit log files Remove-Item $csv1, $csv2, $csv3, $log -Force # Return ZIP file path return $zip1 } else { # Return output objects $Script:LogString += Write-AuditLog -Message "Returning 3 output objects. Create object like this: `$a, `$b, `$c, = Get-ADUserPrivilegedAudit" Start-Sleep 2 return $Export, $Export2, $Export3 } } } #EndRegion '.\Public\Get-ADUserPrivilegeAudit.ps1' 256 #Region '.\Public\Get-ADUserWildCardAudit.ps1' 0 function Get-ADUserWildCardAudit { <# .SYNOPSIS Takes a search string to find commonly named accounts. .DESCRIPTION Takes a search string to find commonly named accounts. For example: If you commonly name service accounts with the prefix "svc", Use "svc" for the WildCardIdentifier to search for names that contain "svc" .EXAMPLE Get-ADUserWildCardAudit -WildCardIdentifier "svc" -Report -Verbose Searches for all user accounts that are named like the search string "svc". .PARAMETER Report Add report output as csv to AttachmentFolderPath directory. .PARAMETER AttachmentFolderPath Default path is C:\temp\ADUserWildCardAudit. This is the folder where attachments are going to be saved. .PARAMETER Enabled If "$false", will also search disabled users. .PARAMETER DaysInactive How far back in days to look for sign ins. Outside of this window, users are considered "Inactive" .PARAMETER WildCardIdentifier The search string to look for in the name of the account. Case does not matter. Do not add a wildcard (*) as it will do this automatically. #> [OutputType([ADAuditTasksUser])] [CmdletBinding()] param ( [Parameter( HelpMessage = 'Active Directory User Enabled or not. Default $true', Position = 0, ValueFromPipelineByPropertyName = $true )] [bool]$Enabled = $true, [Parameter( HelpMessage = 'Days back to check for recent sign in. Default: 90 days', Position = 1, ValueFromPipelineByPropertyName = $true )] [int]$DaysInactive = 90, [Parameter( Mandatory = $true, HelpMessage = 'Name filter attached to users.', ValueFromPipelineByPropertyName = $true )] [string]$WildCardIdentifier, [Parameter( HelpMessage = 'Enter output folder path. Default: C:\temp\ADUserWildCardAudit', Position = 3, ValueFromPipeline = $true )] [string]$AttachmentFolderPath = "C:\temp\ADUserWildCardAudit", [Parameter( HelpMessage = 'Switch to export output to a csv and zipped to Directory C:\temp. Default: $false', Position = 4, ValueFromPipelineByPropertyName = $true )] [switch]$Report ) begin { #Create logging object $Script:LogString = @() #Begin Logging $Script:LogString += Write-AuditLog -Message "Begin Log" $Script:LogString += Write-AuditLog -Message "###############################################" $ScriptFunctionName = $MyInvocation.MyCommand.Name -replace '\..*' ### ActiveDirectory Module Install try { Install-ADModule -ErrorAction Stop -Verbose } catch { throw $_.Exception } ### End ADModule Install # Create Directory Path Build-DirectoryPath -DirectoryPath $AttachmentFolderPath # ADUser Properties to search for. $propsArray = "SamAccountName", "GivenName", "Surname", "Name", "UserPrincipalName", "LastLogonTimeStamp", "Enabled", "LastLogonTimeStamp", "DistinguishedName", "Title", "Manager", "Department" $Script:LogString += Write-AuditLog -Message "###############################################" $Script:LogString += Write-AuditLog -Message "Retriving the following ADUser properties: " $Script:LogString += Write-AuditLog -Message "$($propsArray -join " | ")" # Establish timeframe to review. $Script:LogString += Write-AuditLog -Message "Searching for accounts using search string `"$WildCardIdentifier`" " Start-Sleep 2 } process { # Get Users $WildCardIdentifierstring = '*' + $WildCardIdentifier + '*' Get-ADUser -Filter { Name -like $WildCardIdentifierstring } ` -Properties $propsArray -OutVariable ADExport | Out-Null $Script:LogString += Write-AuditLog -Message "Creating a custom object from ADUser output." $Export = Build-ADAuditTasksUser -ADExport $ADExport } end { $Script:LogString += Write-AuditLog -Message "The $ScriptFunctionName Export was successful." $Script:LogString += Write-AuditLog -Message "There are $($Export.Count) objects listed with the following properties: " $Script:LogString += Write-AuditLog -Message "$(($Export | Get-Member -MemberType property ).Name -join " | ")" if ($Report) { # Add Datetime to filename $ExportFileName = "$AttachmentFolderPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($ScriptFunctionName)_$($env:USERDNSDOMAIN)" # Create FileNames $csv = "$ExportFileName.csv" $zip = "$ExportFileName.zip" $log = "$ExportFileName.AuditLog.csv" Build-ReportArchive -Export $Export -csv $csv -zip $zip -log $log -ErrorVariable BuildErr } else { $Script:LogString += Write-AuditLog -Message "Returning output object." Start-Sleep 2 return $Export } } } #EndRegion '.\Public\Get-ADUserWildCardAudit.ps1' 125 #Region '.\Public\Get-HostTag.ps1' 0 function Get-HostTag { <# .SYNOPSIS Creates a host name or tag based on predetermined criteria for as many as 999 hosts at a time. .DESCRIPTION A longer description of the function, its purpose, common use cases, etc. .EXAMPLE Get-HostTag -PhysicalOrVirtual Physical -Prefix "CSN" -SystemOS 'Windows Server' -DeviceFunction 'Application Server' -HostCount 5 CSN-PWSVAPP001 CSN-PWSVAPP002 CSN-PWSVAPP003 CSN-PWSVAPP004 CSN-PWSVAPP005 This creates the name of the host under 15 characters and numbers them. Prefix can be 2-3 characters. .PARAMETER PhysicalOrVirtual Tab through selections to add 'P' or 'V' for physical or virtual to host tag. .PARAMETER Prefix Enter the 2-3 letter prefix. Good for prefixing company initials, locations, or other. .PARAMETER SystemOS Use tab to cycle through the following options: "Cisco ASA", "Android", "Apple IOS", "Dell Storage Center", "MACOSX", "Dell Power Edge", "Embedded", "Embedded Firmware", "Cisco IOS", "Linux", "Qualys", "Citrix ADC (Netscaler)", "Windows Thin Client", "VMWare", "Nutanix", "TrueNas", "FreeNas", "ProxMox", "Windows Workstation", "Windows Server", "Windows Server Core", "Generic OS", "Generic HyperVisor" .PARAMETER DeviceFunction Use tab to cycle through the following options: "Application Server", "Backup Server", "Directory Server", "Email Server", "Firewall", "FTP Server", "Hypervisor", "File Server", "NAS File Server", "Power Distribution Unit", "Redundant Power Supply", "SAN Appliance", "SQL Server", "Uninteruptable Power Supply", "Web Server", "Management", "Blade Enclosure", "Blade Enclosure Switch", "SAN specific switch", "General server/Network switch", "Generic Function Device" .PARAMETER HostCount Enter a number from 1 to 999 for how many hostnames you'd like to create. #> # Define the output type of the function [OutputType([string[]])] # Define the binding for the cmdlet [CmdletBinding()] # Define the parameters for the function param ( # Define the first parameter, which is mandatory [Parameter( MandaTory = $true, # This parameter is mandatory Position = 0, # This parameter should be the first one in the list HelpMessage = 'Enter 2 character site code or prefix for your devices', # Help message for the parameter ValueFromPipelineByPropertyName = $true # This parameter can be piped to )] [ValidateSet("Physical", "Virtual")] # This parameter can only have these values [string]$PhysicalOrVirtual, # The variable that will hold the value of this parameter # Define the second parameter, which is mandatory [Parameter( MandaTory = $true, # This parameter is mandatory Position = 1, # This parameter should be the second one in the list HelpMessage = 'Enter 2 to 3 character site code or prefix for your devices', # Help message for the parameter ValueFromPipelineByPropertyName = $true # This parameter can be piped to )] [ValidateLength(2, 3)] # This parameter can only have a value of length 2 or 3 [string]$Prefix, # The variable that will hold the value of this parameter # Define the third parameter, which is mandatory [Parameter( MandaTory = $true, # This parameter is mandatory Position = 2, # This parameter should be the third one in the list HelpMessage = 'Tab complete to pick from a list of System OSs', # Help message for the parameter ValueFromPipelineByPropertyName = $true # This parameter can be piped to )] [ValidateSet( "Cisco ASA", "Android", "Apple IOS", "Dell Storage Center", "MACOSX", "Dell Power Edge", "Embedded", "Embedded Firmware", "Cisco IOS", "Linux", "Qualys", "Citrix ADC (Netscaler)", "Windows Thin Client", "VMWare", "Nutanix", "TrueNas", "FreeNas", "ProxMox", "Windows Workstation", "Windows Server", "Windows Server Core", "Generic OS", "Generic HyperVisor" )] # This parameter can only have values from this list [string]$SystemOS, # The variable that will hold the value of this parameter [Parameter( MandaTory = $true, Position = 3, HelpMessage = 'Tab complete to pick from a list of Device Functions', ValueFromPipelineByPropertyName = $true )] [ValidateSet( "Application Server", "Backup Server", "Directory Server", "Email Server", "Firewall", "FTP Server", "Hypervisor", "File Server", "NAS File Server", "Power Distribution Unit", "Redundant Power Supply", "SAN Appliance", "SQL Server", "Uninteruptable Power Supply", "Web Server", "Management", "Blade Enclosure", "Blade Enclosure Switch", "SAN specific switch", "General server/Network switch", "Generic Function Device", "Cache Server","Load Balancer" )] [string]$DeviceFunction, [Parameter( Position = 4, HelpMessage = 'Enter the number of host names you want to create between 1 and 254', ValueFromPipelineByPropertyName = $true )] [ValidateRange(1, 999)] [int]$HostCount = 1 ) begin { switch ($DeviceFunction) { "Application Server" { $DFunction = "APP" } "Backup Server" { $DFunction = "BAK" } "Directory Server" { $DFunction = "DIR" } "Email Server" { $DFunction = "EML" } "Firewall" { $DFunction = "FRW" } "FTP Server" { $DFunction = "FTP" } "Hypervisor" { $DFunction = "HYP" } "File Server" { $DFunction = "FIL" } "NAS File Server" { $DFunction = "NAS" } "Power Distribution Unit" { $DFunction = "PDU" } "Redundant Power Supply" { $DFunction = "RPS" } "SAN Appliance" { $DFunction = "SAN" } "SQL Server" { $DFunction = "SQL" } "Uninteruptable Power Supply" { $DFunction = "UPS" } "Web Server" { $DFunction = "WEB" } "Management" { $DFunction = "MGT" } "Cache Server" { $DFunction = "CSH" } "Load Balancer" { $DFunction = "BAL" } "Blade Enclosure" { $DFunction = "BLDENC" } "Blade Enclosure Switch" { $DFunction = "SW-BLD" } "SAN specific Switch" { $DFunction = "SW-SAN" } "General Server/Network Switch" { $DFunction = "SW-SVR" } Default { $DFunction = "XDV" } } switch ($SystemOS) { "Cisco ASA" { $OSTxt = "ASA" } "Android" { $OSTxt = "DRD" } "Apple IOS" { $OSTxt = "IOS" } "Dell Storage Center" { $OSTxt = "DLS" } "MACOSX" { $OSTxt = "MAC" } "Dell Power Edge" { $OSTxt = "DPE" } "Embedded" { $OSTxt = "EMD" } "Embedded Firmware" { $OSTxt = "EFW" } "Cisco IOS" { $OSTxt = "COS" } "Linux" { $OSTxt = "NIX" } "Qualys" { $OSTxt = "QLS" } "Citrix ADC (Netscaler)" { $OSTxt = "ADC" } "Windows Thin Client" { $OSTxt = "WTC" } "VMWare" { $OSTxt = "VMW" } "Nutanix" { $OSTxt = "NTX" } "TrueNas" { $OSTxt = "FNS" } "FreeNas" { $OSTxt = "XDV" } "ProxMox" { $OSTxt = "PMX" } "Windows Workstation" { $OSTxt = "WWS" } "Windows Server" { $OSTxt = "WSV" } "Windows Server Core" { $OSTxt = "WSC" } "Generic OS" { $OSTxt = "GOS" } Default { $DFunction = "GHV" } } switch ($PhysicalOrVirtual) { "Physical" { $DevType = "P" } Default { $DevType = "V" } } } process { $OutPut = @() 1..$HostCount | ForEach-Object { $CustomName = $Prefix + "-" + $DevType + $OSTxt + $DFunction + $('{0:d3}' -f [int]$_) $Output += $CustomName } # Create Device Name } end { return $Output } } #EndRegion '.\Public\Get-HostTag.ps1' 176 #Region '.\Public\Get-NetworkAudit.ps1' 0 function Get-NetworkAudit { <# .SYNOPSIS Discovers local network and runs port scans on all hosts found for specific or default sets of ports and displays MAC ID vendor info. .DESCRIPTION Scans the network for open ports specified by the user or default ports if no ports are specified. Creates reports if report switch is active. Adds MACID vendor info if found. .NOTES Installs PSnmap if not found and can output a report, or just the results. .LINK Specify a URI to a help page, this will show when Get-Help -Online is used. .EXAMPLE Get-NetworkAudit -report .PARAMETER Ports Default ports are: "21", "22", "23", "25", "53", "67", "68", "80", "443", "88", "464", "123", "135", "137", "138", "139", "445", "389", "636", "514", "587", "1701", "3268", "3269", "3389", "5985", "5986" If you want to supply a port, do so as an integer or an array of integers. "22","80","443", etc. .PARAMETER Report Specify this switch if you would like a report generated in C:\temp. .PARAMETER LocalSubnets Specify this switch to automatically scan subnets on the local network of the scanning device. Will not scan outside of the hosting device's subnet. .PARAMETER NoHops Don't allow scans across a gateway. .PARAMETER AddService Add the service typically associated with the port to the output. .PARAMETER Computers Scan single host or array of hosts using Subet ID in CIDR Notation, IP, NETBIOS, or FQDN in "quotes"' For Example: "10.11.1.0/24","10.11.2.0/24" #> [OutputType([pscustomobject])] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter( ValueFromPipelineByPropertyName = $true, Position = 0 )] [ValidateRange(1, 65535)] [Int32[]]$Ports, [Parameter( Mandatory = $true, ParameterSetName = 'Default', HelpMessage = 'Automatically find and scan local attached subnets', ValueFromPipelineByPropertyName = $true, Position = 1 )] [switch]$LocalSubnets, [Parameter( Mandatory = $true, ParameterSetName = 'Computers', HelpMessage = 'Scan host or array of hosts using Subet ID in CIDR Notation, IP, NETBIOS, or FQDN in "quotes"', ValueFromPipelineByPropertyName = $true, Position = 1 )] [string[]]$Computers, [switch]$NoHops, [switch]$AddService, [switch]$Report ) begin { # Create logging object $Script:LogString = @() # Begin Logging $Script:LogString += Write-AuditLog -Message "Begin Log" # Check if PSnmap module is installed, if not install it. # Tested Version: # https://www.powershellgallery.com/packages/PSnmap/1.3.1 Updated: 7/18/2018 $params = @{ PublicModuleNames = "PSnmap" PublicRequiredVersions = "1.3.1" Scope = "CurrentUser" } Initialize-ModuleEnv @params # Set default ports to scan if (!($ports)) { [Int32[]]$ports = "21", "22", "23", "25", "53", "67", "68", "80", "443", ` "88", "464", "123", "135", "137", "138", "139", ` "445", "389", "636", "514", "587", "1701", ` "3268", "3269", "3389", "5985", "5986" } } # End of begin block process { if ($LocalSubnets) { # Get connected networks on the local device. $internetadapter = Get-NetIPConfiguration -Detailed | Where-Object { $_.NetProfile.IPv4Connectivity -eq "Internet" } $subnetcidr = "$($internetadapter.IPv4Address.IPAddress)/$($internetadapter.IPv4Address.PrefixLength)" $CalcSub = Invoke-PSipcalc -NetworkAddress $subnetcidr -Enumerate # Get subnet in CIDR format $subnet = "$($CalcSub.NetworkAddress)/$($CalcSub.NetworkLength)" # Get DHCP server for the network $DHCPServer = (Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.IPAddress -eq $($internetadapter.IPv4Address.IPAddress) }).DHCPServer # Create Network Scan Object $Script:LogString += Write-AuditLog -Message "Beggining scan of subnet $($subnet) for the following ports:" $Script:LogString += Write-AuditLog -Message "$(($ports | Out-String -Stream) -join ",")" # Begin Reigion Build NetworkAudit Object if ($NoHops) { $IPRange = $CalcSub.IPEnumerated # Use a foreach loop to test each IP address $NonRoutedIPs,$FailedIps = Get-QuickPing -IPRange $IPRange -TTL 1 if ($null -ne $NonRoutedIPs) { $Script:LogString += Write-AuditLog -Message "Local IPs object is populated." $Script:LogString += Write-AuditLog -Message "Scan found $($NonRoutedIPs.count) IPs to scan." $Script:LogString += Write-AuditLog -Message "There were $($FailedIps.count) IPs that failed." $Script:LogString += Write-AuditLog -Message "Begin Invoke-PSnmap" $NetworkAudit = Invoke-PSnmap -ComputerName $NonRoutedIPs -Port $ports -Dns -NoSummary -AddService:$AddService } else { throw "No Hosts found to scan!" } } else { $NetworkAudit = Invoke-PSnmap -ComputerName $subnet -Port $ports -Dns -NoSummary -AddService:$AddService } # End Reigion Build Network Audit Object # Write out information about the network scan. $Script:LogString += Write-AuditLog -Message "##########################################" $Script:LogString += Write-AuditLog -Message "Network scan for Subnet $($Subnet) completed." $Script:LogString += Write-AuditLog -Message "DHCP Server: $($DHCPServer)" $Script:LogString += Write-AuditLog -Message "Gateway: $($internetadapter.IPv4DefaultGateway.nexthop)" $Script:LogString += Write-AuditLog -Message "##########################################" $Script:LogString += Write-AuditLog -Message "Starting with $(($NetworkAudit).count) output objects." # Filter devices that don't ping as no results will be found. $scan = Build-NetScanObject -NetScanObject $NetworkAudit #-IncludeNoPing $Script:LogString += Write-AuditLog -Message "Created $(($scan).count) output objects for the following hosts:" $Script:LogString += Write-AuditLog -Message "$(($scan | Select-Object "IP/DNS")."IP/DNS" -join ", ")" # Normalize Subnet text for filename. $subnetText = $(($subnet.Replace("/", "_"))) # If report switch is true, export the scan to a CSV file with a timestamped filename. <# if ($report) { $csv = "C:\temp\$((Get-Date).ToString('yyyy.MM.dd_hhmm.ss')).$($env:USERDOMAIN)_Subnet.$($subnetText).NetScan.csv" $zip = $csv -replace ".csv", ".zip" $log = $csv -replace ".csv", ".AuditLog.csv" Build-ReportArchive -Export $scan -csv $csv -zip $zip -log $log -ErrorVariable BuildErr } #> # Add the scan to the function output. $results = $scan } # End If $LocalSubnets elseif ($Computers) { $Subnet = $Computers if ($NoHops) { $IPRange = $Subnet $NonRoutedIPs,$FailedIps = Get-QuickPing -IPRange $IPRange -TTL 1 if ($null -ne $NonRoutedIPs ) { $Script:LogString += Write-AuditLog -Message "Local IPs object is populated." $Script:LogString += Write-AuditLog -Message "Scan found $($NonRoutedIPs.count) IPs to scan." if ($FailedIps -eq "NoIPs") { $FailedIpsCount = 0 } else { $FailedIpsCount = $FailedIps.count } $Script:LogString += Write-AuditLog -Message "There were $FailedIpsCount IPs that failed." $Script:LogString += Write-AuditLog -Message "Begin Invoke-PSnmap" $scan = Invoke-PSnmap -ComputerName $NonRoutedIPs -Port $ports -Dns -NoSummary -AddService:$AddService $results = Build-NetScanObject -NetScanObject $scan } else { throw "No Hosts found to scan!" } } else { $Script:LogString += Write-AuditLog -Message "Begin Invoke-PSnmap" $scan = Invoke-PSnmap -ComputerName $Subnet -Port $ports -Dns -NoSummary -AddService:$AddService $results = Build-NetScanObject -NetScanObject $scan } } } # Process Close end { if ($Report) { $csv = "C:\temp\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss')).$($env:USERDOMAIN)_HostScan_$subnetText.csv" $zip = $csv -replace ".csv", ".zip" $log = $csv -replace ".csv", ".AuditLog.csv" Build-ReportArchive -Export $results -csv $csv -zip $zip -log $log -ErrorVariable BuildErr } else { return $results } }# End Close } #EndRegion '.\Public\Get-NetworkAudit.ps1' 190 #Region '.\Public\Get-QuickPing.ps1' 0 function Get-QuickPing { <# .SYNOPSIS Performs a quick ping on a range of IP addresses and returns an array of IP addresses that responded to the ping and an array of IP addresses that failed to respond. .DESCRIPTION This function performs a quick ping on a range of IP addresses specified by the IPRange parameter. The ping is done with a Time-to-Live (TTL) value of 128 (by default), meaning only the local network will be pinged. The function returns an array of IP addresses that responded to the ping and an array of IP addresses that failed to respond. .PARAMETER IPRange Specifies a range of IP addresses to ping. Can be a string with a single IP address, a range of IP addresses in CIDR notation, or an array of IP addresses. .PARAMETER TTL Specifies the Time-to-Live (TTL) value to use for the ping. The default value is 128. .EXAMPLE Get-QuickPing -IPRange 192.168.1.1 Performs a quick ping on the IP address 192.168.1.1 with a TTL of 128 and returns an array of IP addresses that responded to the ping and an array of IP addresses that failed to respond. .EXAMPLE Get-QuickPing -IPRange 192.168.1.0/24 Performs a quick ping on all IP addresses in the 192.168.1.0/24 network with a TTL of 128 and returns an array of IP addresses that responded to the ping and an array of IP addresses that failed to respond. .EXAMPLE Get-QuickPing -IPRange @(192.168.1.1, 192.168.1.2, 192.168.1.3) Performs a quick ping on the IP addresses 192.168.1.1, 192.168.1.2, and 192.168.1.3 with a TTL of 128 and returns an array of IP addresses that responded to the ping and an array of IP addresses that failed to respond. .NOTES Author: DrIOSx #> param ( $IPRange, [int]$TTL = 128, [int32]$BufferSize = 16, [int32]$Count = 1 ) $FailedToPing = @() $Success = @() foreach ($IP in $IPRange) { try { if ($PSVersionTable.PSVersion.Major -ge 7) { [void](Test-Connection $IP -BufferSize $BufferSize -TimeToLive $TTL -Count $Count -ErrorAction Stop -OutVariable test) if ($test.Status -eq 'Success') { $Script:LogString += Write-AuditLog -Message "$IP Found!" $Success += $IP } else{ $FailedToPing += $IP } } else { try { [void](Test-Connection $IP -BufferSize $BufferSize -TimeToLive $TTL -Count $Count -ErrorAction Stop) $Script:LogString += Write-AuditLog -Message "$IP Found!" $Success += $IP } catch { $FailedToPing += $IP } } } catch {} } if ($null -eq $FailedToPing) { $FailedtoPing = "NoIPs" } if ($null -eq $Success) { $FailedtoPing = "NoIPs" } return $Success,$FailedToPing } #EndRegion '.\Public\Get-QuickPing.ps1' 77 #Region '.\Public\Get-WebCertAudit.ps1' 0 function Get-WebCertAudit { <# .SYNOPSIS Retrieves the certificate information for a web server. .DESCRIPTION The Get-WebCert function retrieves the certificate information for a web server by creating a TCP connection and using SSL to retrieve the certificate information. .PARAMETER Url The URL of the web server. .EXAMPLE Get-WebCert -Url "https://www.example.com" This example retrieves the certificate information for the web server at https://www.example.com. .OUTPUTS PSCustomObject Returns a PowerShell custom object with the following properties: Subject: The subject of the certificate. Thumbprint: The thumbprint of the certificate. Expires: The expiration date of the certificate. #> param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [string[]]$Url ) $Export = @() foreach ($link in $Url) { $Req = [System.Net.Sockets.TcpClient]::new($link, '443') $Stream = [System.Net.Security.SslStream]::new($Req.GetStream()) $Stream.AuthenticateAsClient($link) $hash = [ordered]@{ URL = $link Subject = $Stream.RemoteCertificate.Subject Thumbprint = $Stream.RemoteCertificate.GetCertHashString() Expires = $Stream.RemoteCertificate.GetExpirationDateString() } New-Object -TypeName PSCustomObject -Property $hash -OutVariable PSObject | Out-Null $Export += $PSObject } return $Export } #EndRegion '.\Public\Get-WebCertAudit.ps1' 41 #Region '.\Public\Merge-ADAuditZip.ps1' 0 function Merge-ADAuditZip { <# .SYNOPSIS Combines multiple audit report files into a single compressed ZIP file. .DESCRIPTION The Merge-ADAuditZip function combines multiple audit report files into a single compressed ZIP file. The function takes an array of file paths, a maximum file size for the output ZIP file, an output folder for the merged file, and an optional switch to open the directory of the merged file after creation. .PARAMETER FilePaths Specifies an array of file paths to be merged into a single compressed ZIP file. .PARAMETER MaxFileSize Specifies the maximum file size (in bytes) for the output ZIP file. The default value is 24 MB. .PARAMETER OutputFolder Specifies the output folder for the merged compressed ZIP file. The default folder is C:\temp. .PARAMETER OpenDirectory Specifies an optional switch to open the directory of the merged compressed ZIP file after creation. .EXAMPLE $workstations = Get-ADHostAudit -HostType WindowsWorkstations -Report $servers = Get-ADHostAudit -HostType WindowsServers -Report $nonWindows = Get-ADHostAudit -HostType "Non-Windows" -Report Merge-ADAuditZip -FilePaths $workstations, $servers, $nonWindows This example combines three audit reports for Windows workstations, Windows servers, and non-Windows hosts into a single compressed ZIP file. .EXAMPLE Merge-ADAuditZip -FilePaths C:\AuditReports\Report1.csv,C:\AuditReports\Report2.csv -MaxFileSize 50MB -OutputFolder C:\MergedReports -OpenDirectory This example merges two audit reports into a single compressed ZIP file with a maximum file size of 50 MB, an output folder of C:\MergedReports, and opens the directory of the merged compressed ZIP file after creation. .NOTES This function will split the output file into multiple parts if the maximum file size is exceeded. If the size exceeds the limit, a new ZIP file will be created with an incremental number added to the file name. This function may or may not work with various types of input. #> param( [string[]]$FilePaths, [int]$MaxFileSize = 24MB, [string]$OutputFolder = "C:\temp", [switch]$OpenDirectory ) # Remove any blank file paths from the array $FilePaths = $FilePaths | Where-Object { $_ } # Create the output directory if it doesn't exist Build-DirectoryPath -DirectoryPath $OutputFolder # Create a hashtable to store the file sizes $fileSizes = @{} foreach ($filePath in $FilePaths) { $fileSizes[$filePath] = (Get-Item $filePath).Length } # Sort the files by size in descending order $sortedFiles = $fileSizes.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -ExpandProperty Name # Build the output path $dateTimeString = (Get-Date).ToString('yyyy-MM-dd_hh.mm.ss') $domainName = $env:USERDNSDOMAIN $partCounter = 0 $outputFileName = "$($dateTimeString)_$($domainName)_CombinedAudit.zip" $outputPath = Join-Path $OutputFolder $outputFileName # Add files to the zip until the maximum size is reached $currentSize = 0 $filesToAdd = @() foreach ($filePath in $sortedFiles) { if (($currentSize + $fileSizes[$filePath]) -gt $MaxFileSize) { if ($partCounter -eq 0) { # If adding the next file would exceed the maximum size # Create a zip file with the current batch of files $partCounter++ $outputFileName = "$($dateTimeString)_$($domainName)_CombinedAudit-part{0}.zip" -f $partCounter $outputPath = Join-Path $OutputFolder $outputFileName } Compress-Archive -Path $filesToAdd -DestinationPath $outputPath $filesToAdd = @() # Clear the list of files to add $currentSize = 0 # Reset current size counter $partCounter++ $outputFileName = "$($dateTimeString)_$($domainName)_CombinedAudit-part{0}.zip" -f $partCounter $outputPath = Join-Path $OutputFolder $outputFileName } $filesToAdd += $filePath # Add the current file to the list of files to add $currentSize += $fileSizes[$filePath] # Add the size of the current file to the current size counter } # Create a zip file with the remaining files if ($filesToAdd) { $Script:LogString += Write-AuditLog -Message "Compressing Archive with files $filesToAdd." Compress-Archive -Path $filesToAdd -DestinationPath $outputPath } foreach ($filePath in $FilePaths) { if ($filePath) { Remove-Item -Path $filePath -Force } } # Remove the original files if ($OpenDirectory) { # If the OpenDirectory switch is used $Script:LogString += Write-AuditLog -Message "Build Complete. Opening output directory." Invoke-Item (Split-Path $outputPath) # Open the directory of the merged zip file return $outputPath } else { $Script:LogString += Write-AuditLog -Message "Build Complete. Returning output file path." return $outputPath # Otherwise, only return the path of the merged zip file } } #EndRegion '.\Public\Merge-ADAuditZip.ps1' 110 #Region '.\Public\New-GraphEmailApp.ps1' 0 function New-GraphEmailApp { <# .SYNOPSIS Creates a new Microsoft Graph Email app and associated certificate for app-only authentication. .DESCRIPTION This cmdlet creates a new Microsoft Graph Email app and associated certificate for app-only authentication. It requires a 2 to 4 character long prefix ID for the app, files and certs that are created, as well as the email address of the sender and the email of the Group the sender is a part of to assign app policy restrictions. .PARAMETER Prefix The 2 to 4 character long prefix ID of the app, files and certs that are created. Meant to group multiple runs so that if run in different environments, they will stack naturally in Azure. Ensure you use the same prefix each time if you'd like this behavior. .PARAMETER UserId The email address of the sender. .PARAMETER MailEnabledSendingGroup The email of the Group the sender is a member of to assign app policy restrictions. For Example: IT-AuditEmailGroup@contoso.com You can create the group using the admin center at https://admin.microsoft.com or you can create it using the following commands as an example. # Import the ExchangeOnlineManagement module Import-Module ExchangeOnlineManagement # Create a new mail-enabled security group New-DistributionGroup -Name "My Group" -Members "user1@contoso.com", "user2@contoso.com" -MemberDepartRestriction Closed .PARAMETER CertThumbprint The thumbprint of the certificate to use. If not specified, a self-signed certificate will be generated. .EXAMPLE PS C:\> New-GraphEmailApp -Prefix ABC -UserId jdoe@example.com -MailEnabledSendingGroup "Example Mail Group" -CertThumbprint "9B8B40C5F148B710AD5C0E5CC8D0B71B5A30DB0C" .INPUTS None .OUTPUTS Returns a pscustomobject containing the AppId, CertThumbprint, TenantID, and CertExpires. .NOTES This cmdlet requires that the user running the cmdlet have the necessary permissions to create the app and connect to Exchange Online. In addition, a mail-enabled security group must already exist in Exchange Online for the MailEnabledSendingGroup parameter. #> [OutputType([pscustomobject])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The 2 to 4 character long prefix ID of the app, files and certs that are created.")] [ValidatePattern('^[A-Z]{2,4}$')] [string]$Prefix, [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] [String] $UserId, [Parameter(Mandatory = $true, HelpMessage = "The Group the sender is a part of to assign app policy restrictions.")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] [String] $MailEnabledSendingGroup, [Parameter(Mandatory = $false, HelpMessage = "The thumbprint of the certificate to use")] [String] $CertThumbprint = $null ) begin { # Begin Logging $Script:LogString = @() $Script:LogString += Write-AuditLog -Message "Begin Log" $Script:LogString += Write-AuditLog -Message "###############################################" # Install and import the Microsoft.Graph module. Tested: 1.22.0 $PublicMods = ` "Microsoft.Graph", "ExchangeOnlineManagement", ` "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan" $PublicVers = ` "1.22.0", "3.1.0", ` "1.1.2", "1.0.0" $ImportMods = ` "Microsoft.Graph.Authentication", ` "Microsoft.Graph.Applications", ` "Microsoft.Graph.Identity.SignIns", ` "Microsoft.Graph.Users" $params1 = @{ PublicModuleNames = $PublicMods PublicRequiredVersions = $PublicVers ImportModuleNames = $ImportMods Scope = "CurrentUser" } Initialize-ModuleEnv @params1 # Step 4: # Connect to MSGraph with the appropriate permission scopes and then Exchange. $Script:LogString = Write-AuditLog "Connecting to MgGraph and ExchangeOnline using modern authentication pop-up." try { $Script:LogString = Write-AuditLog "Connecting to MgGraph..." Connect-MgGraph -Scopes "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All", "Directory.ReadWrite.All" $Script:LogString = Write-AuditLog "Connected to MgGraph" Read-Host "Press Enter to connect to ExchangeOnline." -ErrorAction Stop Connect-ExchangeOnline -ErrorAction Stop $Script:LogString = Write-AuditLog "Connected to ExchangeOnline." Read-Host "Press Enter to continue." -ErrorAction Stop } catch { throw $_.Exception } # Step 5: # Get the MGContext $context = Get-MgContext # Step 6: # Instantiate the user variable. $user = Get-MgUser -Filter "Mail eq '$UserId'" # Step 7: # Define the application Name and Encrypted File Paths. $AppName = "$($Prefix)-AuditGraphEmail-$($env:USERDNSDOMAIN)-As-$(($user.UserPrincipalName).Split("@")[0])" $graphServicePrincipal = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" $graphResourceId = $graphServicePrincipal.AppId $Script:LogString = Write-AuditLog "Microsoft Graph Service Principal AppId is $graphResourceId" # Step 9: # Build resource requirements variable using Find-MgGraphCommand -Command New-MgApplication | Select -First 1 -ExpandProperty Permissions # Find-MgGraphPermission -PermissionType Application -All | ? {$_.name -eq "Mail.Send"} $resId = (Find-MgGraphPermission -PermissionType Application -All | Where-Object { $_.name -eq "Mail.Send" }).Id } process { # Step 10: # Create or retrieve certificate from the store. try { if (!$CertThumbprint) { # Create a self-signed certificate for the app. $Cert = New-SelfSignedCertificate -Subject "CN=$AppName" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 $CertThumbprint = $Cert.Thumbprint $CertExpirationDate = $Cert.NotAfter } else { # Retrieve the certificate from the CurrentUser's certificate store. $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } if (!($Cert)) { throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store." } $CertThumbprint = $Cert.Thumbprint $CertExpirationDate = $Cert.NotAfter } } catch { # If there is an error, throw an exception with the error message. throw $_.Exception } # Step 11: # Create the app registration with the required permissions and the self-signed certificate. try { $Script:LogString = Write-AuditLog "Creating app registration..." # Build required resource access list. $RequiredResourceAccess = New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess $RequiredResourceAccess.ResourceAppId = $GraphResourceId $RequiredResourceAccess.ResourceAccess += @{ Id = $ResID; Type = "Role" } $AppPermissions = New-Object -TypeName System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $AppPermissions.Add($RequiredResourceAccess) $Script:LogString = Write-AuditLog "App permissions are: $AppPermissions" # Create app registration. $AppRegistration = New-MgApplication -DisplayName $AppName -SignInAudience "AzureADMyOrg" ` -Web @{ RedirectUris = "http://localhost"; } ` -RequiredResourceAccess $RequiredResourceAccess ` -AdditionalProperties @{} ` -KeyCredentials @(@{ Type = "AsymmetricX509Cert"; Usage = "Verify"; Key = $Cert.RawData }) if (!($AppRegistration)) { throw "The app creation failed for $($AppName)." } $Script:LogString = Write-AuditLog "App registration created with app ID $($AppRegistration.AppId)" Start-Sleep 15 } catch { # If there is an error, throw an exception with the error message. throw $_.Exception } # End Region catch # Step 12: # Create a Service Principal for the app. $Script:LogString = Write-AuditLog "Creating service principal for app with AppId $($AppRegistration.AppId)" New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{} # Step 13: # Get the client Service Principal for the created app. $ClientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'" if (!($ClientSp)) { Write-AuditLog "Client service Principal not found for $($AppRegistration.AppId)" -Error throw "Unable to find Client Service Principal." } # Step 14: # Build the parameters for the New-MgOauth2PermissionGrant and create the grant. $Params = @{ "ClientId" = $ClientSp.Id "ConsentType" = "AllPrincipals" "ResourceId" = $GraphServicePrincipal.Id "Scope" = "Mail.Send" } New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false # Step 15: # Create the admin consent url: $adminConsentUrl = ` "https://login.microsoftonline.com/" + $context.TenantId + "/adminconsent?client_id=" + $appRegistration.AppId Write-Host -ForegroundColor Yellow "Please go to the following URL in your browser to provide admin consent" Write-Host $adminConsentUrl Write-Host "Graph Command you can save:" # Step 16: # Generate graph command that can be used to connect later that can be copied and saved. $connectGraph = "Connect-MgGraph -ClientId """ + $appRegistration.AppId + """ -TenantId """` + $context.TenantId + """ -CertificateName """ + $cert.SubjectName.Name + """" Write-Host -ForegroundColor Cyan "After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:" Write-Host $connectGraph } # End Region Process end { # Step 17: # Set app access policy in Exchange Online and constrain to Mail Enabled Sending Group. try { $Script:LogString = Write-AuditLog -Message "Creating Exchange Application policy for $($MailEnabledSendingGroup) for AppId $($AppRegistration.AppId)." New-ApplicationAccessPolicy -AppId $appRegistration.AppId ` -PolicyScopeGroupId $MailEnabledSendingGroup -AccessRight RestrictAccess ` -Description "Limit MSG application to only send emails as a group of users" -ErrorAction Stop $Script:LogString = Write-AuditLog -Message "Created Exchange Application policy for $($MailEnabledSendingGroup)." } catch { throw $_.Exception } # Step 18: # Create the output object with the certificate thumbprint and expiration, the tenantid and appid. if (!(Get-SecretVault -Name GraphEmailAppLocalStore)) { try { $Script:LogString = Write-AuditLog -Message "Registering CredMan Secret Vault" Register-SecretVault -Name GraphEmailAppLocalStore -ModuleName "SecretManagement.JustinGrote.CredMan" -ErrorAction Stop $Script:LogString = Write-AuditLog -Message "Secret Vault: GraphEmailAppLocalStore registered." } catch { throw $_.Exception } } elseif ((Get-SecretInfo -Name "CN=$AppName" -Vault GraphEmailAppLocalStore) ) { $Script:LogString = Write-AuditLog -Message "Secret found! Would you like to delete the previous configuration for `"CN=$AppName.`"?" -Severity Warning try { Remove-Secret -Name "CN=$AppName" -Vault GraphEmailAppLocalStore -Confirm:$false -ErrorAction Stop $Script:LogString = Write-AuditLog -Message "Previous secret CN=$AppName removed." } catch { throw $_.Exception } } $output = [PSCustomObject] @{ AppId = $appRegistration.AppId CertThumbprint = $CertThumbprint TenantID = $context.TenantId CertExpires = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss") SendAsUser = $($user.UserPrincipalName.Split("@")[0]) AppRestrictedSendGroup = $MailEnabledSendingGroup } $delimiter = '|' $joinedString = ($output.PSObject.Properties.Value) -join $delimiter try { Set-Secret -Name "CN=$AppName" -Secret $joinedString -Vault GraphEmailAppLocalStore -ErrorAction Stop } catch { throw $_.Exception } $Script:LogString = Write-AuditLog -Message "Returning output. Save the AppName $("CN=$AppName"). The AppName will be needed to retreive the secret containing authentication info." return $output } # End Region End } #EndRegion '.\Public\New-GraphEmailApp.ps1' 267 #Region '.\Public\New-PatchTuesdayReport.ps1' 0 function New-PatchTuesdayReport { <# .SYNOPSIS Generates a Patch Tuesday report HTML file based on a CSV input file. .DESCRIPTION The function generates an HTML report file with the latest Microsoft updates released on Patch Tuesday. The report file includes separate sections for client and server operating systems. To use this function, follow these steps: Go to the Microsoft Security Response Center website at https://msrc.microsoft.com/update-guide. Select the appropriate filters to display the updates you want to include in the report. For example, select the following options: Product: Windows Server 2022, Windows Server 2019, Windows Server 2016, Windows Server 2012 R2, Windows 11 Version 22H2 for x64-based Systems, Windows 10 Version 22H2 for x64-based Systems. Severity: Critical Release Date: Last 30 days Click on "Download all as CSV" to download the updates as a CSV file. The function will import the CSV file with the appropriate headers: Import-Csv -Path $Path -Header 'Release Date','Product','Platform','Impact','Max Severity','Article','ArticleUrl','Download','Download Url','Build Number','Details','Details Url','Base Score' Use the New-PatchTuesdayReport function to generate the HTML report file. Future updates will include options to specify the parameters. .PARAMETER CsvPath The path to the CSV input file containing the Microsoft update information. .PARAMETER DateId A string value used to identify the date of the Patch Tuesday report. .PARAMETER LogoUrl A string value representing the URL of the logo to be displayed in the report. .PARAMETER ImportHeaderAs An array of strings representing the header row of the CSV input file. .PARAMETER OSList An array of strings representing the list of operating systems to include in the report. .EXAMPLE PS C:> New-PatchTuesdayReport -CsvPath "C:\updates.csv" -DateId "2022-Oct" -LogoUrl "https://example.com/logo.png" -OSList @("Windows Server 2012 R2", "Windows Server 2016", "Windows Server 2019", "Windows Server 2022", "Windows 11", "Windows 10") This example generates a Patch Tuesday report for October 2022 with updates for Windows Server 2012 R2, Windows Server 2016, Windows Server 2019, Windows Server 2022, Windows 11, and Windows 10 operating systems. The report includes a logo displayed at the top of the report. .INPUTS None. .OUTPUTS A string value containing the HTML code for the Patch Tuesday report. .NOTES None. #> [CmdletBinding()] param( [ValidateNotNull()] [string]$CsvPath, [ValidateNotNull()] [string]$DateId, [string]$LogoUrl, [string[]]$ImportHeaderAs = @("Release Date", "Product", "Platform", "Impact", "Max Severity", "Article", "ArticleUrl", "Download", "Download Url", "Build Number", "Details", "Details Url", "Base Score"), [string[]]$OSList = @('Windows Server 2012 R2', 'Windows Server 2016', 'Windows Server 2019', 'Windows Server 2022', 'Windows 11', 'Windows 10') ) begin { $AllUpdates = Import-Csv -Path $CsvPath -Header $ImportHeaderAs $Updates = Group-UpdateByProduct -AllUpdates $AllUpdates -OSList $OSList # Read CSS, JavaScript, and HTML template $moduleBase = (Get-Module ADAuditTasks).ModuleBase $assetsPath = Join-Path $moduleBase "assets" $cssContent = Read-FileContent -FilePath (Join-Path $assetsPath "styles.css") $jsContent = Read-FileContent -FilePath (Join-Path $assetsPath "scripts.js") $htmlTemplate = Read-FileContent -FilePath (Join-Path $assetsPath "template.html") # Replace placeholders in the HTML template with the CSS and JavaScript content $htmlTemplate = $htmlTemplate -replace '/\* CSS-PLACEHOLDER \*/', $cssContent $htmlTemplate = $htmlTemplate -replace '/\* JS-PLACEHOLDER \*/', $jsContent } Process { # Generate the report content using the HTML template $html = $htmlTemplate -replace "<!--LOGO-URL-PLACEHOLDER-->", $LogoUrl -replace "<!--DATE-ID-PLACEHOLDER-->", $DateId $clientOSList = @('Windows 11', 'Windows 10') $serverOSList = $OSList | Where-Object { $_ -notin $clientOSList } $clientUpdates = $clientOSList | ForEach-Object { @{ 'Title' = "$_ Updates"; 'Updates' = $Updates[$_] } } $serverUpdates = $serverOSList | ForEach-Object { @{ 'Title' = "$_ Updates"; 'Updates' = $Updates[$_] } } $clientUpdatesHtml = Show-OSUpdateSection $clientUpdates $serverUpdatesHtml = Show-OSUpdateSection $serverUpdates $html = $html -replace "<!--CLIENT-UPDATES-PLACEHOLDER-->", $clientUpdatesHtml $html = $html -replace "<!--SERVER-UPDATES-PLACEHOLDER-->", $serverUpdatesHtml } End { return $html } } #EndRegion '.\Public\New-PatchTuesdayReport.ps1' 93 #Region '.\Public\Send-AuditEmail.ps1' 0 function Send-AuditEmail { <# .SYNOPSIS This is a wrapper function for Send-MailKitMessage and takes string arrays as input. .DESCRIPTION Other Audit tasks can be used as the -AttachmentFiles parameter when used with the report switch. .EXAMPLE Send-AuditEmail -SMTPServer "smtp.office365.com" -Port 587 -UserName "Username@contoso.com" ` -From "Username@contoso.com" -To "user@anothercompany.com" -Pass (Read-Host -AsSecureString) -AttachmentFiles "$(Get-ADActiveUserAudit -Report)" -SSL This will automatically send the report zip via email to the parameters specified. There is no cleanup of files. Please cleanup the directory of zip's if neccessary. .EXAMPLE Send-AuditEmail -SMTPServer "smtp.office365.com" -Port 587 -UserName "Username@contoso.com" ` -From "Username@contoso.com" -To "user@anothercompany.com" -AttachmentFiles "$(Get-ADActiveUserAudit -Report)" -FunctionApp "MyVaultFunctionApp" ` -Function "MyClientSpecificFunction" -Token "ABCDEF123456" -SSL This will automatically send the report zip via email to the parameters specified. There is no cleanup of files. Please cleanup the directory of zip's if neccessary. .PARAMETER SMTPServer The SMTP Server address. For example: "smtp.office365.com" .PARAMETER AttachmentFiles The full filepath to the zip you are sending: -AttachmentFiles "C:\temp\ADHostAudit\2023-01-04_03.45.14_Get-ADHostAudit_AD.CONTOSO.COM.Servers.zip" The Audit reports output this filename if the "-Report" switch is used allowing it to be nested in this parameter for ease of automation. .PARAMETER Port The following ports can be used to send email: "993", "995", "587", "25" .PARAMETER UserName The Account authorized to send email via SMTP. From parameter is usually the same. .PARAMETER SSL Switch to ensure SSL is used during transport. .PARAMETER From This is who the email will appear to originate from. This is either the same as the UserName, or, if delegated, access to an email account the Username account has delegated permissions to send for. Link: https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/give-mailbox-permissions-to-another-user?view=o365-worldwide .PARAMETER To This is the mailbox who will be the recipient of the communication. .PARAMETER Subject The subject is automatically populated with the name of the function that ran the script, as well as the domain and hostname. If you specify subject in the parameters, it will override the default with your subject. .PARAMETER Body The body of the message, pre-populates with the same data as the subject line. Specify body text in the function parameters to override. .PARAMETER Pass Takes a SecureString as input. The password must be added to the command by using: -Pass (Read-Host -AsSecureString) You will be promted to enter the password for the UserName parameter. .PARAMETER Function If you are using the optional function feature and created a password retrieval function, this is the name of the function in Azure AD that accesses the vault. .PARAMETER FunctionApp If you are using the optional function feature, this is the name of the function app in Azure AD. .PARAMETER Token If you are using the optional function feature, this is the api token for the specific function. Ensure you are using the "Function Key" and NOT the "Host Key" to ensure access is only to the specific funtion. #> [CmdletBinding(DefaultParameterSetName = 'Pass')] param ( [Parameter( MandaTory = $true, HelpMessage = 'Enter the Zip file paths as comma separated array with quotes for each filepath', ValueFromPipelineByPropertyName = $true )][string[]]$AttachmentFiles, # Array of paths to zip files that will be attached to the email [string]$SMTPServer, # SMTP server for sending the email [Parameter( HelpMessage = 'Enter the port number for the mail relay', ValueFromPipelineByPropertyName = $true )] [ValidateSet("993", "995", "587", "25")] [int]$Port, # Port number for the mail relay [string]$UserName, # Username for SMTP authentication [switch]$SSL, # Whether to use SSL for the SMTP connection [string]$From, # Email address for the sender [string]$To, # Email address for the recipient [string]$Subject = "$($script:MyInvocation.MyCommand.Name -replace '\..*') report ran for $($env:USERDNSDOMAIN) on host $($env:COMPUTERNAME).", # Email subject line [string]$Body = "$($script:MyInvocation.MyCommand.Name -replace '\..*') report ran for $($env:USERDNSDOMAIN) on host $($env:COMPUTERNAME).", # Email body text [Parameter( ParameterSetName = 'Pass', HelpMessage = 'Enter this as the parameter: (Read-Host -AsSecureString)' )] [securestring]$Pass, # SecureString containing the password for SMTP authentication [Parameter( ParameterSetName = 'Func', HelpMessage = 'Enter the name of the Function as showing in the function app' )] [string]$Function, # Name of the function in the Azure Function App [Parameter( ParameterSetName = 'Func', HelpMessage = 'Enter the name of the function app' )] [string]$FunctionApp, # Name of the Azure Function App [Parameter( ParameterSetName = 'Func', HelpMessage = 'Enter the API key associated with the function. Not the Host Key.' )] [string]$Token # API key for the Azure Function App ) begin { # Install/Import Required Module # Tested Version: # https://www.powershellgallery.com/packages/Send-MailKitMessage/3.2.0-preview1 Updated: 11/8/2021 $params = @{ PrereleaseModuleNames = "Send-MailKitMessage" PrereleaseRequiredVersions = "3.2.0-preview1" Scope = "CurrentUser" } Initialize-ModuleEnv @params # Create recipient list $RecipientList = [MimeKit.InternetAddressList]::new() $RecipientList.Add([MimeKit.InternetAddress]$To) # Create attachment list $AttachmentList = [System.Collections.Generic.List[string]]::new() foreach ($currentItem in $attachmentfiles) { $AttachmentList.Add("$currentItem") } # From $From = [MimeKit.MailboxAddress]$From # Mail Account variable $User = $UserName if ($Pass) { # If the -Pass parameter is provided, set the credentials to the value of the parameter. $Credential = ` [System.Management.AuTomation.PSCredential]::new($User, $Pass) } elseif ($FunctionApp) { # If a function app name and API key are provided, retrieve credentials from the function app URL. $url = "https://$($FunctionApp).azurewebsites.net/api/$($Function)" $a, $b = (Invoke-RestMethod $url -Headers @{ 'x-functions-key' = "$Token" }).split(',') $Credential = ` [System.Management.AuTomation.PSCredential]::new($User, (ConvertTo-SecureString -String $a -Key $b.split(' ')) ) } } Process { # Set the parameters for the email message $Parameters = @{ "UseSecureConnectionIfAvailable" = $SSL "Credential" = $Credential "SMTPServer" = $SMTPServer "Port" = $Port "From" = $From "RecipientList" = $RecipientList "Subject" = $Subject "TextBody" = $Body "AttachmentList" = $AttachmentList } # Send the email using the Send-MailKitMessage cmdlet with the parameters above Send-MailKitMessage @Parameters } End { # Clear sensitive variables from memory Clear-Variable -Name "a", "b", "Credential", "Token" -Scope Local -ErrorAction SilentlyContinue } } #EndRegion '.\Public\Send-AuditEmail.ps1' 161 #Region '.\Public\Send-GraphAppEmail.ps1' 0 function Send-GraphAppEmail { <# .SYNOPSIS Sends an email using the Microsoft Graph API. .DESCRIPTION The Send-GraphAppEmail function uses the Microsoft Graph API to send an email to a specified recipient. The function requires the Microsoft Graph API to be set up and requires a pre-created Microsoft Graph API app to send the email. The AppName can be passed in as a parameter and the function will retrieve the associated authentication details from the Credential Manager. .PARAMETER AppName The pre-created Microsoft Graph API app name used to send the email. .PARAMETER To The email address of the recipient. .PARAMETER FromAddress The email address of the sender who is a member of the Security Enabled Group allowed to send email that was configured using the New-GraphEmailApp. .PARAMETER Subject The subject line of the email. .PARAMETER EmailBody The body text of the email. .PARAMETER AttachmentPath An array of file paths for any attachments to include in the email. .EXAMPLE Send-GraphAppEmail -AppName "GraphEmailApp" -To "recipient@example.com" -FromAddress "sender@example.com" -Subject "Test Email" -EmailBody "This is a test email." .NOTES The function requires the Microsoft.Graph and MSAL.PS modules to be installed and imported. #> [CmdletBinding()] param ( [Parameter(HelpMessage = "The Pre-created New-GraphEmailApp Name for sending the email.")] [ValidateNotNullOrEmpty()] [string]$AppName, [Parameter(Mandatory = $true, HelpMessage = "The email address of the recipient.")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] [string]$To, [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] [string]$FromAddress, [Parameter(Mandatory = $true, HelpMessage = "The subject line of the email.")] [ValidateNotNullOrEmpty()] [string]$Subject, [Parameter(Mandatory = $true, HelpMessage = "The body text of the email.")] [ValidateNotNullOrEmpty()] [string]$EmailBody, [Parameter(Mandatory = $false, HelpMessage = "An array of file paths for any attachments to include in the email.")] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path $_ -PathType 'Leaf' })] [string[]]$AttachmentPath ) begin { $Script:LogString = @() $Script:LogString += Write-AuditLog -Message "Begin Log" $Script:LogString += Write-AuditLog -Message "###############################################" # Install and import the Microsoft.Graph module. Tested: 1.22.0 $PublicMods = ` "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan", "MSAL.PS" $PublicVers = ` "1.1.2", "1.0.0", "4.37.0.0" $params1 = @{ PublicModuleNames = $PublicMods PublicRequiredVersions = $PublicVers Scope = "CurrentUser" } Initialize-ModuleEnv @params1 # If a GraphEmailApp object was not passed in, attempt to retrieve it from the local machine if ($AppName) { try { # Step 7: # Define the application Name and Encrypted File Paths. $Auth = Get-Secret -Name "$AppName" -Vault GraphEmailAppLocalStore -AsPlainText -ErrorAction Stop $delimiter = "|" $values = $Auth.Split($delimiter) # Create a new PSCustomObject using the values $authobj = [PSCustomObject] @{ AppId = $values[0] CertThumbprint = $values[1] TenantID = $values[2] CertExpires = $values[3] SendAsUser = $values[4] AppRestrictedSendGroup = $values[5] } $GraphEmailApp = $authobj } catch { Write-Error $_.Exception.Message } } # End Region If if (!$GraphEmailApp) { throw "GraphEmailApp object not found. Please specify the GraphEmailApp object or provide the AppName and RedirectUri parameters." } # End Region If # Instatiate the required variables for retreiving the token. $AppId = $GraphEmailApp.AppId $CertThumbprint = $GraphEmailApp.CertThumbprint $Tenant = $GraphEmailApp.TenantID $Expiration = $GraphEmailApp.CertExpires $Script:LogString += Write-AuditLog -Message "The Certificate $CertThumbprint will expire on $Expiration" # Retrieve the self-signed certificate from the CurrentUser's certificate store $cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } if (!($cert)) { throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store" } # End Region If Write-Output "The certificate thumbprint is $CertThumbprint" } # End Region Begin Process { # Authenticate with Azure AD and obtain an access token for the Microsoft Graph API using the certificate $MSToken = Get-MsalToken -ClientCertificate $Cert -ClientId $AppId -TenantId $Tenant -Authority "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" -ErrorAction Stop # Set up the request headers $authheader = @{Authorization = "Bearer $($MSToken.AccessToken)" } # Set up the request URL $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail" # Build the message body # Add a "from" field to the message object in $Message $FromField = @{ emailAddress = @{ address = "$($FromAddress)" } } $Message = @{ message = @{ subject = "$Subject" body = @{ contentType = "text" content = "$EmailBody" } toRecipients = @( @{ emailAddress = @{ address = "$To" } } ) from = $FromField } } if ($AttachmentPath) { $attachmentName = (Split-Path -Path $AttachmentPath -Leaf) $attachmentBytes = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($AttachmentPath)) $attachment = @{ "@odata.type" = "#microsoft.graph.fileAttachment" "Name" = $attachmentName "ContentBytes" = $attachmentBytes } $Message.message.attachments = $attachment } $jsonMessage = $message | ConvertTo-Json -Depth 4 $body = $jsonMessage } End { try { # Send the email message using the Invoke-RestMethod cmdlet $Script:LogString += Write-AuditLog -Message "Sending email via Microsoft Graph" Invoke-RestMethod -Headers $authHeader -Uri $url -Body $body -Method POST -ContentType 'application/json' $Script:LogString += Write-AuditLog -Message "Message sent to $To from $FromAddress with $(($Message.message.attachments).Count) attachments." } catch { throw $_.Exception } } # End Region End } #EndRegion '.\Public\Send-GraphAppEmail.ps1' 171 #Region '.\Public\Submit-FTPUpload.ps1' 0 function Submit-FTPUpload { <# .SYNOPSIS Uploads a file to an FTP server using the WinSCP module. .DESCRIPTION The Submit-FTPUpload function uploads a file to an FTP server using the WinSCP module. The function takes several parameters, including the FTP server name, the username and password of the account to use, the protocol to use, and the file to upload. .PARAMETER FTPUserName Specifies the username to use when connecting to the FTP server. .PARAMETER Password Specifies the password to use when connecting to the FTP server. .PARAMETER FTPHostName Specifies the name of the FTP server to connect to. .PARAMETER Protocol Specifies the protocol to use when connecting to the FTP server. The default value is SFTP. .PARAMETER FTPSecure Specifies the level of security to use when connecting to the FTP server. The default value is None. .PARAMETER SshHostKeyFingerprint Specifies the fingerprint of the SSH host key to use when connecting to the FTP server. This parameter is mandatory with SFTP and SCP. .PARAMETER LocalFilePath Specifies the local path to the file to upload to the FTP server. .PARAMETER RemoteFTPPath Specifies the remote path to upload the file to on the FTP server. .OUTPUTS The function does not generate any output. .EXAMPLE PS C:\> Submit-FTPUpload -FTPUserName "username" -Password $Password -FTPHostName "ftp.example.com" -Protocol "Sftp" -FTPSecure "None" -SshHostKeyFingerprint "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff" -LocalFilePath "C:\temp\file.txt" -RemoteFTPPath "/folder" In this example, the Submit-FTPUpload function is used to upload a file to an FTP server. The FTP server is named "ftp.example.com" and the file to upload is located at "C:\temp\file.txt". The SSH host key fingerprint is also provided. .NOTES This function requires the WinSCP PowerShell module. .LINK https://winscp.net/eng/docs/library_powershell #> [CmdletBinding()] param ( [string]$FTPUserName, # FTP username [securestring]$Password, # FTP password [string]$FTPHostName, # FTP host name [ValidateSet("Sftp", "SCP", "FTP", "Webdav", "s3")] [string]$Protocol = "Sftp", # FTP protocol [ValidateSet("None", "Implicit ", "Explicit")] [string]$FTPSecure = "None", # FTP security #[int]$FTPPort = 0, # Not used # Mandatory with SFTP/SCP [string[]]$SshHostKeyFingerprint, # SSH host key fingerprint #[string]$SshPrivateKeyPath, # Not used [string[]]$LocalFilePath, # Local file path # Send-WinSCPItem # './remoteDirectory' [string]$RemoteFTPPath # Remote FTP path ) process { # This script will run in the context of the user. Please be sure it's a local admin with cached credentials. # Required Modules Import-Module WinSCP # Capture credentials. $Credential = [System.Management.Automation.PSCredential]::new($FTPUserName, $Password) # Open the session using the SessionOptions object. $sessionOption = New-WinSCPSessionOption -Credential $Credential -HostName $FTPHostName -SshHostKeyFingerprint $SshHostKeyFingerprint -Protocol $Protocol -FtpSecure $FTPSecure # New-WinSCPSession sets the PSDefaultParameterValue of the WinSCPSession parameter for all other cmdlets to this WinSCP.Session object. # You can set it to a variable if you would like, but it is only necessary if you will have more then one session open at a time. $WinSCPSession = New-WinSCPSession -SessionOption $sessionOption # Check if the remote FTP path exists. If it doesn't, create it. if (!(Test-WinSCPPath -Path $RemoteFTPPath -WinSCPSession $WinSCPSession)) { New-WinSCPItem -Path $RemoteFTPPath -ItemType Directory -WinSCPSession $WinSCPSession } # Upload each file in the local file path array to the remote FTP path. $errorindex = 0 foreach ($File in $LocalFilePath) { $sendvar = Send-WinSCPItem -Path $File -Destination $RemoteFTPPath -WinSCPSession $WinSCPSession -ErrorAction Stop -ErrorVariable SendWinSCPErr if ($sendvar.IsSuccess -eq $false) { $Script:LogString += Write-AuditLog -Message $SendWinSCPErr -Severity Error $errorindex += 1 } } # If there was an error during the file upload, throw an error and exit. if ($errorindex -ne 0) { Write-Output "Error" throw 1 } # Close and remove the session object. Remove-WinSCPSession -WinSCPSession $WinSCPSession } } #EndRegion '.\Public\Submit-FTPUpload.ps1' 89 |