Simple Audit Using Powershell
Windows Powershell allows you to automate time consuming tasks. Read this article to learn how to make it work for you.-
Introduction – Powershell Basics
This series of articles will introduce some simple and easy to use concepts in Windows Powershell which will allow you to automate time consuming or repetitive tasks quickly and easily. Let’s say for example that you are planning to roll out a new operating system and you need to quickly find out where you can get away with the existing computers and where you will need to refresh the hardware. To achieve this you will need to find out the processor, RAM, and disk configuration of multiple machines.
The examples will show how to use Powershell to get information remotely from computers and build up to create a functional Powershell script that will do a mini audit of a list of computers that are contained in a text file. We will also see how to populate that text file from a search of Active Directory and export the resulting data to a spreadsheet. In this case you will then be able to see where you need to invest in new hardware to run the new operating system. This first article in the series covers the basics of Powershell.
Powershell is a command line environment and scripting language that can be used in two ways, by typing commands directly into the command line or by running a script. Time for a quick example, If you have experience running scripts you may want to jump to the next article.
Powershell uses commands known as command-lets (cmdlet's) that follow a verb-noun syntax e.g. Get-Service or New-Item. At the Powershell command line type the folowing: Get-Service
You will be presented with a list of services running on your local computer and some extra information about the status and Display Name :
Windows PowerShell
Copyright (C) 2006 Microsoft Corporation. All rights reserved.
PS C:\Users\willr> Get-Service
Status Name DisplayName
------ ---- -----------
Running AeLookupSvc Application Experience
Stopped ALG Application Layer Gateway Service
Running AppHostSvc Application Host Helper Service
Stopped Appinfo Application Information
Running Apple Mobile De... Apple Mobile Device
Stopped AppMgmt Application Management
Stopped aspnet_state ASP.NET State Service
Running AudioEndpointBu... Windows Audio Endpoint Builder
Running AudioSrv Windows Audio
Running BFE Base Filtering Engine
Running BITS Background Intelligent Transfer Ser...
This information can be very useful to an administrator and Powershell is full of cmdlet’s that can be used in this way to get information about the state and configuration of a windows machine. To see a default list of cmdlet's available run Get-Command at the Powershell command prompt.
The other way of using Powershell is to create a script that can contain a combination of these cmdlet’s and other commands to provide more complex queries and tasks e.g. getting multiple pieces of information and storing them in a text file. To run a Powershell script the commands should be saved into a text file with a .ps1 extension and can be executed from the command line by typing .\myscript.ps1
-
Use notepad to save the text write-host “Powershell Script” into a file called myscript.ps1 into a folder of your choice. I normally use c:\scripts or my Desktop folder.
-
Run Powershell and navigate to your chosen folder by using normal DOS commands like cd c:\scripts
-
Type .\myscript.ps1
(Notice the .\ at the start, without it the script wont run)
PS C:\Users\willr\Desktop> .\myscript.ps1
Powershell Script
PS C:\Users\willr\Desktop>
What you should have seen is the words “Powershell Script” printed to the screen as above. If you get an error close Powershell and run it as administrator and type set-executionpolicy unrestricted and rerun the script. The reason for this is that Powershell has security built in so that scripts are not run accidentally or maliciously by any user. In production environments scripts should be signed with a certificate so it is difficult for unauthorised people to create and run scripts.
The next thing to understand about Powershell is that things are objects not just text strings like when working with batch files. This gives you lots of extra power and saves time not having to parse text. For example take the previous Get-Service cmdlet, what you see on screen is only a subset of the information contained in that response. Powershell stores variables using the dollar sign ($) at the start (unlike VBScript). Next example we will create a variable and fill it with the response from Get-Service. Try the following:
-
Type $services = Get-Service at the Powershell command line
-
Next Type $services
What you get is the same information on screen as if you just typed Get-Service but you now have a collection of objects which represent the services on your computer stored in a variable called $services.
A quick note on Objects may be useful here. An Object is a bucket of stuff that has properties (bits of data) and methods (bits of code to do something with the data) all bundled together. An object is based on a Class which defines which properties and methods the object has. So for example here we are using an object based on the class System.ServiceProcess which gives it among other things a property called Status which we will check later and a method called stop() which will stop the service if called.
This is very useful as you can now get specific info on a particular service by checking the properties, lets check the eventlog service:
-
Type $elservice = $services | where-object { $_.Name -eq “eventlog”}
-
Type $elservice
PS C:\Users\willr\Desktop> $services = get-service
PS C:\Users\willr\Desktop> $elservice = $services | where-object { $_.Name -eq "eventlog"}
PS C:\Users\willr\Desktop> $elservice
Status Name DisplayName
------ ---- -----------
Running Eventlog Windows Event Log
Now that looks a bit complicated but literally means make $elservice the same as $services but only where the services Name property equals “evenlog” (the current object is represented by $_ so $_.Name gives the Name property). This gives us $elservice that contains the details of only the eventlog service. Now lets see whats contained in this object, by default as in the previous examples when you print the details to the screen a formatted list of a subset of the info is displayed, if you want to see all the data contained in the object you can use the format-list cmdlet and the pipe symbol (|) This symbol ‘pipes’ the result of the command to the next command and is used extensively in Powershell.
-
Type $elservice | Format-List * (this can be abbreviated to $elservice | fl *)
PS C:\Users\willr\Desktop> $elservice | Format-List *
Name : Eventlog
CanPauseAndContinue : False
CanShutdown : True
CanStop : True
DisplayName : Windows Event Log
DependentServices : {Wecsvc, Schedule}
MachineName : .
ServiceName : Eventlog
ServicesDependedOn : {}
ServiceHandle :
Status : Running
ServiceType : Win32ShareProcess
Site :
Container :
As you can see there is a lot more information available. To acces that info you can use the Property name preceded by a dot (.) e.g. to see the status of the eventlog service in $elservice you could Type the following:
-
Type $elservice.Status
PS C:\Users\willr\Desktop> $elservice.status
Running
PS C:\Users\willr\Desktop>
This returns the value of the property you requested in this case ‘Running’.
In this Article we have introduced the subject for the series, explored the basic running of Powershell, run cmdlet’s, run scripts, assigned variables and interrogated the objects contained in those variables. The next article will cover a general process for writing scripts and explore searching the Active Directory in order to get a list of computer that we can audit.
This is the second article in this series of five looking at using Powershell to solve a problem by doing an audit of the basic hardware specification of computers in your Active Directory. This will cover a basic script writing methodology, searching the Active Directory and saving results to text files. I will start with a simple approach to script writing which works for me. First thing I do is break down what it is I want to do and make a list. In this case I want a script or scripts that will do a mini audit of a list of computers, to do this I will have to do the following things. (Ignore the green text for now):
-
Get a list of computers from Active Directory – Use [ADSI] to search
-
Write that list to a text file – Either export-csv or set-content
-
Have an opportunity to edit the text file – Excel or notepad
-
Read the text file into memory – Import-csv or get-content
-
Loop through each computer in the list – foreach
-
Check if they are on the network, if not make a note. – Use WMI win32_pingstatus
-
Gather information from the ones on the network
-
Manufacturer and model – Use WMI win32_computersystem
-
Amount of RAM - Use WMI win32_computersystem
-
Number of processors - Use WMI win32_computersystem
-
Serial Number, bios and chipset information - Use WMI win32_bios
-
C: and D: drive size, free space, File system. – Use WMI win32_logicaldisk
-
This list could go on and on but I’ll leave it there for this example.
-
-
Note down any failures – Check if results have values or are null
-
Put the results into a list – create a custom object that contains all values
-
Put the failures into another list - create a custom object that contains all values
-
-
Save the lists to a text file that I can read in Excel. – Use Export-csv
I start with the top level bullet points then go down until I’ve dumped as much information as I can think of then I go down the list again and try to think how I will achieve each part (marked in green text in the list above). If I don’t already know roughly how to achieve it I use my favourite search engine to find examples of someone doing similar. This then allows me to chop my scripts up into smaller manageable parts which seem easier to think about.
So let’s start with the first one on the list, getting a list of computers from Active Directory. With Version 1.0 of Powershell there are no built-in cmdlets that work on Active Directory directly so this example is not as easy to understand as others but it is still mostly human readable. What we are going to do is create an object that is based on a class that has a directory searcher (method) built into it, set some properties of that search and then execute the search using the FindAll() method. The AD search script looks like this:
$Filter = "(objectCategory=Computer)"
$domain = [ADSI]""
$ADSearcher = New-Object System.DirectoryServices.DirectorySearcher
$ADSearcher.SearchRoot = $domain
$ADSearcher.PageSize = 1000
$ADSearcher.Filter = $Filter
$ADSearcher.PropertiesToLoad.Add("name")
$ADResults = $ADSearcher.FindAll()
-
We start off with setting a a filter for the search: $Filter = "(objectCategory=Computer)" This makes the search only look for AD objects that have the category Computer. This could also be another object category if we wanted to search for users or contacts etc.
-
Next we create an [ADSI] object called $domain. This gets the domain information and keeps it in an object ready to use.
-
Next we create an object called $ADsearcher that is based on a .NET class which already has the code (method) to do a directory search for us.
-
We then tell this searcher object that the root of the search should be the whole domain, if we only wanted to search an OU we would change this root to be the Ldap path of the OU instead of the domain.
-
This limits the return results to 1000.
-
This sets the filter to search on to $filter which as stated above is set to computer objects.
-
This tells the searcher object what properties on the objects that are found to retrieve, in this case we just want the name. Notice this is also a method called Add()
-
This is the actual Directory search. This sets a variable called $ADResults to contain a collection of computer objects.
So now we have a list of computer objects in $ADResults we now want to dump the names into a text file. The following script will create the file containing the list of servers.
$computerlist = @("Computername")
foreach ($Result in $ADResults) {$computerlist += ($Result.Properties).name}
new-item computerlist.csv -itemtype file
add-content computerlist.csv -value $computerlist
-
Here we are creating an array with Computername as the first entry in the list, this will help later when we want to reimport the file for the audit.
-
This statement loops through the list of computer objects in $ADResults and adds the name of the computer to the array $computerlist. Notice how we get the actual string of the name (not the whole object) by stating ($result.properties).name There will be more info on creating foreach loops and working with objects in a later article in this series.
-
This creates a file in the same directory as the script is running called computerlist.csv
-
Add-content dumps the list in $computerlist to the file computerlist.csv
We now have a text file called computerlist.csv that should contain a list of all the computer objects in your Active Directory. Open it in Excel and see if it contains what you expected.
In this Second Article we covered a basic methodology to script writing and looked at searching the Active Directory for objects of a specified category, we then saved that list of objects to a comma separated variable (CSV) file and opened the results in Excel. The Next article will look at Windows Management Instrumentation (WMI) and how to use Powershell to create WMI queries that give us all the information we need about the computers we have just found in the Active Directory.
-
Importing CSV’s and Using WMI
This is the third article in this series of five looking at using Powershell to solve a problem by doing an audit of the basic hardware specification of computers in your Active Directory. This will cover using CSV files to provide input data to a script and using Windows Management Instramentation (WMI) queries to get useful information from computers locally and remotely on the network.
Lets start with the csv file created in the previous article computerlist.csv. When opened in Excel it should look similar to this with a heading of Computername followed by a list:
|
|
A |
|
1 |
ComputerName |
|
2 |
TRU-ISA-01 |
|
3 |
TRU-LAP-02 |
|
4 |
TRU-MSG-01 |
|
5 |
TRU-SRV-001 |
|
6 |
TRU-SRV-002 |
|
7 |
TRU-VMM-01 |
|
8 |
TRU-VMM-02 |
|
9 |
TRU-WILLR-01 |
|
10 |
TRU-WILLR-02 |
|
11 |
TRU-WILLR-03 |
|
12 |
TRU-WIN7-01 |
|
13 |
TRU-XP-01 |
We want to import this into a variable so we can use it to do stuff. The Import-Csv cmdlet is very useful for this purpose. The following examples import the csv to a variable called $list then displays the variable on the screen.
-
At the Powershell commandline navigate to the folder that contains computerlist.csv and type:
-
$list = import-csv –path computerlist.csv
-
Write-host $list
-
You’ll see the same list as was in the spreadsheet
PS C:\Users\willr\Desktop> $list = Import-Csv -path computerlist.csv
PS C:\Users\willr\Desktop> write-host $list
Computername
------------
TRU-ISA-01
TRU-LAP-02
TRU-MSG-01
TRU-SRV-001
TRU-SRV-002
TRU-VMM-01
TRU-VMM-02
TRU-WILLR-01
TRU-WILLR-02
TRU-WILLR-03
TRU-WIN7-01
TRU-XP-01
We’ll look at using that data in the next article for the moment lets look at WMI. Wikipedia says “Windows Management Instrumentation (WMI) is a set of extensions to the Windows Driver Model that provides an operating system interface through which instrumented components provide information and notification. WMI is Microsoft's implementation of the Web-Based Enterprise Management (WBEM) and Common Information Model (CIM) standards from the Distributed Management Task Force (DMTF).”
What this means for us is that there is a fairly easy way to get at a load of configuration and state information using WMI queries, note that you can also set configuration using WMI but that is outside the scope of this article.
In Powershell there is a cmdlet called Get-WMIObject which allows us to grab information from the system. To get the information you have to know where it is stored, in WMI terms which namespace and class. The namespace is easy as we will only use the default one called CIMV2 which as it defaults to that we dont need to specify it. What we do need to find out is what class the information we want is stored in. This can be painful at first but you soon get to know the standard ones. They are generally called win32_
On to some examples, earlier we decided that we needed information for our audit about manufacturer and model, this info happens to be stored in the win32_computersystem class. At the Powershell commandline type Get-WMIObject –class win32_computersystem and you will get the following:
PS C:\Users\willr\Desktop> Get-WmiObject win32_computersystem
Domain : TruConsulting.local
Manufacturer : System manufacturer
Model : System Product Name
Name : TRU-WILLR-01
PrimaryOwnerName : will
TotalPhysicalMemory : 4292476928
This shows a subset of the information, to get the whole list type:
Get-WMIObject –class win32_computersystem | Format-List *
This gives the information from the local computer, to query another computer add the computername argument to the Get-WMIObject cmdlet:
Get-WMIObject –class win32_computersystem –computername
To set the info to a variable:
$compsys = Get-WMIObject –class win32_computersystem
To read values from the variable
Write-Host $Compsys.Manufacturer
Write-Host $compsys.Model
To get the list of properties that you can read you can either see the full list
Write-Host $compsys | Format-List *
Or you could use the Get-Member cmdlet to show what properties the object can contain.
$compsys | Get-Member
PS C:\Users\willr\Desktop> $compsys = Get-WmiObject win32_computersystem
PS C:\Users\willr\Desktop> $compsys | Get-Member
TypeName: System.Management.ManagementObject#root\cimv2\Win32_ComputerSystem
Name MemberType Definition
---- ---------- ----------
JoinDomainOrWorkgroup Method System.Management.ManagementBaseObject
Rename Method System.Management.ManagementBaseObject
SetPowerState Method System.Management.ManagementBaseObject
UnjoinDomainOrWorkgroup Method System.Management.ManagementBaseObject
AdminPasswordStatus Property System.UInt16 AdminPasswordStatus {get;set;}
AutomaticManagedPagefile Property System.Boolean AutomaticManagedPagefile {get;set;}
AutomaticResetBootOption Property System.Boolean AutomaticResetBootOption {get;set;}
AutomaticResetCapability Property System.Boolean AutomaticResetCapability {get;set;}
BootOptionOnLimit Property System.UInt16 BootOptionOnLimit {get;set;}
BootOptionOnWatchDog Property System.UInt16 BootOptionOnWatchDog {get;set;}
BootROMSupported Property System.Boolean BootROMSupported {get;set;}
BootupState Property System.String BootupState {get;set;}
Caption Property System.String Caption {get;set;}
ChassisBootupState Property System.UInt16 ChassisBootupState {get;set;}
CreationClassName Property System.String CreationClassName {get;set;}
CurrentTimeZone Property System.Int16 CurrentTimeZone {get;set;}
DaylightInEffect Property System.Boolean DaylightInEffect {get;set;}
Description Property System.String Description {get;set;}
DNSHostName Property System.String DNSHostName {get;set;}
Domain Property System.String Domain {get;set;}
DomainRole Property System.UInt16 DomainRole {get;set;}
EnableDaylightSavingsTime Property System.Boolean EnableDaylightSavingsTime {get;set;}
FrontPanelResetStatus Property System.UInt16 FrontPanelResetStatus {get;set;}
InfraredSupported Property System.Boolean InfraredSupported {get;set;}
InitialLoadInfo Property System.String[] InitialLoadInfo {get;set;}
InstallDate Property System.String InstallDate {get;set;}
KeyboardPasswordStatus Property System.UInt16 KeyboardPasswordStatus {get;set;}
LastLoadInfo Property System.String LastLoadInfo {get;set;}
Manufacturer Property System.String Manufacturer {get;set;}
Model Property System.String Model {get;set;}
Name Property System.String Name {get;set;}
NameFormat Property System.String NameFormat {get;set;}
NetworkServerModeEnabled Property System.Boolean NetworkServerModeEnabled {get;set;}
NumberOfLogicalProcessors Property System.UInt32 NumberOfLogicalProcessors {get;set;}
NumberOfProcessors Property System.UInt32 NumberOfProcessors {get;set;}
OEMLogoBitmap Property System.Byte[] OEMLogoBitmap {get;set;}
OEMStringArray Property System.String[] OEMStringArray {get;set;}
PartOfDomain Property System.Boolean PartOfDomain {get;set;}
PauseAfterReset Property System.Int64 PauseAfterReset {get;set;}
PCSystemType Property System.UInt16 PCSystemType {get;set;}
PowerManagementCapabilities Property System.UInt16[] PowerManagementCapabilities
PowerManagementSupported Property System.Boolean PowerManagementSupported {get;set;}
PowerOnPasswordStatus Property System.UInt16 PowerOnPasswordStatus {get;set;}
PowerState Property System.UInt16 PowerState {get;set;}
PowerSupplyState Property System.UInt16 PowerSupplyState {get;set;}
PrimaryOwnerContact Property System.String PrimaryOwnerContact {get;set;}
PrimaryOwnerName Property System.String PrimaryOwnerName {get;set;}
ResetCapability Property System.UInt16 ResetCapability {get;set;}
ResetCount Property System.Int16 ResetCount {get;set;}
ResetLimit Property System.Int16 ResetLimit {get;set;}
Roles Property System.String[] Roles {get;set;}
Status Property System.String Status {get;set;}
SupportContactDescription Property System.String[] SupportContactDescription {get;set;}
SystemStartupDelay Property System.UInt16 SystemStartupDelay {get;set;}
SystemStartupOptions Property System.String[] SystemStartupOptions {get;set;}
SystemStartupSetting Property System.Byte SystemStartupSetting {get;set;}
SystemType Property System.String SystemType {get;set;}
ThermalState Property System.UInt16 ThermalState {get;set;}
TotalPhysicalMemory Property System.UInt64 TotalPhysicalMemory {get;set;}
UserName Property System.String UserName {get;set;}
WakeUpType Property System.UInt16 WakeUpType {get;set;}
Workgroup Property System.String Workgroup {get;set;}
__CLASS Property System.String __CLASS {get;set;}
__DERIVATION Property System.String[] __DERIVATION {get;set;}
__DYNASTY Property System.String __DYNASTY {get;set;}
__GENUS Property System.Int32 __GENUS {get;set;}
__NAMESPACE Property System.String __NAMESPACE {get;set;}
__PATH Property System.String __PATH {get;set;}
__PROPERTY_COUNT Property System.Int32 __PROPERTY_COUNT {get;set;}
__RELPATH Property System.String __RELPATH {get;set;}
__SERVER Property System.String __SERVER {get;set;}
__SUPERCLASS Property System.String __SUPERCLASS {get;set;}
POWER PropertySet POWER {Name, PowerManagementCapabilities,
PSStatus PropertySet PSStatus {AdminPasswordStatus, BootupState,
ConvertFromDateTime ScriptMethod System.Object ConvertFromDateTime();
ConvertToDateTime ScriptMethod System.Object ConvertToDateTime();
Delete ScriptMethod System.Object Delete();
GetType ScriptMethod System.Object GetType();
Put ScriptMethod System.Object Put();
This gives us a list of all the properties and methods available in the win32_computersystem WMI class. Remember that this can also be used for any WMI class so you can use this to trawl through the wealth of info available and find the stuff you want. WMI classes we will use in this script are:
-
Win32_Computersystem – System Manufacturer, Model, memory, system type and Processors
-
Win32_Systemenclosure – shows the type of machine, tower,desktop etc.
-
Win32_logicaldisk – Information about disks
-
Win32_bios – serial number, motherboard
-
Win32_Pingstatus – to check computer is responding on network
This last one is especially useful to cut down the time the script will run. It is much quicker to check the target machine responds to a ping and not bother if it fails than wait for a WMI query to fail against a remote computer and time out. This is a different way of using the Get-WMIObject cmdlet using the –Query Argument. This will return an object that has a property called StatusCode. If this value is Zero then the ping was successful, anything else and is was not.
To use this class to ping type the following:
get-wmiobject -Query "select * from win32_pingstatus where Address='
Windows PowerShell
Copyright (C) 2006 Microsoft Corporation. All rights reserved.
PS C:\Windows\System32> get-wmiobject -Query "select * from win32_pingstatus where Address='tru-srv-002'"
__GENUS : 2
__CLASS : Win32_PingStatus
__SUPERCLASS :
__DYNASTY : Win32_PingStatus
__RELPATH : Win32_PingStatus.Address="tru-srv-002",BufferSize=32...
__PROPERTY_COUNT : 24
__DERIVATION : {}
__SERVER : TRU-WILLR-01
__NAMESPACE : root\cimv2
__PATH : \\TRU-WILLR-01\root\cimv2:Win32_PingStatus.Address="...
Address : tru-srv-002
BufferSize : 32
NoFragmentation : False
PrimaryAddressResolutionStatus : 0
ProtocolAddress : 10.10.0.11
ProtocolAddressResolved :
RecordRoute : 0
ReplyInconsistency : False
ReplySize : 32
ResolveAddressNames : False
ResponseTime : 0
ResponseTimeToLive : 128
RouteRecord :
RouteRecordResolved :
SourceRoute :
SourceRouteType : 0
StatusCode : 0
Timeout : 4000
TimeStampRecord :
TimeStampRecordAddress :
TimeStampRecordAddressResolved :
TimestampRoute : 0
TimeToLive : 128
TypeofService : 0
Notice the StatusCode is indeed 0 in this case. Again if you wanted to use this in a script you would set that statement to a variable:
$ping = get-wmiobject -Query "select * from win32_pingstatus where Address='
To interrogate the value of the statuscode property you would use a conditional statement like If.
If ($ping.StatusCode –eq 0) {do something]
Else { do something else}
More on using conditional statements and loops in the next article. In this article we have looked at using Import-Csv to set a variable to contain the contents of a CSV file, we have looked at using Get-WMIObject to retrieve information using various WMI classes and we have used WMI to quickly ping another computer.
-
Using Loops and Objects
This is the fourth article in this series of five looking at using Powershell to solve a problem by doing an audit of the basic hardware specification of computers in your Active Directory. Previously we have concentrated on using Powershell cmdlets to get information and ways of importing or exporting that information to text files. In this article we will look at putting that data to work to apply some logic and get exactly the results we want by using Conditional statements, loops and objects.
First of all lets start with that list of computers we created in the third article:
$list = import-csv –path computerlist.csv
This gave us a list of computers which we now want to work with. To work with a list we can use a loop which will start with the first one, do something then go to the next one in the list. The ForEach-Object cmdlet does this for us in Powershell, normally shortend to foreach which is called an alias.
A quick note about alias’s. Powershell has a feature where you can register an alias for something which allows you to type less. There are many pre-canned alias’s some of which we have already used.
-
Ft is an alias of Format-Table
-
Dir is an alias of Set-Location
-
Foreach or % is an alias of ForEach-Object
-
Gwmi is an alias of Get-WMIObject
To get a full list of alias’s type Get-Alias at the Powershell command line. To create you own alias use the Set-Alias cmdlet. To get help on this type Get-Help Set-Alias.
Back to the article. Generally in Powershell the syntax for conditional statements (if, foreach ) are command followed by the condition in round brackets (condition) followed by the ‘what to do if condition is met’ in curly brackets {do something}(Also known as a scriptblock). Somewhat like the following (proper syntax will follow):
Foreach (something in list) {do something}
If ($stuff equals 10) {add 10 to something else} Else {note an error}
The operator ‘equals’ is written -eq so a valid if statement might read
If ($stuff -eq 10) {$answer = $thing + $stuff} Else {Write-Host “stuff did not equal 10”}
The foreach loop we will be using to go through the list in $list earlier will look like this:
Foreach ($computer in $list) {Write-Host $computer.computername}
Lets go through that, $computer is a variable created on the fly that contains just the current object in the list. Remember back when we created the computerlist.csv we put a heading called ComputerName. When we reimport the CSV this becomes a property we can use. So $computer is an Object that contains a property called Computername which currently contains a value of the first computer in the list contained in $list. So to get at that data we use $computer.computername to return that value. In this case it will just write it to the screen, then go to the next one in $list.
To make it do something more useful rather than just writing to the screen we need to put the code we were talking about in earlier articles into the scriptblock {}. The following code will loop through the list and ping each machine. It will also write the result to the screen:
Foreach ($computer in $list)
{ $pingtarget = $computer.computername
$ping = get-wmiobject -Query "select * from win32_pingstatus where Address='$pingtarget'"
if ($ping.statuscode -eq 0)
{Write-Host “Ping Worked for $pingtarget”}
Else { Write-Host “Ping failed for $pingtarget”}
}
OK so far so good, now we want to do the WMI queries if the ping is good so we replace the write-host bits again for something more useful.
Foreach ($computer in $list)
{ $pingtarget = $computer.computername
$ping = get-wmiobject -Query "select * from win32_pingstatus where Address='$pingtarget'"
if ($ping.statuscode -eq 0)
{$compsys = get-wmiobject win32_computersystem -computername $pingtarget
$sysenc = get-wmiobject win32_systemenclosure -computername $pingtarget
$sysbios = get-wmiobject win32_bios -computername $pingtarget
$logicaldisk = get-wmiobject win32_logicaldisk -computername $pingtarget}
Else { Write-Host “Ping failed for $pingtarget”}
}
So now we have four variables that contain the WMI information we want $compsys, $sysenc, $sysbios and $logicaldisk, we need to put that data somewhere else so that its not lost when the foreach loop goes to the next one in the list.
We could dump these values directly to a csv at this point but this is a good opportunity to look at customising an object to contain the properties we want and store this data in that object. This is useful as we may want to do further processing on the data before we output it. We could create an object from scratch but we already have one that will easily export to a csv format, namely $list that we imported from a csv in the first place, that contained one extra property that we wanted called Computername. For this to be useful for storing the wmi data we have collected we need to add properties to the object (similar to adding headings in the spreadsheet). To do this we can use the Add-Member cmdlet. First we create $list from the csv file(a collection of objects) then we grab the first one in the list (similar to what will happen in the foreach loop). This is a single object that contains one property “Computername”. We can then use the Add-Member cmdlet to add another property and value. To test this type the following at the Powershell commandline:
$list = Import-Csv computerlist.csv
$computer = $list[0]
Write-host $computer
You will see the following result (the [0] grabs the first one in the list, [4] would grab the fifth one):
Computername
------------
TRU-ISA-01
Next Add a new property
Add-Member -InputObject $computer -MemberType noteproperty -Name TestProperty -Value "test"
$computer
You will see the following result:
Computername TestProperty
------------ ------------
TRU-ISA-01 test
As you can see there is now a new property called TestProperty with a value of test. We can use this mechanism to add the results from the WMI queries to a customised object which we can then easily export to csv later.
In this article we have looked at conditional statements and looping with if and foreach, we’ve seen how you can use if statements inside loops to get the WMI results we wanted, we then looked at creating custom cbjects with extra properties and looked at populating them with specific data. In the next article we will put all these techniques together to create the final script that will audit the list of computers.
-
Putting the Script Together
This is the last article in this series of five looking at using Powershell to solve a problem by doing an audit of the basic hardware specification of computers in your Active Directory. In this final article we will put together the techniques used in this series to create a working script.
A few tips on working with scripts to start
-
Call your scripts, variables, input and output files something descriptive, its really annoying when you know you’ve written something useful somewhere but all your scripts are called test.ps1, test2.ps1, testing.ps1 etc. I normally call them for their purpose and version e.g. simple-audit-v1.0.ps1.
-
Comment your script so that when you look back you can easily understand what you were trying to do. In Powershell the hash symbol (#) denotes a comment and Powershell will ignore anything that starts with this.
-
Put comments in the top of your script that describe what it does and any dependencies, this is useful if you came back to them later or share scripts with others
Ok so lets start with the top comments.
#####################################################################
# ScriptName : simple-audit-v1.0.ps1
# Author : Will Rawlings, Tru Consulting Ltd. www.truconsulting.co.uk
# Purpose : This script pulls computer objects from AD, saves to a CSV
# then uses that CSV to gather WMI info from each computer on the list
# the results are exported to audit-results.csv and failures are saved
# to audit-noresults.csv.
#######################################################################
This might seem over the top but later you can use Get-Content to search your scripts for things of interest. Next we start with the AD Search Stuff.
# Directory search for computer objects
write-host -foregroundcolor cyan "Reading Computer Objects From Active Directory...."
$Filter = "(objectCategory=Computer)"
$domain = [ADSI]""
$ADSearcher = New-Object System.DirectoryServices.DirectorySearcher
$ADSearcher.SearchRoot = $domain
$ADSearcher.PageSize = 1000
$ADSearcher.Filter = $Filter
$ADSearcher.PropertiesToLoad.Add("name")
$ADResults = $ADSearcher.FindAll()
This is explained in the second article the only addition is the inclusion of the Write-host line, this is not needed but can be useful to get feedback when you run the script. Next we collect the computers and dump them to a csv file.
# Create a list from the search results and dump to csv, delete existing if present.
$computerlist = @("Computername")
foreach ($Result in $ADResults)
{$computerlist += ($Result.Properties).name}
if ((test-path computerlist.csv) -eq $true)
{write-host -foregroundcolor yellow "deleting existing computerlist.csv"
remove-item computerlist.csv}
write-host -foregroundcolor cyan "writing computerlist.csv...."
new-item computerlist.csv -itemtype file
add-content computerlist.csv -value $computerlist
Here we build the list $computerlist by looping through the results of the Active Directory search and appending the value of the name property to $computerlist . += means append in this case. I’ve added the test-path cmdlet to show how you can test to see if files are present. This is not actually needed as the file would be overwritten anyway but its good to test and see. Then New-item is used to create a blank file, computerlist.csv and add-content is used to dump the data to the file.
This could be created as one script with the rest in a second script that used computerlist.csv. For this example I will create one script with a pause, where you will be able to manually edit the csv file. Reason for this is you may not want to audit 1000’s of machines at once as this could take a very long time, might be better to carve it up into smaller jobs and collate the data at the end.
# pause giving chance to edit the csv file
write-host -foregroundcolor green "Computerlist.csv ready for edit."
read-host "Hit Enter to continue"
The read-host cmdlet takes input from the command line, if you wanted to capture this input you could assign a variable to it e.g. $response = read-host “type something”. In this case we are just using it to pause the script so the csv file can be examined. Next we import the csv file to a variable called $list and create the $results and $noresults variables that we will store all the results in.
#Import the computerlist from the csv file .script can start from here if manual #computerlist.csv created.
$list = import-csv computerlist.csv
#create results variables
$results = "",""
$noresults = "",""
Now we have a list of computers and somewhere to store the results, next its time for the main loop of the script. The loop itself contains other loops and if..else statements nested inside so that we can test for an error and then jump out to the next one without running commands that we know will fail. For example if the computer cannot be pinged then it will not repond to get-wmiobject so jump to the else statement instead. The loop looks like the following with colours showing the tests for error conditions.
foreach ($computer in $list)
{if (ping status –eq 0 )
{get wmi info}
{if (wmi results –ne $null)
{Do stuff with results}
{Add to results table}
else {Add Failled WMI to noresults table}
else {Add Failed Ping to no results table}
Now lets look at the actual code in detail.The foreach loop takes the list of computers in $list and first tests to see if they are on the network by pinging them. Notice the $errorActionPreference = “silentlyContinue” this switches off the red error text to the screen. This is done as errors are anticipated when computers are not contactable. Its useful to switch it back to “Continue” when you don’t expect errors so you can see unexpected error text when the script is run. $pingtarget is created from the computername property of the current computer object $computer and used in the get-wmiobject query that does the ping, the results are storeed in $ping.
# Main loop which takes the list from the csv file and queries each computer in turn
foreach ($computer in $list)
{
# ping target computer to see if its available on network
$ErrorActionPreference = "SilentlyContinue"
$pingtarget = $computer.computername
$ping = get-wmiobject -Query "select * from win32_pingstatus where Address='$pingtarget'"
We then test the result of the ping to see if the statuscode property equals 0, if not then the script will jump to the end of the curly brackets {} straight after the if statement. If the statuscode does equal 0 then we go ahead and get the wmi info from this computer. Notice $ErrorActionPreference = "Continue" is set so that we can now see any errors that occur, this is good for troubleshooting if the script fails.
# if ping is good do stuff else go to next in list
if ($ping.statuscode -eq 0)
{
#get wmi info
$compsys = get-wmiobject win32_computersystem -computername $pingtarget
$sysenc = get-wmiobject win32_systemenclosure -computername $pingtarget
$sysbios = get-wmiobject win32_bios -computername $pingtarget
$logicaldisk = get-wmiobject win32_logicaldisk -computername $pingtarget
$ErrorActionPreference = "Continue"
Next we check to see if the wmi queries were successful by seeing if any of the results are empty (null) represented in Powershell as $null. So if any of $compsys, $sysenc, $sysbios or $logicaldisk are empty we fail and jump to the end of the curly brackets {} following the if statement.
if ($compsys -ne $null -and $sysenc -ne $null -and $sysbios -ne $null -and $logicaldisk -ne $null)
{
Next we know that we have some results to work with so now its time to process them. Not all the results are in a useable format yet.
Firstly we get the Chassistype property from $sysenc which returns a numerical value which system builders write to the bios. Each number refers to a different chassis type as documented here: http://msdn.microsoft.com/en-us/library/aa394474.aspx
This is a good example of when to use a switch statement, this allows us to compare a result against a list and take action accordingly, In this case we take the number returned in $sysenc.chassistype and compare it to a list of known results. In the switch statement if $chassistype equalled 7 the the statement would run the following code: {$Chassis = "Tower"}.
# determine the type of chassis based on numerical value returned
$chassistype = $sysenc.chassistypes
switch ($chassistype)
{
1 {$Chassis = "Other"}
2 {$Chassis = "Unknown"}
3 {$Chassis = "Desktop"}
4 {$Chassis = "Low Profile Desktop"}
5 {$Chassis = "Pizza Box"}
6 {$Chassis = "Mini Tower"}
7 {$Chassis = "Tower"}
8 {$Chassis = "Portable"}
9 {$Chassis = "Laptop"}
10 {$Chassis = "Notebook"}
11 {$Chassis = "Hand Held"}
12 {$Chassis = "Docking Station"}
13 {$Chassis = "All in One"}
14 {$Chassis = "Sub Notebook"}
15 {$Chassis = "Space-Saving"}
16 {$Chassis = "Lunch Box"}
17 {$Chassis = "Main System Chassis"}
18 {$Chassis = "Expansion Chassis"}
19 {$Chassis = "SubChassis"}
20 {$Chassis = "Bus Expansion Chassis"}
21 {$Chassis = "Peripheral Chassis"}
22 {$Chassis = "Storage Chassis"}
23 {$Chassis = "Rack Mount Chassis"}
24 {$Chassis = "Sealed-Case PC"}
default {$Chassis = "Unable to Determine"}
}
Next we will look at the disk configuration. $logicaldisk contains all the disks that Windows can see including CD/DVD drives , cardslots etc. We are interested in The C: and D: drives as long as they are hard disks . The foreach loop goes through the list and checks for the DeviceID (drive letter) and DriveType properties(3 means a hard disk). Once its found the C: or D: drive and checked its a hard drive it creates three new variables for the drive size, free space and the format of the disk. I’ve use the [INT] when declaring these if I want the value to be an Integer (whole number). I also divide by 1048676 to turn bytes into Megabytes. So we now have six more variables that contain specific information about disks.
#determine the disk config
foreach ($disk in $logicaldisk)
{
# change the drive sizes to MB /1048576
#find c drive
if ($disk.DeviceID -eq "C:" -and $disk.DriveType -eq 3)
{[int]$cdrivesize = $disk.Size/1048576
[int]$cdrivefreespace = $disk.FreeSpace/1048576
$cformat = $disk.FileSystem}
#find d drive
if ($disk.DeviceID -eq "D:" -and $disk.DriveType -eq 3)
{[int]$ddrivesize = $disk.Size/1048576
[int]$ddrivefreespace = $disk.FreeSpace/1048576
$dformat = $disk.FileSystem}
}
Next we will take all this information and put in in a custom object $result using the add-member cmdlet. Notice that I have shortened the add-member lines as the arguments don’t always need to be stated Powershell is clever enough to work out what the values are for , I’ve left out –name, -value and shortened –inputobject to –in and –membertype to –type. In each case I add a property followed by a value e.g “Manufacturer” $compsys.manufacturer. This then gives us an object that contains all the info we are interested in called $result. The last line appends $result to $results which will contain all the info from all computers tested.
#colate the results into $results
$result = $computer
add-member -in $result -Type noteproperty "Manufacturer" $compsys.manufacturer
add-member -in $result -Type noteproperty "Model" $compsys.model
add-member -in $result -Type noteproperty "System Type" compsys.SystemType
add-member -in $result -Type noteproperty "TotalRam" ($compsys.TotalPhysicalMemory/1048576)
add-member -in $result -Type noteproperty "LogicalProcs" $compsys.NumberOfLogicalProcessors
add-member -in $result -Type noteproperty "Chassis Type" $Chassis
add-member -In $result -Type noteproperty "Ping-able" "Yes"
add-member -in $result -Type noteproperty "Ping Time" $ping.responsetime
add-member -in $result -Type noteproperty "Serial Number" $sysbios.SerialNumber
add-member -in $result -Type noteproperty "motherboard" $sysbios.SMBIOSBIOSVersion
add-member -in $result -Type noteproperty "chipsetverion" $sysbios.Version
add-member -in $result -Type noteproperty "Cdrivesize" $Cdrivesize
add-member -in $result -Type noteproperty "Ddrivesize" $Ddrivesize
add-member -in $result -Type noteproperty "cdrivefreespace" $cdrivefreespace
add-member -in $result -Type noteproperty "ddrivefreespace" $ddrivefreespace
add-member -in $result -Type noteproperty "cformat" $cformat
add-member -in $result -Type noteproperty "dformat" $dformat
$results += $result
}
The next statement is an else {} which will only run if the condition in the if statement was not met. Here we create an object $noresult and add two properties Ping-able = “Yes” (as this must have passed the ping test to get to this bit of code) and RPC available = “NO” as this error was due to a WMI call failure. Then append the $noresult to $noresults.
else
{
$noresult = $computer
add-member -In $noresult -Type noteproperty "Ping-able" "YES"
add-member -In $noresult -Type noteproperty "RPC available" "NO"
$noresults += $noresult
}
The Last part of the main loop ends here with an else that runs if the original ping failed. Here we add the same properties to $noresults as before but the values are different as this ran from a different error condition. $noresult is then appended to $noresults
}
else
{
$noresult = $computer
add-member -In $noresult -Type noteproperty "Ping-able" "No"
add-member -In $noresult -Type noteproperty "RPC available" "Not Tried"
$noresults += $noresult
}
}
Finally once the main loop has completed we end up with two variables called $results which contains all the good results and $noresults which contains the failures and an indication of why they failed. These are then exported to csv files using the Export-CSV cmdlet, sorted by the Computername field.
# export the results and failures into 2 csv files
write-host -foregroundcolor green "Writing audit-results.csv and audit-noresults.csv"
$results | sort-object computername | export-csv audit-results.csv -notypeinformation
$noresults | sort-object computername | export-csv audit-noresults.csv -notypeinformation
In the final article of this series we looked at putting the whole script together using all the techniques seen in the series. This has hopefully shown that using Powershell you can quickly and easily create quite complex scripts that can solve real problems and make repetitive tasks easy. Using WMI you have access to masses of configuration and state information which can help you make day to day IT tasks quicker.
