Wednesday, March 31, 2021

Powershell Logon hours comparison - Updated

Not so long ago, I posted my functioning logon hours comparison script . . a few years and more powershell know-how has made my current self a-bit shocked at how my old script attempts look to my eyes now.

And so , I had a feeling to do a newer re-write: 

- same premise as the old script
- only edits required are the "username" values
- the CSV to XLSX conversion needs Excel installed on the machine running the script. 

**************************Begin Script***********************************

Function CSVtoXLSX
{
    [cmdletbinding()]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [string]$arg1,
       
        [Parameter(Mandatory=$true, Position=1)]
        [string]$arg2
    )
    $XL = new-object -comobject Excel.application
    $XL.visible=$false
    $XL.displayalerts=$false
    $XL.workbooks.open("$arg1").SaveAs("$arg2",51)
    $XL.quit()
    #
    $XL2=new-object -comobject Excel.application
    $XL2.visible=$false
    $XL2.displayalerts=$false
    $WB=$XL2.workbooks.open("$arg2")
    $ws1=$wb.worksheets.item(1)
    $ws1.activate()
    #
    # Find the column count, to define our working-range.
    #
    $findrange=$ws1.usedrange.cells
    $colcount=$findrange.columns.count
    $workrange=$ws1.range($ws1.cells.item(1, 1), $ws1.cells.item(1, $colcount))
    #
    # Adjust the first row into a Header type Row
    #
    $workrange.font.bold=$true
    $workrange.interior.colorindex=15
    #
    # Set autofilter for all columns, and then autofit all columns.
    #
    $xl2.selection.autofilter() | out-null
    [void]$ws1.cells.entirecolumn.autofit()
    #
    # Save changes and close out.
    #
    $wb.saveas("$arg2")
    $wb.close()
    $XL2.quit()
}

import-module activedirectory

$report=@{}

$Default=get-aduser username -properties * # Adjust the username value 
$Defaultcheck=$default.logonhours
$Allhours=get-aduser username -properties * # Adjust the username value
$Allhourscheck=$allhours.logonhours

$users=get-aduser -filter 'enabled -eq $true' -properties *

foreach ($user in $users)
{
$Name=$user.name
$Manager=$user.manager
$Userhours=$user.logonhours
$Temp=[ordered]@{}
$Temp.Name=$Name
#
If ($userhours -eq $null)
{
$Temp.LogonHours="Not Set"
If ($manager -eq $null)
{
$Temp.Manager="Not Set"
$Report +=new-object -typename psobject -property $Temp
}
ELSE
{
$Manquery=get-aduser $Manager
$Manname=$manquery.name
$Temp.Manager=$Manname
$Report +=new-object -typename psobject -property $Temp
}
}
ELSE
{
$Compare1="$Userhours" -match "$Defaultcheck"
If ($Compare1 -eq $True)
{
$Temp.LogonHours="Default"
If ($manager -eq $null)
{
$Temp.Manager="Not Set"
$Report +=new-object -typename psobject -property $Temp
}
ELSE
{
$Manquery=get-aduser $Manager
$Manname=$manquery.name
$Temp.Manager=$Manname
$Report +=new-object -typename psobject -property $Temp
}
}
$Compare2="$Userhours" -match "$Allhourscheck"
If ($Compare2 -eq $True)
{
$Temp.Logonhours="All hours access"
If ($manager -eq $null)
{
$Temp.Manager="Not Set"
$Report +=new-object -typename psobject -property $Temp
}
ELSE
{
$Manquery=get-aduser $Manager
$Manname=$manquery.name
$Temp.Manager=$Manname
$Report +=new-object -typename psobject -property $Temp
}
}
}
$csv="C:\temp\logonhoursCompareReport.csv"
$xlsx="C:\temp\logonhoursCompareReport.xlsx"
$Report | export-csv "$csv" -notypeinformation
CSVtoXLSX $csv $xlsx
RI $CSV 
***********************end Script********************************************

Thursday, September 17, 2020

Powershell: Change static DNS values on the fly

Problem = you have a metric crap ton of statically IP assigned windows machines, and you want to change their DNS settings. 

 I've seen much of this info posted elsewhere, but it feels like no one really puts it all together very well, so here's my own take on how to pull this particular trick off. 

First off, the commands needed to "set" new DNS values, want to know which "adapter" on a machine you're going to change, this is returned as a number value with a name of "interfaceindex". That number VARIES per machine. 

So the following can be run per machine, which will quickly find the proper adapter to adjust and adjust it:


******************Begin script********************

$adapter=get-netadapter | select -expandproperty interfaceindex

$newdns1="1.1.1.1"

$newdns2="2.2.2.2"

set-dnsclientserveraddress -interfaceindex $adapter -serveraddresses ($newdns1,$newdns2)

****************End script***********************


run interactively, these command would require elevation to execute, so plan for that in your deployment. 

This change is immediate, with no event log errors generated, or service outages caused. 


 



Friday, March 27, 2020

Powershell: Function: Convert CSV into auto-filtered XLSX files

The purpose of this powershell function is to be used as a means of within script generating report like files.

This function has been generalized for use under a variety of situations, but expect that so long as your data is broken out within the input CSV the result XLSX file should have:

- Top row - to last filled column is assumed to be a header row.
  - this row gets bolded, a filled in color, and the filters are set at this layer.

System executing a script that contains this function needs to have excel installed and working.

**********************script begin***************************************

Function CSVtoXLSX
{
    [cmdletbinding()]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [string]$arg1,
       
        [Parameter(Mandatory=$true, Position=1)]
        [string]$arg2
    )
    $XL = new-object -comobject Excel.application
    $XL.visible=$false
    $XL.displayalerts=$false
    $XL.workbooks.open("$arg1").SaveAs("$arg2",51)
    $XL.quit()
    #
    $XL2=new-object -comobject Excel.application
    $XL2.visible=$false
    $XL2.displayalerts=$false
    $WB=$XL2.workbooks.open("$arg2")
    $ws1=$wb.worksheets.item(1)
    $ws1.activate()
    #
    # Find the column count, to define our working-range.
    #
    $findrange=$ws1.usedrange.cells
    $colcount=$findrange.columns.count
    $workrange=$ws1.range($ws1.cells.item(1, 1), $ws1.cells.item(1, $colcount))
    #
    # Adjust the first row into a Header type Row
    #
    $workrange.font.bold=$true
    $workrange.interior.colorindex=15
    #
    # Set autofilter for all columns, and then autofit all columns.
    #
    $xl2.selection.autofilter() | out-null
    [void]$ws1.cells.entirecolumn.autofit()
    #
    # Save changes and close out.
    #
    $wb.saveas("$arg2")
    $wb.close()
    $XL2.quit()
}

