The script works with no issue but instead of out put i want to use custom object
####################
# Get AD Site List #
####################
Write-Verbose “Get AD Site List `r”
[array] $ADSites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites
$ADSitesCount = $ADSites.Count
Write-Output “There are $ADSitesCount AD Sites `r”
ForEach ($Site in $ADSites)
{ ## OPEN ForEach Site in ADSites
$SiteName = $Site.Name
$SiteSubnets = $Site.Subnets
Write-Output “Site Name: $SiteName `r”
Write-Output “Site Servers: $SiteServers `r”
Write-Output ” `r”
}
Updated Script:
#################### # Get AD Site List # ####################
Write-Verbose “Get AD Site List r”
$ADSites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites
#$adsites= #{}
$ADSitesCount = $ADSites.Count
Write-Output “There are $ADSitesCount AD Sites r”
$properties = #{'SiteName'=$ADSites.Name; 'SiteServers'=$ADSites.server; }
$object = New-Object –TypeName PSObject –Prop $properties
Write-Output $object
The problem lies in this line:
$properties = #{'SiteName'=$ADSites.Name; 'SiteServers'=$ADSites.server; }
$ADSites is an array. It contains all sitenames so what you will create looks like this:
Sites
----
{a, b, c}
The solution is to create an array that holds multiple objects and add one object per site:
$collection= #()
foreach($site in $ADSites){
$properties = #{ "Name" =$site.Name; "SiteServers"=$site.Server}
$collection +=(New-Object –TypeName PSObject –Prop $properties )
}
$collection
Write-Output is used to output to the pipeline not for producing screen output
Not sure if you just might be over thinking this but this will give you the same information that you are going for in a table format simply using Select-Object to get only the properties you want
$ADSites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites
Write-Output “There are $($ADSites.Count) AD Sites `r”
$ADSites | Select-Object name,servers
Obfuscated output
There are 4 AD Sites
Name Servers
---- -------
SITE1 {SERVER1.DOMAIN.NET, SERVER2.DOMAIN.NET}
STE345 {SERVER3.DOMAIN.NET}
LOCCOMP {SERVER4.DOMAIN.NET}
THING {SERVER9720.DOMAIN.NET, SERVER7.DOMAIN.NET}
Related
I have following powershell script reading from csv and exporting to another csv. It's working in terms of basic functionality. Script below is currently exporting as such:
USERS
jdoe
mprice
tsmith
Add-PSSnapin microsoft.sharepoint.powershell -ErrorAction SilentlyContinue
# csv file name
[parameter(Mandatory=$false)][string]$CsvFilePath = ".\AllSiteCollectionsLocal.csv"
$csvItems = Import-Csv $CsvFilePath
$resultsarray = #()
$firstObject = New-Object PSObject
# iterate lines in csv
foreach($Item in $csvItems)
{
$site = new-object Microsoft.SharePoint.SPSite($Item.SiteCollection)
$web = $site.openweb()
$siteUsers = $web.SiteUsers
Write-Host $Item.SiteCollection -ForegroundColor Green
foreach($user in $siteUsers)
{
Write-Host $user.LoginName
$loginnames = #{
USERS = $user.LoginName
}
$resultsarray += New-Object PSObject -Property $loginnames
}
$web.Dispose()
$site.Dispose()
$resultsarray | export-csv -Path c:\temp\sitesandusers.csv -NoTypeInformation
}
I need to export as below. Note, I dont even need a header, but do need $Item.SiteCollection value to print out between each iteration of users under each site, so the outer foreach needs to print $Item.SiteCollection then the inner foreach would print $user.LoginName
http://test1.com
jdoe
mprice
http://test2.com
tsmith
I'm guessing you wanted to do parameters for your script to be called from elsewhere? As of now, your metadata attribute on $CsvFilePath are redundant to what PowerShell already does for you.
As for your question, you would just have to append $Item.SiteCollection to your PSObject. This too isn't needed as PowerShell streaming capabilities allow you to assign directly to a variable; so no need for += - which can be computationally expensive on larger lists slowing overall performance. Now we end up with:
Param (
[parameter(Mandatory=$false)]
[string]$CsvFilePath = ".\AllSiteCollectionsLocal.csv"
)
Add-PSSnapin microsoft.sharepoint.powershell -ErrorAction SilentlyContinue
$csvItems = Import-Csv $CsvFilePath
$variable = foreach($Item in $csvItems)
{
$site = new-object Microsoft.SharePoint.SPSite($Item.SiteCollection)
$web = $site.openweb()
$siteUsers = $web.SiteUsers
Write-Host -Object $Item.SiteCollection -ForegroundColor Green
Write-Output -InputObject $Item.SiteCollection
foreach($user in $siteUsers)
{
Write-Host -Object $user.LoginName
Write-Output -InputObject $user.LoginName
}
$null = $web.Dispose()
$null = $site.Dispose()
}
$variable | Out-File -FilePath 'c:\temp\sitesandusers.csv'
Bypassing $variable you can assign the output directly to the file placing the export outside the first foreach statement.
This requires the use of a sub-expression operator $() to wrap around the loop.
Also added a Param ( ) statement for your parameter declaration.
Didn't mess with the parameter attributes as it can show the Authors intentions regardless if it's needed or not.
Probably should add that, Write-Output will explicitly write to the success stream allowing the values to be assigned to the variable, whereas Write-Host writes to the information stream, so no object pollution (duplicates) occur.
This question already has answers here:
Unexpected ConvertTo-Json results? Answer: it has a default -Depth of 2
(2 answers)
Closed 2 years ago.
My goal was to create a collection of objects (SharePoint sites) with in each object another collection of objects (Lists in that sites). After writing the code below I thought I succeeded but I don't know how to access the Lists objects.
$sites = #()
foreach ($s in $Subsites){
Connect-PnPOnline -Url $s.Url
$Lists = Get-PnPList
$ctx = Get-PnPContext
$web = $ctx.web
$ctx.Load($web)
$ctx.ExecuteQuery()
$listsCollection = #()
foreach ($list in $Lists) {
$props = #{
ListName = $list.Title
ListItems = $list.ItemCount
LastDeletedDate = $list.LastItemDeletedDate
LastModifiedDate = $list.LastItemUserModifiedDate
}
$listObj = New-Object -TypeName PSObject -Property $props
$listsCollection += $listObj
}
$props = #{
SiteName = $s.Title
LastModified = $web.LastItemUserModifiedDate
URL = $web.Url
Lists = $listsCollection
}
$webObj = New-Object -TypeName PSObject -Property $props
$sites += $webObj
}
After running the code I can access the site information like I expected to do
$sites[0].SiteName gives me: "My site name"
And I can see the list information in the object too but it seems to me that it is only string information and not real objects.
$sites[0].Lists gives me:
#{LastDeletedDate=06/12/2019 09:24:57; LastModifiedDate=06/12/2019 09:27:30; ListName=MyList1; ListItems=6}
#{LastDeletedDate=04/19/2019 12:48:14; LastModifiedDate=04/19/2019 12:48:14; ListName=MyList2; ListItems=0}
but I can't acces ListName by using $sites[0].Lists[0].ListName . Get-Member gives me just one property Length. The TypeName of the object is System.String. I tried several other things like using other ways to create a CustomObject and using select -ExpandProperty or Key and Value but no succes either.
Sorry, I didn't include the intermediate steps I used. In that steps I output the $sites object with $sites | ConvertTo-Json | Out-File .\sites.json and later on I import it again with Get-Content .\stes.json | ConvertFrom-Json. After adding the Depth parameter with ConvertToJson -Depth 5 as suggested by AdminOfThings it worked perfectly
I need to grab members in particular AD group and add them into array. Using net group I can easily get the members of AD group.
However, I am not familier with the filter on Windows. I just want to get the user name from output.
Group name test
Comment
Members
---------------------------------------------------------------------
mike tom jackie
rick jason nick
The command completed successfully.
I can't use Get-ADGroupMember command using PowerShell. If there is a way to get a data and filter using PowerShell, it is also OK.
Well, the good news is that there is rarely only one way to do things in PowerShell. Here's part of a larger script I have on hand for some group related things where I don't always have the AD module available (such as on servers that other teams own):
$Identity = 'test'
$LDAP = "dc="+$env:USERDNSDOMAIN.Replace('.',',dc=')
$Filter = "(&(sAMAccountName=$Identity)(objectClass=group))"
$Searcher = [adsisearcher]$Filter
$Searcher.SearchRoot = "LDAP://$LDAP"
'Member','Description','groupType' | %{$Searcher.PropertiesToLoad.Add($_)|Out-Null}
$Results=$Searcher.FindAll()
$GroupTypeDef = #{
1='System'
2='Global'
4='Domain Local'
8='Universal'
16='APP_BASIC'
32='APP_QUERY'
-2147483648='Security'
}
If($Results.Count -gt 0){
$Group = New-Object PSObject #{
'DistinguishedName'=[string]$Results.Properties.Item('adspath') -replace "LDAP\:\/\/"
'Scope'=$GroupTypeDef.Keys|?{$_ -band ($($Results.properties.item('GroupType')))}|%{$GroupTypeDef.get_item($_)}
'Description'=[string]$Results.Properties.Item('description')
'Members'=[string[]]$Results.Properties.Item('member')|% -Begin {$Searcher.PropertiesToLoad.Clear();$Searcher.PropertiesToLoad.Add('objectClass')|Out-Null} {$Searcher.Filter = "(distinguishedName=$_)";[PSCustomObject][ordered]#{'MemberType'=$Searcher.FindAll().Properties.Item('objectClass').ToUpper()[-1];'DistinguishedName'=$_}}
}
$Group|Select DistinguishedName,Scope,Description
$Group.Members|FT -AutoSize
}
Else{"Unable to find group '$Group' in '$env:USERDNSDOMAIN'.`nPlease check that you can access that domain from your current domain, and that the group exists."}
Here's one way to get the direct members of an AD group without using the AD cmdlets:
param(
[Parameter(Mandatory)]
$GroupName
)
$ADS_ESCAPEDMODE_ON = 2
$ADS_SETTYPE_DN = 4
$ADS_FORMAT_X500 = 5
function Invoke-Method {
param(
[__ComObject]
$object,
[String]
$method,
$parameters
)
$output = $object.GetType().InvokeMember($method,"InvokeMethod",$null,$object,$parameters)
if ( $output ) { $output }
}
function Set-Property {
param(
[__ComObject]
$object,
[String]
$property,
$parameters
)
[Void] $object.GetType().InvokeMember($property,"SetProperty",$null,$object,$parameters)
}
$Pathname = New-Object -ComObject "Pathname"
Set-Property $Pathname "EscapedMode" $ADS_ESCAPEDMODE_ON
$Searcher = [ADSISearcher] "(&(objectClass=group)(name=$GroupName))"
$Searcher.PropertiesToLoad.AddRange(#("distinguishedName"))
$SearchResult = $searcher.FindOne()
if ( $SearchResult ) {
$GroupDN = $searchResult.Properties["distinguishedname"][0]
Invoke-Method $Pathname "Set" #($GroupDN,$ADS_SETTYPE_DN)
$Path = Invoke-Method $Pathname "Retrieve" $ADS_FORMAT_X500
$Group = [ADSI] $path
foreach ( $MemberDN in $Group.member ) {
Invoke-Method $Pathname "Set" #($MemberDN,$ADS_SETTYPE_DN)
$Path = Invoke-Method $Pathname "Retrieve" $ADS_FORMAT_X500
$Member = [ADSI] $Path
"" | Select-Object `
#{
Name="group_name"
Expression={$Group.name[0]}
},
#{
Name="member_objectClass"
Expression={$member.ObjectClass[$Member.ObjectClass.Count - 1]}
},
#{
Name="member_sAMAccountName";
Expression={$Member.sAMAccountName[0]}
}
}
}
else {
throw "Group not found"
}
This version uses the Pathname COM object to handle name escaping and outputs the the object class and sAMAccountName for each member of the group.
I found this function I'd like to use in a script I'm writing, but it keeps coming back $false when I can see an account is in a group and I can't figure out why?
function Check-IsGroupMember{
Param($user,$grp)
$strFilter = "(&(objectClass=Group)(name=" + $grp +"))"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.PageSize = 1000
$objSearcher.Filter = $strFilter
$objSearcher.SearchScope = "Subtree"
$colResults = $objSearcher.FindOne()
$objItem = $colResults.Properties
([string]$objItem.member).contains($user)
}
Usage:
Check-IsGroupMember "name of user" "DomainAdmins"
$objItem.member contains the DistinguishedName value of each principal who is a member of the group.
Even though the proper name of a person might be John Doe, the common name of the user account object may still be Doe, John, John G. Doe or anything else. This means that Contains() check (which is just a simple substring search) is not guaranteed to work as you expect.
The only real way to check is to either run another search for the user to find his/her DistinguishedName.
Personally, I would go for the AD PowerShell module from RSAT, rather than using a DirectorySearcher:
function Test-GroupMembership
{
Param(
[string]$UserName,
[string]$GroupName
)
$User = Get-ADUser -Identity $UserName
$Group = Get-ADGroup -Identity $GroupName -Properties member
$Group.member -contains $User.DistinguishedName
}
If size limit is your problem, you can use the DirectoryServer to retrieve a ranged result of the member attribute:
function Test-GroupMembership
{
[CmdletBinding()]
Param(
[string]$UserName,
[string]$GroupName
)
# Fetch User
$User = Get-ADUser -Identity $UserName
# return on failure
if(-not $User){
Write-Error -Message ('User "{0}" not found' -f $GroupName)
return $false
}
# Use DirectorySearcher to retrieve ranged member attribute
$GroupSearcher = '' -as [adsisearcher]
$GroupSearcher.Filter = '(&(objectClass=group)(name={0}))' -f $GroupName
$GroupSearcher.SearchScope = 'Subtree'
$GroupSearcher.SearchRoot = '' -as [adsi]
# AD reponds with at least 1500 values per multi-value attribute since Windows Server 2003
$Start = 1
$Range = 1500
$GroupMembers = #()
$HasMoreMembers = $false
# Keep retrieving member values until we've got them all
do{
# Use range operator to "page" values
# Ref: https://msdn.microsoft.com/en-us/library/aa367017(v=vs.85).aspx
$RangedMember = 'member;range={0}-{1}' -f $Start,$($Start + $Range - 1)
$GroupSearcher.PropertiesToLoad.Add($RangedMember) | Out-Null
# Retrieve group
$Group = $GroupSearcher.FindOne()
# return on failure
if(-not $Group) {
Write-Error -Message ('Group "{0}" not found' -f $GroupName)
return $false
}
# If we've reached the end of the member list,
# AD will return a property where the upper range
# value is *, so it might not be the same property
# name we specified in PropertiesToLoad
$ReturnedMember = #($Group.Properties.PropertyNames) -like 'member;*'
# Add all members to the $GroupMembers variable
foreach($member in $Group.Properties."$ReturnedMember") {
# Test if user is in the member list
if($member -eq $User.DistinguishedName){
return $true
}
}
# If we've reached the end, exit the loop
if($ReturnedMember -eq $RangedPropertyName){
$HasMoreMembers = $true
}
} while ($HasMoreMembers)
# User wasn't found
return $false
}
To provide a bit of consistency in user experience, please use Approved Verbs for command names in PowerShell (eg. Test-* instead of Check-*)
[adsisearcher] is a type accelerator for the DirectorySearcher class
So basically, what I have here is a script that will scan a CSV that it imports, and for every entry in the spreadsheet, except for people in the RANDOM.DOMAIN, it will find the managers email address and send an automated email to the manager telling them user XYZ is about to expire soon, and they need to do something about it.
If the managers email is unavailable for some reason, then it defaults to sending the email to me.
This script works well.
The problem I am running into is, I want to make it so only one email is sent to each manager, despite multiple users (or entries) from the spreadsheet, list them as the manager.
I.e. if Joe Bloggs has a manager Aaron T and Jane Doe has the manager Aaron T, then Aaron T will get two emails, one email for each user.
MY QUESTION:
Is there an easy way to only get it to send one email per manager, even if that manager has multiple users reporting to them that are about to expire?
$datai = Import-Csv "Soon-to-expire User Accounts22.csv" | select 'Display Name',Manager,'Domain Name','Account Expiry Time'
Connect-QADService -Service another.DC | Out-Null
$expiringUsers = #{}
foreach ($i in $datai) {
$dn = $i.'Display Name'
$dn1 = $i.'Domain Name'
$man = $i.'Manager'
$aet = $i.'Account Expiry Time'
$subject = "Account about to expire: $dn"
$getmail = get-qaduser "$man" -LdapFilter '(mail=*)' | select mail
$emailAD = $getmail.mail
if ($man -eq "-" -or $man -like 'CN=*' -or $getmail -eq $null -or $man -eq "") {
$man = "Aaron T"
$getmail = get-qaduser "$man" -LdapFilter '(mail=*)' | select mail
$emailAD = $getmail.mail
}
if ($expiringUsers.Contains($emailAD)) {
$expiringUsers[$emailAD]["dn"] += $dn += "`n"
$expiringUsers[$emailAD]["aet"] += $aet += "`n"
$expiringUsers[$emailAD]["man"] += $man += "`n"
} else {
$expiringUsers[$emailAD] = #{
#"dn1" = $dn1
#"aet" = $aet
#"man" = $man
# "dn" = #( $dn )
}
}
}
$expiringUsers | fc #as suggested
foreach ($emailAD in $expiringUsers.Keys) {
$dn = $expiringUsers[$emailAD]["dn"]
$dn1 = $expiringUsers[$emailAD]["dn1"]
$man = $expiringUsers[$emailAD]["man"]
$aet = $expiringUsers[$emailAD]["aet"]
$subject = "Account/s About to Expire!"
$content = #"
Hi,
$dn `n
$dn1 `n
$man `n
$aet `n
$emailAD `n
Technology Services
"#
Send-MailMessage -from "aaron#website.com" `
-To $emailAD `
-Subject $subject `
-Body $content `
-Priority high `
-smtpServer "relay.server"
#using this as a test instead of sending mass emais all the time
Write-Host $content
}
UPDATED with the new script as requested.... still having issues.
Is there an easy way to only get it to send one email per manager, even if that manager has multiple users reporting to them that are about to expire?
For this you need to defer e-mail processing. Collect the users in a hashtable, e.g. by manager e-mail address:
...
$expiringUsers = #{}
foreach ($i in $datai) {
If ($i.'Domain Name' -notmatch "RANDOM.DOMAIN") {
...
if ($expiringUsers.Contains($emailAD)) {
$expiringUsers[$emailAD]["dn"] += $dn
} else {
$expiringUsers[$emailAD] = #{
"dn1" = $dn1
"aet" = $aet
"man" = $man
"dn" = #( $dn )
}
}
}
}
and move the actual e-mail processing outside the loop:
foreach ($emailAD in $expiringUsers.Keys) {
$dn1 = $expiringUsers[$emailAD]["dn1"]
$man = $expiringUsers[$emailAD]["man"]
$aet = $expiringUsers[$emailAD]["aet"]
$subject = "Account about to expire: $($expiringUsers[$emailAD]["dn"])"
$content = #"
Hi,
...
Technology Services
"#
Send-MailMessage -from "Test Script - Powershell <email#test.com>" `
-To "$emailAD" `
-Subject $subject `
-Body $content `
-Priority high `
-smtpServer servername
Write-Host "Mail Sent to $man"
}
Note that for simplicity reasons the above code only records the expiry date of the first user. If you want the expiry date of each user recorded separately, you'll have to take additonal steps, e.g.
$expiringUsers[$emailAD]["expiry"] += #{
"name" = $dn;
"date" = $aet;
}
instead of
$expiringUsers[$emailAD]["dn"] += $dn
So I finally decided to revisit this script, after many, many months.
I'm get a little better at PowerShell and while I'm sure this isn't the most effective way to do it, this is something that works for me.
I've also changed the input method; it pulls the information directly from AD, instead of using a CSV file that used to be generated from an application called 'AD Manager Plus' (Hate it).
Remember, using Quest CMDlets here because we don't have a 2008 environment. (so using Get-QADUser instead of Get-ADuser)
FYI, I have only posted the code here which sorts out the data into separate tables - you can decide how you want to utilize those results. For our environment, I have it build an nice HTML table and body, then send it to the appropriate manager to deal with.
#user data input
$data = get-qaduser -SizeLimit 0 -includedproperties accountexpires | where {$_.AccountExpires -ne $null -and $_.AccountExpires -le ((Get-Date).AddDays(45)) }
#get a list of managers, unique.
$uniqueMan = $data | select Manager -Unique
#foreach manager from $uniqueman
Foreach ($manager in $uniqueman) {
#create the array variable / clear it out for the next manager.
$myarray = #()
#foreach User found in in $data query
Foreach ($user in $data) {
#Search the $user's query for people with the same manager as that of the $uniqueman list.
If ($user.Manager -eq $manager.Manager) {
#do what with the result.
#add the person to an array
$myarray += New-Object psobject -Property #{
Name = $user.'Name'
UserName = $user.'SAMAccountName'
AccountExpires = $user.'AccountExpires'
Manager = $user.Manager
}
}
#for testing, to output the results to an HTML file.
#$myarray | ConvertTo-Html | Out-File ("C:\test\" + $manager.Manager + ".html")
}
}