*********************************end script********************

Use case:

$input="c:\testfolder\testfile.csv"
$output="c:\testfolder\convertedreport.csv"

CSVtoXLSX $input $output

Note:

The default sheet name given to the spreadsheet created will be the name of the original CSV file.

Random Notes:

I have for many years now, leveraged a VBS script to perform the same tasks I wrote this powershell function for. It still works wonderfully, I just wanted to see if I could do this conversion within powershell itself vs calling a seperate script.

There may be additional edits to this one adding features going forward, but the base code as it is solid and meant to be slapped into any existing powershell script. 


Thursday, February 20, 2020

Powershell: reporting on simple ldap DC connections

Every windows admin should be aware of what's coming March 2020:

https://support.microsoft.com/en-us/help/4520412/2020-ldap-channel-binding-and-ldap-signing-requirement-for-windows

The general recommendation at this point is to make this registry key adjustment to all your domain controllers:

# Enable Simple LDAP Bind Logging

Reg Add HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics /v "16 LDAP Interface Events" /t REG_DWORD /d 2

Once this has been done, you can monitor the event log on your DCs for event ID 2889 under the directory service log . . or you can run my script to check all your servers, and create a single report of all connections over the last 24 hours.

My script is based off the nice work by "Russell Tomkins" from Microsoft, his version available here:

https://github.com/russelltomkins/active-directory

The differences between our versions, his checks a single dc, mine checks the domain gets a list of DC's to check, then creates a report of all connections across them all.

The only parts to edit, are the lines for where to find the OU for he domain controllers, enter your domain name. And the path for the output CSV needs to exist as well.

hope it helps

**********start script***************

import-module activedirectory
cls
echo " "
echo " "
#
# Create shell arrays for holding the 2 needed data sets.
#
$Comps=@()
$Data=@()
#
# Gather list of Domain controllers
#
$Comps=get-adcomputer -filter * -searchbase "OU=Domain Controllers,DC=YOURDOMAINNAMEGOESHERE!!!,DC=com" | Sort Name
$compstocheck=$comps.count
#
# Gather data from each server's event logs, pull into single array.
#
echo " "
write-host "I found $compstocheck domain controllers, and will start checking their event data one by one" -foregroundcolor green
echo " "
ForEach ($DC in $Comps)
    {
        $dcname=$DC.name
        echo " "
        write-host "Pulling events from $dcname" -foregroundcolor Yellow
        echo " "
        $hours=24
        $Events=get-winevent -computername $dcname -filterhashtable @{Logname='Directory Service';Id=2889; StartTime=(get-date).AddHours("-$hours")} -ea silentlycontinue
        write-host "Processing events from $dcname" -foregroundcolor Cyan
        echo " "
        ForEach ($Event in $Events)
            {
$Etime=$Event.Timecreated
                $eventXML = [xml]$Event.ToXml()
                $Client = ($eventXML.event.EventData.Data[0])
                $IPAddress = $Client.SubString(0,$Client.LastIndexOf(":"))
                $Port = $Client.SubString($Client.LastIndexOf(":")+1)
                $User = $eventXML.event.EventData.Data[1]
                Switch ($eventXML.event.EventData.Data[2])
{
                        0 {$BindType = "Unsigned"}
                        1 {$BindType = "Simple"}
                    }
                $Row="" | select DCname,IPAddress,Port,User,BindType,TimeCreated
                $Row.DCname=$dcname
                $Row.IPAddress=$IPAddress
                $Row.Port=$Port
                $Row.User=$User
                $Row.BindType=$BindType
                $Row.TimeCreated=$ETime
                #
# Add the found event data to the master array
                #
                $Data +=$Row
            }
        write-host "Completed processing all related events for $DCname, moving on" -foregroundcolor Green
        echo " "
        }
#
#
write-host "Generating report" -foregroundcolor Green
echo " "
$Reportcsv="C:\SimpLdap\SimpleLdapReport.csv"
#
$Data | export-csv "$Reportcsv" -notypeinformation
#
*************End Script***********

Wednesday, July 24, 2019

Powershell - Arrays and how they handle multi-lined input data

For years with various methods of scripting I've often fallen to using temporary files as a means to store and retrieve the information my scripts were working on. I've always felt it was a 'poor mans' solution that any good scripter would look to avoid as much as possible.

So as I've spent more time with powershell recently, I've been making an effort to move away from temporary files, and trying to use powershell's hash table and array functions to store and retrieve the scripted info I need.

I had a case, where I needed to create a report and the data I was gathering was seemingly going into my array the same way that it was being displayed in my console...but everytime I went to extract the data I had put into my arrays, the returned info in code was junk and my reports never built.

After much trial and error I found 2 ways around my problem, the first was modifying export-csv command within my script to update my data as it was extracted so it would display properly .. . the second was "cleaning" my data before i imported it into the array . .and that's what I want to discuss with this post.

But first, the problem that powershell attempts to help you with.

You have a text file, with the following data within it:

the quick brown fox
jumped over the slow
brown cow who ate grass
with ducks by the pond

This is multi-lined data, here's how powershell displays this info once it's been pulled into an array:



Where did those comma's come from?

Did the text data, have comma separators?  Hmm..Looks like no...


This would be an example of powershell "adapting" your content, because it realized as it was ingesting data that was 'multi-line' so it had to adjust it so it could work with it within the array framework.

Line separations are defined within the array as "commas" and the entire set of data is bracketed "{}".

And so the question becomes, once you know this limitation of powershell arrays exist, how do you plan around it?

For me, the "best" way around this is going back to what I know well . . LOL . . using text files as temporary staging areas to "clean" the data the I'm importing into my arrays so that it can be exported cleanly with export-csv and no special tricks.

"Data cleaning of the txt file" in this case, means removing empty lines, adding semi-colons to the end of each line, and merging all lines into a single line . . . the commands that help with those are:

Remove empty lines:

(GC $txtfile) | foreach {$_.trimend()} | where {$_ -ne ""} | sc $txtfile

Adding semi-colons:

(gc $txtfile) | foreach {$_ + ";"} | sc $txtfile

Turning all lines into one line for array import (2 liner):

$txtarrayimport=gc $txtfile
$DataToImport=$txtarrayimport -join ""

This may seem like an extreme amount of effort, but it's been extremely important lesson for me in terms of knowing that powershell can and will adjust your data on it's own. Taking the time to import data into powershell arrays in a format it can handle better can mean a big difference in exporting that data later.




Friday, November 16, 2018

Powershell - lessons learned when trying to document ADSI permissions...

I had a need to document permissions assigned from within ADSI to a configuration container with hundreds / thousands of objects beneath.

Powershell surprisingly treats active directory much like the file system with regards to it's object references when you're looking for access rights information.

What I couldn't find was a good reference for how to recursively report on permissions under Active Directory's configuration area (think MS Exchange).

Below is my first attempt at code to dump all CN / OU permissions. Note that AD pathing is important, and you'll want to edit the $config line to the path you want to report on.

****************************Begin Script******************************************

Import-module activedirectory
$Temp="c:\temp\test.txt"
$config=get-childitem -recurse -path "AD:CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=YourDomain,DC=com" | select -expandproperty DistinguishedName
Foreach ($object in $config)
{
$Path="AD:" + $object
$object >>$Temp
(get-acl $path).Access | Where {$_.IsInherited -eq $FALSE}|select InheritanceType,AccessControlType,IdentityReference,IsInherited >>$Temp
echo " ">>$Temp
}

****************************End Script ********************************************

This code, got the job done, it documented my permissions, but it created a fairly large Txt file which was a bit of a chore to sift through. It can be done better. . .

However, lets talk about what is going on with what we have first. . . If you're following along in the script, the data returned from the $config query is the full path to the AD object we want to query. But we can't just give that path to the "get-acl" command, it won't take it. Instead, we have to pre-pend the "AD:" to the query for get-acl, so a separate variable is used to turn our $config path into a value that get-acl can actually query and report on.

Also note, the data set is trimmed to only return directly assigned permissions. Any "inherited" permissions won't be shown as that makes for a massively sized report.

So I had results, but I wasn't happy with how it was presented, so I thought a bit about how I was getting at the data I wanted, and how it was being expressed . . and I realized something that I feel is very important to understand with powershell . . .

I am using a command in "get-acl" which returns multiple values. I point it at a folder / file / adobject, and it gives me a response that I can further filter to get only the data I want . . but I have to tinker with powershell a bit to do it...in my first code attempt no filtering was being done of the get-acl data, I was just displaying it as powershell would present it . . to a text file.

Here's another attempt at the same task, but filtering the get-acl query into specific values that a report is then built from:

**********************Begin Script****************************************

Import-module activedirectory
$Temp="c:\temp\test.txt"
$DataSet=get-childitem -recurse -path "AD:CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=YourDomain,DC=com" | select -expandproperty DistinguishedName
echo "CN Path;Group;Allow-Deny;Permissions;Inherited">$Temp
Write-host "Gathering permissions data..." -foregroundcolor Green
Foreach ($object in $DataSet)
{
$Path="AD:" + $object
$PermCheck=(get-acl $path).Access | Where {$_.IsInherited -eq $FALSE} | select ActiveDirectoryRights,AccessControlType,IdentityReference,IsInherited
Foreach ($Perm in $PermCheck)
{
$ADR=$perm.activedirectoryrights
$ACT=$perm.accesscontroltype
$IR=$perm.IdentityReference
$II=$perm.isinherited
echo "$object;$IR;$ACT;$ADR;$II">>$Temp
}
}
echo " "
Write-host "Data dump completed, finalizing report..." -foregroundcolor Green
$csv="c:\temp\MSAD-Perms-Config-Exchange.csv"
import-csv $Temp -delimiter ";" | export-csv $csv -NoTypeInformation
$ReportXLSX="c:\temp\MSAD-Perms-Config-Exchange.xlsx"
$vbscript="\\server\networkshare\CSV-Convert\csv_to_excel.vbs"
& $vbscript $csv $reportxlsx
timeout /t 5 /nobreak
echo " "
write-host "Report created" -foregroundcolor Green
echo " "

********************End Script*******************************************

Following along in the code here, I've changed the "get-acl" into a variable, then added a foreach section to make new variables out of each of the specific values I want to see queried from each permission. Then, only those values are "echo'd" to my waiting temp text file. The data sent to the txt file is separated with a ';' for importing with import-csv later in the script. I also referencing some code I've leveraged in the past on my blog in other postings, to convert a CSV file into a pre-formated XLSX file.

The result is a clear break-out of any and all permission assignments for an AD structure. The more I think about this, it feels like it wouldn't take much to convert this same code to reporting on file system permissions. I may test that out in another blog posting . . . ;)

Hope this helps others-

Thursday, September 20, 2018

Powershell - quickly stopping all skype services

For some reason, there's very little out there on a quick way to find and stop all skype services (this is now needed when running skype updates.)

Skype has it's own powershell commandlets for doing a variety of administration for skype systems, in this case we're going to leverage a single line command to get and stop all services:

*****script start*****

get-cswindowsservice | stop-cswindowsservice

*****script end*****

I have seen cases where this will simply hang out because the services themselves are locked up. In those cases, you either need to wait out the service stopping, or force a reboot and try again after your server is back online.

Tuesday, October 17, 2017

Forcing password resets for a group of users

Management is always good for coming up with requests that are . . .unique.

In today's case, my management was planning to bring extra staff in to "help" with a push to force all staff members to change their network passwords. The initial plan on this was that these 'extras' would help by personally clicking through user accounts and flagging them to 'change password on next login'.

"Why can't we just script it?" I asked.

"Well we can't just set everyone to change their password we need to be able to exclude specific staff, and only force specific accounts to be forced to change. ". I was told in response.

"No problem..." I say.

There are plenty of samples out there for using command line and powershell commands to flag all members of an OU, or all users in a domain to have their passwords set to change on next logon, but I was unable to find what I needed to allow us to be more selective.

What I needed was a way to set "change password on next logon" for select staff quickly and with reporting. I decided to base my process on a windows group. Here's the script result and what it does.

- Checks the members of a group in AD, dumps that member list to a text file.
- Runs through the resulting list of names and sets them to change password on next login. 

*****Begin Script*****

# Output Variables
$GM = "c:\scripts\groupmembers.txt"
$results = "c:\scripts\changerequests.txt"

# Add AD module for queries
import-module ActiveDirectory

# Dump group members
get-adgroupmember -identity ForcePwdChange | select Name | Sort Name >$GM

# Prep results file
echo " ">$results

# Clean up group members data dump
(gc $GM | select -Skip 3) | sc $GM
$Lines = (gc $GM)
$Lines | ForEach-Object { $_.TrimEnd(); } | Out-File $GM -Encoding Ascii
(gc $GM) | ? {$_.trim() -ne "" } | sc $GM

# Process the list of names
$List = (gc $GM)
Foreach ($U in $List)
{
get-aduser -filter 'Name -like $U' | set-aduser -changepasswordatlogon:$true
echo "$U set to change password on next login" >>$results
echo "$U PWD set to change on next Logon"
echo " "
}
read-host "press any key to exit"

*****End Script*****

I need to do a bit of clean up with adding some error catching to this, but I'm posting it now to get it out there. ;) 

I've worked before with parsing the user lists from AD groups, and the sub-section included here for 'clean up group members data dump' is essential, because otherwise the user names won't get clearly read by powershell for the other actions we need. 

This solution is entirely situational . .but that's what scripting is meant to help with, situational issues. Hopefully this helps someone else out there. 





Thursday, November 10, 2016

Powershell - print all files from a directory

This likely doesn't come up very often for folks these days, but someone out there may appreciate this script.

This script came from a business need to print off a few hundred text files from a directory on the network. With windows printers, you can drag files to a printer and "drop" them into it, to be able to print a lot of files at once, but there's still quite a bit of clicking involved.

This script when run, will prompt for selecting a directory from mapped drives on the machine, it will then print all contents of that directory to the default printer for the machine.

I could get fancier with this, but don't feel a need to as yet. Hopefully this may serve to help others as is / with some minor modifications.

**********Begin script***********

cls
echo " "
echo " "
write-host "Would you like to Print all the Txt files from a directory?" -foregroundcolor RED
echo " "
$Selection = read-host "Type y or n:"
echo " "
cls
If ($Selection -eq "y")
{
cls
Write-Host "Select where to pull the Txt files from..." -ForegroundColor yellow
function Read-FolderBrowserDialog([string]$Message, [string]$InitialDirectory)
{
$app = New-Object -ComObject Shell.Application
$folder = $app.BrowseForFolder(0, $Message, 0, $InitialDirectory)
if ($folder) { return $folder.Self.Path } else { return '' }
}
$directory = Read-FolderBrowserDialog ("Select the folder to print Txt files from","C:\")
$PrintFiles = Get-childItem $directory
ForEach ($PrintFile in $PrintFiles)
{
Start-Process -FilePath "$directory\$PrintFile" -Verb Print -PassThru |%{sleep 5;$_}|kill
echo " "
write-host "Printing $PrintFile..." -foregroundcolor Green
echo " "
}
echo " "
Write-host "All files from the selected directory have been Printed" -foregroundcolor Green
echo " "
read-host "Press enter to exit:"
}
ELSE
{
echo " "
write-host "You chose no, this script is exiting..."
echo " "
read-host "press enter to exit:"
}

**********End Script*******************

Tuesday, July 26, 2016

Powershell - Logon hours value comparisons

Time to post an update to my widely un-read blog. :)

I had a case where management was requesting a check on the LogonHours value for staff members, in particular they wanted to know specific users who had a specific logonhours setup, but they also wanted to track the staff who either had no logonhours config, or could logon at all hours.

A quick google will bring up many many hits about how to user powershell to "SET" your logonhours values, and there's a really nice script from Richard Mueller "http://www.rlmueller.net/PowerShell/PSAllUsersLogonHours.txt" that can be used to make a report of current settings.

But there was nothing I could personally find regarding "comparing" / or really "confirming" that logon hours are set as management desires. This value in AD is stored oddly, so many scripts devolve into trying to parse the data apart into more readable formats . .that's all well and good, but a waste of time for my purposes. So I decided I needed to find my own way on this . .

I will post my finished script shortly, but want to first clarify the steps I used:

$Default = get-aduser BaseUser -properties *
$DefaultCheck = $Default.logonhours
By breaking variables out in the above manner, we get a variable which is specifically a logonhours value for a user account we want to use as baseline for comparisons in the script. (Note that BaseUser should be the samaccount name you want to check against in AD.)
 Again, lots of folks want to break down this data into a readable format . .that's great, but I don't honestly care, I want to compare a 'correct' setting against other accounts, and build a report, to do that....:

$Compare1 = "$Userhours" -match "$DefaultCheck"
More variables, but this line is our "comparison" by using -match. The return is a "True" "False" which can be further coded against. (Note $Userhours in the above case is a variable similar to $DefaultCheck, it's the user logon values we've pulled from AD and want to compare to our baseline value).

So with the above clarified, the remaining logic for the script is:


  • Identify the AD accounts in AD to be used for LogonHour comparisons.
  • Decide how you want the data reported.
For my particular case, my management had the following requests for the report:
  • list the user, list the user's manager, list the logon hours setup.
  • a spreadsheet would be nice.
For my LogonHours comparisons, I had:
  • 1 user logonhour value that could be used as my Corporate baseline.
  • 1 user logonhour value that could be used as my baseline for AllHours access.
  • a catch routine for tracking users that had "NULL" as their logonhours value.
    • this means it was never defined in AD, and is effectively "AllHours access".
  • a catch group for tracking users that didn't match any of the above for their logonhours value.
For the Excel file report generation:
  • I tend to favor spreadsheets with autoformatting, in order to get such an output from my scripts I will often place a call to a VBS subroutine from: http://jeffkinzer.blogspot.com/2010/06/vbscript-to-convert-csv-to-xlsx.html
    • Jeff's very handy "csv_to_excel.vbs" can be easily called from within powershell to create autoformatted spreadsheets. 
The below script, will require a few edits to work in another environment, so pay attention to variable declarations. And as always, use at your own risk . .TEST TEST TEST.

Powershell script example:



Import-module activedirectory

#define variables for reports
$Report = "c:\Reports\Fulllist.txt"
$Nulls = "c:\Reports\Users-with-Null.txt"

#define logon hours results to query against
$Default = get-aduser UserNameVariable -properties *
$DefaultCheck = $Default.logonhours
$Allhours = get-aduser UserNameVariable -properties *
$AllhoursCheck = $Allhours.logonhours

#Pull user domain list
$Users = get-aduser -filter 'enabled -eq $true' -properties * -searchbase "LDAP domain search string variable for example DC=,DC=,DC=com"

#Prep reports with data column names
echo "Name ; LogonHours ; Manager" >>$Report

#Sort through results
ForEach ($User in $Users)
{
$Name = $user.name
$UserHours = $user.logonhours
$Manager = $user.Manager
#Check if logon hours is Null or not
If ($user.logonhours -eq $Null)
{
If ($Manager -eq $Null)
{
echo "$Name ; NotSet ; NotSet">>$Report
}
ELSE
{
$ManQuery = get-aduser $Manager
$ManName = $ManQuery.Name
echo "$Name ; NotSet ; $Manname">>$Report
}
}
ELSE
{
#Compare users logon hours with default.
$Compare1 = "$Userhours" -match "$DefaultCheck"
If ($Compare1 -eq $True)
{
If ($Manager -eq $Null)
{
echo "$Name ; Default ; NotSet">>$Report
}
ELSE
{
$ManQuery = get-aduser $Manager
$ManName = $ManQuery.Name
echo "$Name ; Default ; $Manname">>$Report
}
}
#Computer users logon hours with All hours authorization.
$Compare2 = "$Userhours" -match "$AllhoursCheck"
If ($Compare2 -eq $True)
{
If ($Manager -eq $Null)
{
echo "$Name ; AllHours ; NotSet">>$Report
}
ELSE
{
$ManQuery = get-aduser $Manager
$ManName = $ManQuery.Name
echo "$Name ; AllHours ; $Manname">>$Report
}
}
ELSEIF ($Compare1 -eq $False)
{
If ($Manager -eq $Null)
{
echo "$Name ; Variant ; NotSet">>$Report
}
ELSE
{
$ManQuery = get-aduser $Manager
$ManName = $ManQuery.Name
echo "$Name ; Variant ; $Manname">>$Report
}
}
}
}

write-host "report generation completed"
write-host "converting to XLSX"
$CSV = "c:\reports\Logonhours.csv"
$XLSX = "c:\reports\LogonHours.xlsx"

import-csv $Report -delimiter ";" | export-csv $csv -NoTypeInformation
$vbscript = "c:\csv-convert\csv_to_excel.vbs"
& $vbscript $Csv $xlsx
timeout /t 5 /nobreak
Remove-item $CSV -ea silentlycontinue
exit

Thursday, March 19, 2015

Powershell - Export Archive mailbox to PST - one at a time

Had a unique situation where we want to export all Mailbox archives from an older exchange server that has very limited resources.

Issuing a bulk mailbox export to PST was causing serious performance issues on the exchange server.

For these reasons I needed to only export mailbox archives one at a time.

The following script, connects to a single exchange server, checks for archives on all mailboxes, and starts to export them one at a time. This script will monitor the export progress till it reaches 100%, it will then clear the completed request, and move onto the next archive.

Make sure that you have permissions set as required by MS for your mailboxexport requests to work.

Also makes sure at the least to edit the "$ArchivePath" variable.

---------------------------------------Begin Script----------------------------------------------------------
#Load Exchange powershell commandlets
. 'C:\Program Files\Microsoft\Exchange Server\V14\bin\RemoteExchange.ps1'
Connect-ExchangeServer -auto

#Define PST UNC share for all exports

$ArchivePath="\\" #Specify a local or UNC path here.

#Process Archives
$Archives=get-mailbox -Archive | sort name
ForEach ($Archive in $Archives)
{
$ArchiveOut=$ArchivePath + "\" + $Archive + "-Archive" + ".pst"
cls
echo " "
echo " "
write-host "I found an Archive called $Archive" -foregroundcolor green
echo " "
write-host "Issuing PST Export command for $Archive" -foregroundcolor Cyan
echo " "
New-MailboxExportRequest -mailbox $Archive -Isarchive -FilePath $ArchiveOut
echo " "
$Loop="1"
While ($Loop -eq "1")
{
$ExportInfo=get-mailboxexportrequest | get-mailboxexportrequeststatistics
$ExportProgress=$ExportInfo.PercentComplete
$ExportProgress2=$ExportInfo.Status
IF ($ExportProgress -eq "100")
{
echo " "
write-host "The PST export for $Archive has completed" -foregroundcolor Green
echo " "
timeout /t 25 /nobreak
get-mailboxexportrequest | remove-mailboxexportrequest -confirm:$false
echo " "
write-host "Completed processing this Archive, moving onto next..."
echo " "
$Loop="2"
}
ELSE
{
echo " "
write-host "working on exporting archive..." -foregroundcolor Yellow
write-host $Archive
write-host $ExportProgress2
write-host $ExportProgress
echo " "
timeout /t 15 /nobreak
}
}
}
cls
echo " "
echo " "
write-host "All processing of mailbox archives has completed" -foregroundcolor Green
echo " "
echo " "
read-host "press any key to continue:"

---------------------------------------------End Script---------------------------------------------------------

Tuesday, March 10, 2015

Powershell Function - Check process

I wrote a quick powershell function for tracking if a process is running. I've found this useful for cases where I want my script to wait for a process to complete before continuing, or for providing 'visual' feedback that work is occurring.

In the sample below, the process I'm tracking with the function is "robocopy". (as declared with $Process=)

----------------------------------------------Begin Script-------------------------------------------------------------

$Process="Robocopy"
Function Check-Process-Alive
{
<#
.Synopsis
Checks if a process is running, and updates the screen with a new "line" for each second the process continues to run.
.Description
This particular function is helpful to see when a long running process stops.
.Example
PS C:\> Check-Process-Alive %processname%
#>
Param ($X)
While (Get-Process $X -ErrorAction SilentlyContinue)
{
Write-Host "Copying files..." -foregroundcolor Green
Start-Sleep -s 10
}
} # End function


Check-Process-Alive $Process

-------------------------------------------------End Script-----------------------------------------------------------

This code essentially allows you to call off a process in your script (an install or a file copy for instance) and your script will then 'pause' in essence while waiting for the process you launched to complete.


Powershell - search a folder tree and remove select objects.

Had an interesting issue the other day. We're migrating user data from a different domain, and all the user folders which were copied, showed up in windows explorer as "documents" instead of their real names.

This was an issue with the desktop.ini file under each of the folders, which was causing windows explorer to display the folders with a different name. The fix was a simple "delete" of the desktop.ini from under each user directory . . however getting powershell to help me out with this took me a little bit of tinkering.

Desktop.ini by default is a hidden system file. Powershell, has known bugs with it's "remove-item" command, and the common work around is "get-childitem". Most references I could find for having powershell recursively search a directory structure didn't find the files I wanted to delete, so I built the following:

----------------------------Begin Script----------------------------------------------------------------------------

$path="c:\RootFolderToBeSearched"
$folders=Get-ChildItem -path $path -force | Where-Object{$_.PsIsContainer} | ForEach-Object{$_.FullName}
foreach ($folder in $folders)
{
$BadItem=Get-ChildItem $folder -Filter desktop.ini -Force
If ($BadItem -ne $null)
{
$bad=$folder + "\" + $BadItem
remove-item $bad -force
}
ELSE
{
write-host "$folder is clean..." -foregroundcolor Green
}
}

-----------------------------End Script-----------------------------------------------------------------

In my case, i was looking specifically for the file desktop.ini, to search and remove other files, simply replace the file name. 

This script is specifically only looking 2 folders deep into a folder tree, adding a "-recurse" to either of the "get-childitem" lines, will return content from all subfolders. If you wish to play with this particular option, you'll want to edit the "remove" lines, as well since they won't work based on paths returned from 'recursive' searches of the folders. 

Thursday, October 30, 2014

PowerShell script for automating local user account password changes

It has been a while since I've posted an update here, so today I'm going to provide my Local User password change automation powershell script.

This is a rather advanced script, in that it's got a fair number of moving parts, and functions it supports.

I created it specifically with "flexibility" and "multiple options for using it" in mind. There should be little to no need to edit the script itself by anyone wishing to use it. The script has built in prompts for specific input so that it can complete it's work.

What this script does:

- Automates the changing of Local user account passwords on Windows based computers.
- Windows 2000 and higher systems are supported.
- Can process through a manually provided list of computers, dump lists of computers from Active directory, or can target specific stand alone systems.
- When processing through a list of computers, all actions successful, or not, are logged to paths you provide.
- You can specify the user account name you wish to update, (the script prompts for input here) won't force you to update the "administrator" account only.

Requirements for running:

- Administrative privileges are required for all systems you wish to attempt to change password data on.
- Support for the AD powershell module should exist already on the system you execute this script from. (if pulling a list of computers to process from AD).

 Considerations for using:

- This is an "active system" script. So when using it, it will only be able to perform updates against computers that are online (and reachable) at the time of it's use.
- All systems which can't be reached will be written out to a log.
- The Log created for "compsnotonline" can be used at another time to try updating missed systems.
- An AD domain OU with approx 400 workstations spread across a WAN took 30 - 45 minutes to run.
- Carefully read all prompts, and follow the provided directions within the script while using it.
- Note that at this time no logs are generated when choosing to target single machines.
- This script has been successfully run against a production domain on multiple occasions. But as with anything found on the internet. USE AT YOUR OWN RISK . . TEST TEST TEST.

------------------------------------------Begin Script------------------------------------------------
cls
echo " "
echo " "
write-host "This script will help you change passwords for computer based user accounts." -foregroundcolor Yellow
echo " "
write-host "You will be given several options for how to proceed.`nPlease read and respond to each question carefully." -foregroundcolor Yellow
echo " "
write-host "All script actions are logged.`nYou will be prompted for where the logs will be written to." -foregroundcolor Yellow
echo " "
write-host "You can EXIT this script at any time by using the 2 keys CTRL and C`nand then typeing Y to exit." -foregroundcolor RED
echo " "
Read-host "Press enter to continue:"
cls
echo " "
write-host "Would you like to update an account password on a list of computers?`nOr would you like to update an account password on a single machine?" -foregroundcolor "Green"
echo " "
$1stChoice = read-host "Type list or single:"
If ($1stChoice -eq "single")
{
$Loop = "1"
While ($Loop -eq "1")
{
cls
$SingleComp = read-host "Type the computer name to be updated:"
$Account = read-host "Type the account name to be updated:"
$password = read-host "Type the new password:"
cls
([adsi]"WinNT://$SingleComp/$Account").SetPassword("$password")
If ($? -eq "True")
{
echo " "
write-host "Password change successful" -foregroundcolor Green
echo " "
}
ELSE
{
echo " "
write-host "Password change failed" -foregroundcolor Red
echo " "
}
echo " "
echo " "
write-host "Would you like to process another system?" -foregroundcolor Yellow
$LoopBack = read-host "Type Y or N:"
If ($Loopback -eq "y")
{
$Loop = "1"
}
ELSE
{
$Loop = "2"
}
}
echo " "
cls
write-host "You have selected to not update anymore passwords." -foregroundcolor Yellow
write-host "Have a good day" -foregroundcolor Yellow
read-host "Press enter to exit:"
}
ELSE
{
cls
echo " "
write-host "You have chosen to perform updates on a list of computers." -foregroundcolor Yellow
echo " "
write-host "Would you like to import a text file list of computers?`nOr import a list of systems from Active Directory?" -foregroundcolor Yellow
echo " "
$2ndChoice = read-host "Type AD or Txt:"
IF ($2ndChoice -eq "AD")
{
cls
write-host "Please provide the location for all script output.`nWith this script always define a path with a subfolder for your root path.`nFor example 'c:\scriptdata'." -foregroundcolor Red
echo " "
write-host "The folders you specify 'do not have to exist in advance'.`nThey will be created automatically by the script." -foregroundcolor red
echo " "
write-host "Failure to follow these directions `nwill cause all script output to fail to be written." -foregroundcolor red
echo " "
$RootDrv = read-host "Type the desired base drive letter to be used, 'example C, or D, or G', 'DO NOT INCLUDE A ':' :"
$RD = $RootDrv + ':' + '\'
$OutputPath = read-host "Type a folder name that will be created at the root of your drive letter:"
$Output = $RD + $OutputPath
$CompsNotOnline="$Output\CompsNotOnline.txt"
$PasswordChangeFailed="$Output\PasswordChangeFailed.txt"
$Success="$Output\PasswordChanged.txt"
$TXT="$Output\ADComps.txt"
If (!(Test-Path -Path $Output ))
{
new-item -path $RD -name $OutputPath -type directory -force
}
ELSE
{
Remove-item $TXT -erroraction silentlycontinue
Remove-item $CompsNotOnline -erroraction silentlycontinue
Remove-item $PasswordChangeFailed -erroraction silentlycontinue
Remove-item $Success -erroraction silentlycontinue
}
import-module ActiveDirectory
cls
write-host "Please type the fully qualified LDAP path you'd like to process.`nAll OU's under the path specified will be processed" -foregroundcolor Yellow
write-host "For example, if your domain is named Bob.fred.com,`nand you want to process computers from an OU called Computers" -foregroundcolor Yellow
write-host "You would type = OU=Computers,DC=Bob,DC=fred,DC=com" -foregroundcolor Yellow
echo " "
$Ldap = read-host "Ldap Path:"
get-adcomputer -filter * -properties * -SearchBase $Ldap| sort Name | Select-Object Name >>$TXT
(gc $TXT | select -Skip 3) | sc $TXT
$Lines = (gc $TXT)
$Lines | ForEach-Object { $_.TrimEnd(); } | Out-File $TXT -Encoding Ascii
echo " "
Write-host "Gathering list of computers..."
echo " "
Timeout /t 10 /nobreak
cls
$Computers = (gc $TXT)
$UserName = read-host "Type the user name to update:"
$password = read-host "Type the new password:"
cls
echo " "
Write-host "Script is ready to begin processing the computer list`nto update the password for the user $Username..." -foregroundcolor "Green"
Read-host "Press Enter to Begin:"
cls
ForEach ($Computer in $Computers)
{
get-wmiobject -computer $Computer Win32_group -erroraction silentlycontinue | out-null
If ($? -eq "True")
{
echo " "
write-host "$Computer is online"
([adsi]"WinNT://$Computer/$UserName").SetPassword("$password")
If ($? -eq "True")
{
echo " "
write-host "Password changed for $Username on $Computer" -foregroundcolor "Green"
echo "Password changed for $Username on $Computer" >>$Success
}
ELSE
{
echo " "
write-host "Password change for $Username on $Computer failed" -foregroundcolor "Red"
echo "$Computer" >>$PasswordChangeFailed
}
}
ELSE
{
echo " "
write-host "There was a problem contacting $Computer" -foregroundcolor RED
echo " "
write-host "Adding machine name to CompsNotOnline.txt under $Output" -foregroundcolor RED
echo "$Computer" >>$CompsNotOnline
}
}
cls
echo " "
Write-host "The selected systems with the user account $UserName`nhave been updated with the new password $password. Please update your records." -foregroundcolor Cyan
read-host "Press enter to exit:"
}
ELSE
{
cls
write-host "Please provide the location for all script output.`nWith this script always define a path with a subfolder for your root path.`nFor example 'c:\scriptdata'." -foregroundcolor Red
echo " "
write-host "The folders you specify 'do not have to exist in advance'.`nThey will be created automatically by the script." -foregroundcolor red
echo " "
write-host "Failure to follow these directions `nwill cause all script output to fail to be written." -foregroundcolor red
echo " "
$RootDrv = read-host "Type the desired base drive letter to be used, 'example C, or D, or G', 'DO NOT INCLUDE A ':' :"
$RD = $RootDrv + ':' + '\'
$OutputPath = read-host "Type a folder name that will be created at the root of your drive letter:"
$Output = $RD + $OutputPath
$CompsNotOnline="$Output\CompsNotOnline.txt"
$PasswordChangeFailed="$Output\PasswordChangeFailed.txt"
$Success="$Output\PasswordChanged.txt"
$TXT="$Output\ADComps.txt"
If (!(Test-Path -Path $Output ))
{
new-item -path $RD -name $OutputPath -type directory -force
}
ELSE
{
Remove-item $TXT -erroraction silentlycontinue
Remove-item $CompsNotOnline -erroraction silentlycontinue
Remove-item $PasswordChangeFailed -erroraction silentlycontinue
Remove-item $Success -erroraction silentlycontinue
}
echo " "
Write-Host "Select TXT file of systems to update..." -ForegroundColor yellow
function Read-OpenFileDialog([string]$WindowTitle, [string]$InitialDirectory, [string]$Filter = "All files (*.*)|*.*", [switch]$AllowMultiSelect)
{
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.Title = $WindowTitle
if ($InitialDirectory -eq $Null) { $openFileDialog.InitialDirectory = $InitialDirectory }
$openFileDialog.Filter = $Filter
if ($AllowMultiSelect) { $openFileDialog.MultiSelect = $true }
$openFileDialog.ShowHelp = $true    # Without this line the ShowDialog() function may hang depending on system configuration and running from console vs. ISE.
$openFileDialog.ShowDialog() > $null
if ($AllowMultiSelect) { return $openFileDialog.Filenames } else { return $openFileDialog.Filename }
}
$var = Read-OpenFileDialog("Select list of systems to process...:","C:\")
$Lines = (gc $var)
$Lines | ForEach-Object { $_.TrimEnd(); } | Out-File $var -Encoding Ascii
$Computers = (gc $var)
cls
echo " "
$UserName = read-host "Type the user name to update:"
echo " "
$password = read-host "Type the new password:"
cls
Write-host "Script is ready to begin processing the computer list`nto update the password for the user $Username..." -foregroundcolor "Green"
Read-host "Press Enter to Begin:"
cls
ForEach ($Computer in $Computers)
{
get-wmiobject -computer $Computer Win32_group -erroraction silentlycontinue | out-null
If ($? -eq "True")
{
echo " "
write-host "$Computer is online"
([adsi]"WinNT://$Computer/$UserName").SetPassword("$password")
If ($? -eq "True")
{
echo " "
write-host "Password changed for $Username on $Computer" -foregroundcolor "Green"
echo "Password changed for $Username on $Computer" >>$Success
}
ELSE
{
echo " "
write-host "Password change for $Username on $Computer failed" -foregroundcolor "Red"
echo "$Computer" >>$PasswordChangeFailed
}
}
ELSE
{
echo " "
write-host "There was a problem contacting $Computer" -foregroundcolor RED
echo " "
write-host "Adding machine name to CompsNotOnline.txt under $Output" -foregroundcolor RED
echo "$Computer" >>$CompsNotOnline
}
}
cls
echo " "
Write-host "The selected systems with the user account $UserName`nhave been updated with the new password $password. Please update your records." -foregroundcolor Cyan
read-host "Press enter to exit:"
}

}

--------------------------------End Script--------------------------------------------------




Tuesday, July 22, 2014

Cool powershell tricks

Objective:

  • From within script, launch a specific EXE with a number of / switches in the launch command.

Statement:

  • Setting your full command with all switches as a variable and calling it, doesn’t work as you may expect in powershell, instead the way to go is to create multiple variables and then invoke them all at once using “&”.

Example:

  • Required command =
    • "C:\Program Files (x86)\Microsoft Office\Office14\Outlook.exe  /importprf C:\progra~2\micros~2\custom14.prf"
  • To invoke this EXE command string in powershell (define 3 variables ‘$’, then call them all with ‘&’):
    • $Outlook = "C:\Program Files (x86)\Microsoft Office\Office14\Outlook.exe"
    • $OutlookArg1 = "/importprf"
    • $OutlookArg2 = "C:\progra~2\micros~2\custom14.prf"
    • & $Outlook $OutlookArg1 $OutlookArg2

----------------------------------------------------------------------------------------------------------------------------------

Objective:

  • You’re displaying text to the console via ‘write-host’ and you want to control where the word wrap breaks occur

Statement:

  • There are a number of posts discussing word wrapping options for the output from powershell, but to really have the control over the write-host text wrap, you need to leverage “`n”
  • ` is called a “backtick” in powershell, and using it with the ‘n’ character tells powershell to start a new line.
  • This can be called mid-string…

Example:


  • Write-host “This is a sample bit of code to show how`nformating can work”


-----------------------------------------------------------------------------------------------------------------------------
Objective:

  • You need to work with user directories . . . logged on users defined by an obscure value and %username% doesn’t seem to work.

Statement:

  • There are a number of tricks to pull this info, the one I’ve found most effective currently is $env:username
    • This pulls the username of the user currently running the script.

Example:

  • Define as a variable a subdirectory under their local user profile on the server.
  • $Dir = “C:\users\” + $env:username + “\appdata\local\microsoft\outlook”