Documenting Automatically With PowerShell
When I first started learning PowerShell it was quick one-liners, like get-aduser. My progression followed the crawl/walk/run model, and about when I learned to stand figuratively, I came up with the idea of making a script that could audit and write the documentation for a server for me. Looking back that script was, lets just say, not good, but in my defense it was PowerShell 1.0.
The idea was to run the script on the server, and it would spit out an HTML document that had everything I would want about a server in it, such as the hardware, OS info, server roles, software, etc. Subsequent versions incorporated the ability to run remotely, and stripped the creation of an HTML document in favor of just creating a hash table which could be fed to another function to output it as HTML, json, or some other format. As I am using Tiddlywiki to hold all my documentation I now also have a distinct script that grabs all the server names out of AD and then runs the documentation script against that list, formats the audit as html and inserts it into the Tiddlywiki where it is supposed to go, but those advanced features may be better suited for a different post; here I will stick to just getting the audit and formatting it.
Since this was a PowerShell 1.0 script, which I updated to a 5.1 script several years ago, it is Windows only, so for the Mac or Linux users you'll just have to wait until I need to run that on those operating systems before I update the script again. Arguably, I should be using pscustomobjects instead of just hashtables, and I'm sure there are a few other small changes to be made to conform to best practices, but I'm still going to present the script 'as is' because I feel it was an important stepping point over the years in my progression of learning PowerShell.
One of the things I wanted to do with this latest version was enable reading in pipeline values, meaning reading in an array of computers rather than having to run the function in a loop for each, which means enabling what PowerShell calls 'advanced functions', so the beginning of the get-computeraudit function looks like this:
function get-computerAudit{
[cmdletbinding()]
param([Parameter(Mandatory=$false,valuefrompipeline)][string]$computer='localhost')
BEGIN{
#initialize the list of audits
$Auditlist=[System.Collections.ArrayList]@()
} #end BEGIN
Two things to note in the above codeblock. One, the only parameter we are getting is what computer, or computers to run it on. We could take other parameters, and in fact my prior versions did allow for different selections to format the HTML in different ways or skip auditing certain things but I found over time I never used any of those parameters so did not carry them forward to this instance. Your mileage may vary if this inspires you to customize this script.
The second thing I would note is that we are creating an empty arraylist that the Process section of the script will add each computer's audit to, and that the End section will return.
If you've never used a Process block before, basically it automatically loops over the inputs from the pipeline (using the variable $_
), whereas the begin and end blocks only get run once regardless of however many values are passed. In this case, we want to run a script on a remote computer and get a return, so we will begin the Process block with this:
Process{
#invoke command on remote computer
$auditinfo=$null
$auditinfo=invoke-command $_ -scriptblock {
import-module webadministration -ErrorAction SilentlyContinue
Essentially, once the invoke-command runs it will run everything in the scriptblock on that remote machine. The first thing we will need to do on the remote machine is import and modules we will need to get the info we want, which for now is only the webadministration module in case it is an IIS server. Again, you may want additional modules if you are going to be auditing data that I haven't accounted for.
A large portion of the info we want is going to be given to us in one command: get-computerinfo
. The issue is that that command takes a noticeable amount of time to run, so running it once and storing the results in a variable will help keep the overall runtime down.
$computerinfo=get-computerinfo
For readability, I've broken out several functions:
Is the Computer a Virtual Machine?
Function isVM {
param([Parameter(Mandatory=$true,valuefrompipeline)]$computerinfo)
$bios = @{
version=$computerinfo.biosversion
serialnumber=$computerinfo.biosseralnumber
}
$compsys = @{
Model=$computerinfo.csmodel
manufacturer=$computerinfo.biosmanufacturer
}
if($bios.Version -match "VRTUAL") {"Virtual - Hyper-V"}
elseif($bios.Version -match "A M I") {"Virtual - Virtual PC"}
elseif($bios.Version -like "*Xen*") {"Virtual - Xen"}
elseif($bios.SerialNumber -like "*VMware*") {"Virtual - VMWare"}
elseif($compsys.manufacturer -like "*Microsoft*") {"Virtual - Hyper-V"}
elseif($compsys.manufacturer -like "*VMWare*") {"Virtual - VMWare"}
elseif($compsys.model -like "VMware*") {"Virtual - VMWare"}
elseif($compsys.model -like "*Virtual*") {"Virtual"}
else {"Physical"}
} #used to tell if the machine is a Virtual Machine
What are the contents of the hosts file?
function get-firewallrules{
$rules=get-netfirewallrule -policystore activestore |where {$_.direction -eq 'inbound' -and $_.enabled -eq "true" -and $_.action -eq'Allow'}
foreach ($rule in $rules){
$appfilter=get-netfirewallapplicationfilter -AssociatedNetFirewallRule $rule
$portfilter=get-netfirewallportfilter -AssociatedNetFirewallRule $rule
$addressfilter=Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $rule
[ordered]@{
displayname=$rule.displayname
profile=$rule.profile
applicationfilter=$appfilter.program
protocol=$portfilter.protocol
localport=$portfilter.localport
remoteport=$portfilter.remoteport
localaddress=$addressfilter.localaddress
remoteaddress=$addressfilter.remoteaddress
}
}
}
What are software is installed? (note, assumes standard windows applications)
function get-installedSoftware{
$UninstallKey="SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
$Uninstall64Key="SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
$reg=[microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$env:computername)
$regkey=$reg.OpenSubKey($UninstallKey)
#Retrieve an array of strings that contain all the subkey names
$subkeys=$regkey.GetSubKeyNames()
foreach($key in $subkeys){
try{
$thisKey=$UninstallKey+"\\"+$key
$thisSubKey=$reg.OpenSubKey($thisKey)
if($thisSubKey.GetValue("DisplayName"))
{
[ordered]@{
displayname=$thisSubKey.GetValue("DisplayName")
displayversion=$thisSubKey.GetValue("DisplayVersion")
publisher=$thisSubKey.GetValue("Publisher")
Location=$thisSubKey.GetValue("InstallLocation")
uninstallkey=$key
}
}
}catch{}
}
$regkey2=$reg.OpenSubKey($Uninstall64Key)
#Retrieve an array of strings that contain all the subkey names
$subkeys=$regkey2.GetSubKeyNames()
foreach($key in $subkeys){
try{
$thisKey=$UninstallKey+"\\"+$key
$thisSubKey=$reg.OpenSubKey($thisKey)
if($thisSubKey.GetValue("DisplayName"))
{
[ordered]@{
displayname=$thisSubKey.GetValue("DisplayName")
displayversion=$thisSubKey.GetValue("DisplayVersion")
publisher=$thisSubKey.GetValue("Publisher")
Location=$thisSubKey.GetValue("InstallLocation")
uninstallkey=$key
}
}
}catch{}
}
}
Before I continue, lets talk about this function. This isn't an absolute always show everything installed. It only checks for things installed for everyone on the machine, meaning programs installed into the context of user X or Y won't show up. Also it doesn't necessarily show installed 'apps' that you would get from the Microsoft store. Perhaps a task to tackle in a future version.
Lastly, a function to return all the incoming enabled firewall rules.
function get-firewallrules{
$rules=get-netfirewallrule -policystore activestore |where {$_.direction -eq 'inbound' -and $_.enabled -eq "true" -and $_.action -eq'Allow'}
foreach ($rule in $rules){
$appfilter=get-netfirewallapplicationfilter -AssociatedNetFirewallRule $rule
$portfilter=get-netfirewallportfilter -AssociatedNetFirewallRule $rule
$addressfilter=Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $rule
[ordered]@{
displayname=$rule.displayname
profile=$rule.profile
applicationfilter=$appfilter.program
protocol=$portfilter.protocol
localport=$portfilter.localport
remoteport=$portfilter.remoteport
localaddress=$addressfilter.localaddress
remoteaddress=$addressfilter.remoteaddress
}
}
}
Now we can begin putting it all together. Lets start by making a hashtable of all the OS info:
$osinfo=[ordered]@{
name=$computerinfo.windowsproductname
installationdate=$computerinfo.windowsinstalldatefromregistry
LastWindowsUpdateinstall=(New-Object -com "Microsoft.Update.AutoUpdate").Results|select -exp lastinstallationsuccessdate
isvirtual=isVM $computerinfo
}
Then a hashtable of the server role information:
$serverroles=[ordered]@{
Domaincontroller=if((gwmi win32_computersystem).domainrole -in (4,5)){$true} else {$false}
PrimaryDomainController=if((gwmi win32_computersystem).domainrole -eq 5){$true} else {$false}
DHCP=try{if ((get-dhcpserverv4scope -erroraction silentlycontinue |measure|select -exp count) +
(get-dhcpserverv6scope -erroraction silentlycontinue|measure|select -exp count)){$true}else{$false}}catch{$false};
DNS=try{if(get-service dns -erroraction SilentlyContinue |where {$_.status -eq "running"}|select -exp status ){$true}else{$false}}catch{$false};
SQL=try{if((gwmi -namespace "root\microsoft\sqlserver" -class "__Namespace" -erroraction SilentlyContinue|measure|select -exp count) -gt 0){$true}else{$false}}catch{$false};
Fileserver=try{if((gwmi -class win32_share -filter "type=0" -erroraction SilentlyContinue|measure|select -exp count) -gt 0){$true}else{$false}}catch{$false};
Printserver=try{if((gwmi -class win32_share -filter "type=1" -erroraction SilentlyContinue|measure|select -exp count) -gt 0){$true}else{$false}}catch{$false};
VCenter=try{if(get-service vpxd -erroraction SilentlyContinue |where {$_.status -eq "Running"}){$true}else{$false}}catch{$false};
IIS=try{if($sites=get-childitem IIS:\sites -ErrorAction SilentlyContinue){$true}else{$false}}catch{$false};
NPS=if (Get-Module -ListAvailable -Name nps){$true}else{$false}
}
A hashtable of hardware information:
$hardware=[ordered]@{
manufacturer=$computerinfo.csmodel
model=$computerinfo.csmodel
serialnumber=$computerinfo.biosserialnumber
Processor=@{
Processor=(gwmi win32_processor -filter "deviceid='CPU0'" -property name |select -exp name)
ProcessorCount=(gwmi win32_processor |measure|select -exp count)
ProcessorCores=(gwmi win32_processor -filter "deviceid='CPU0'" -property numberofcores |select -exp numberofcores)
ProcessorSpeed=(gwmi win32_processor -filter "deviceid='CPU0'" -property maxclockspeed |select -exp maxclockspeed)
L2CacheSize=(gwmi win32_processor -filter "deviceid='CPU0'" -property L2CacheSize |select -exp L2CacheSize)
ExternalClock=(gwmi win32_processor -filter "deviceid='CPU0'" -property extclock |select -exp extclock)
}
memory=@{
TotalMemory="$(gwmi -class Win32_PhysicalMemory | Measure-Object -Property capacity -Sum | % {[Math]::Round(($_.sum / 1GB),2)})"+"GB"
MemoryBanks=foreach ($Dimm in (gwmi -class Win32_PhysicalMemory) )
{
@{
tag=$Dimm.tag
size="$([math]::round(($Dimm.capacity/1Gb),2))" +"GB"
speed=$Dimm.configuredclockspeed
}
};
}
disks=foreach ($disk in (get-disk)){
[pscustomobject]@{
disknumber=$disk.number
size="$([math]::round(($disk.size/1GB),2))GB"
manufacturer=$disk.manufacturer
Model=$disk.model
serialnumber=$disk.serialnumber
bootable=$disk.isboot
partitions=try{
foreach ($part in (get-partition -DiskNumber $disk.number -erroraction stop)){
[PSCustomObject]@{
partitionNumber=$part.partitionnumber
driveletter=if($part.driveletter -match "\W+"){"N/A"}else{$part.driveletter}
size=[math]::round(($part.size/1Gb),2)
remaining=[math]::round(((get-volume ($part).DriveLetter -ErrorAction SilentlyContinue).sizeremaining/1Gb),2)
system=$part.issystem
boot=$part.isboot
}
if (-not $part.driveletter -in ('C','D')){break}
}
}catch{$null}
}
}
network=foreach ($Adapter in get-netadapter){
@{
name=$adapter.name
Description=$adapter.interfacedescription
status=$adapter.status
speed=$adapter.linkspeed
MAC=$adapter.macaddress
IPAddress=try{
if((get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv4 -erroraction SilentlyContinue|measure |select -exp count)+(get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv6 -erroraction SilentlyContinue|measure |select -exp count))
{
foreach ($ipAddress in (get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv4))
{
@{
address=$ipaddress.ipaddress
gateway=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).ipv4defaultgateway).nexthop
DNS=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).dnsserver).serveraddresses
}
}
#output ipv6 Addresses
foreach ($ipAddress in (get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv6))
{
@{
address=$ipaddress.ipaddress
gateway=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).ipv6defaultgateway).nexthop
DNS=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).dnsserver).serveraddresses
}
}
}else{$null}
}
Catch{$null};
}
}
}
And a hashtable from server role details:
$serverroledetails=[ordered]@{
iisconfig=if($serverroles.iis){
foreach ($Site in $sites) {
@{
Id=$Site.id
name=$site.name
state=$site.state
path=$site.physicalpath
protocol=($site |select name |get-webbinding|select -exp protocol -ErrorAction SilentlyContinue)
binding=($site |select name |get-webbinding|select -exp bindinginformation -ErrorAction SilentlyContinue)
}
}
}else{$null};
dhcp=if($serverroles.dhcp){
if (get-service dhcpserver |where {$_.status -eq "Running"}){
foreach($scope in get-dhcpserverv4scope){
@{
name=$scope.name
ID=$scope.scopeid
subnetmask=$scope.subnetmask
startrange=$scope.startrange
endrange=$scope.endrange
}
}
}else{"disabled"}
}else{$null};
NPS=if($serverroles.NPS){
#check to see if the service is running
if (get-service IAS |where {$_.status -eq "Running"}){
foreach($client in get-npsradiusclient){
@{
name=$client.name
address=$client.address
enabled=$client.enabled
}
}
}
}else{$null};
fileshares=if($serverroles.fileserver){
foreach($share in (gwmi -class win32_share -filter "type=0")){
@{
name=$share.name
path=$share.path
description=$share.description
#note to self, get the ACL for the share permissions here
}
}
}else{$null};
printers=if($serverroles.Printserver){
try{
if(get-printer -erroraction SilentlyContinue|measure|select -exp count){
foreach($printer in get-printer){
@{
name=$printer.Name
type=$printer.Type
drivername=$printer.driverName
portname=$printer.portName
shared=$printer.shared
}
}
}
}
catch{$null}
}else{$null};
}
And finally, we can put it all together into one hashtable:
$Auditinfo=[ordered]@{
fqdn=$env:computername+"."+$env:userdnsdomain
auditdate=(get-date).tostring()
OSinfo=$osinfo
ServerRoles=$serverroles
Hardware=$hardware
serverroledetails=$serverroledetails
windowsFeatures=try{get-windowsfeature |select displayname, depth, installed, installstate}catch{$null};
services=get-service |select status, name, displayname
openInboundFirewallRules=get-firewallrules
VisibleInstalledSoftware=get-installedsoftware
hostentry=get-hostfileaudit
}
The end of the audit script returns the data from the remote computer (the invoke-command scriptblock), adds the returned data to our auditlist array we created in the Begin section, and then runs our End section:
return $auditinfo
} #end invoke-command scriptblock
#add to the Array of computers
$auditlist.add($auditinfo)|out-null
} #end Process
End {
return $auditlist
} #end END
}
Once it is all assembled, calling the function is just: $auditlist=$computers|get-computerAudit
Once you have the $auditlist it's pretty easy to convert to json using the convertto-json cmdlet as long as you use an appropriate depth. In fact that is how my format function does it: $output=convertto-json $audit -depth 10
. Converting to HTML is a more subjective exercise however, as you may want things left oriented or right, collapsible or not, and to that effect I'm going to only post the function and not attempt to explain why it's outputting things in a particular way.
function get-computerAudit{
[cmdletbinding()]
param([Parameter(Mandatory=$false,valuefrompipeline)][string]$computer='localhost')
BEGIN{
#initialize the list of audits
$Auditlist=[System.Collections.ArrayList]@()
} #end BEGIN
Process{
#invoke command on remote computer
$auditinfo=$null
$auditinfo=invoke-command $_ -scriptblock {
import-module webadministration -ErrorAction SilentlyContinue
$computerinfo=get-computerinfo
#------------helper functions
Function isVM {
param([Parameter(Mandatory=$true,valuefrompipeline)]$computerinfo)
$bios = @{
version=$computerinfo.biosversion
serialnumber=$computerinfo.biosseralnumber
}
$compsys = @{
Model=$computerinfo.csmodel
manufacturer=$computerinfo.biosmanufacturer
}
if($bios.Version -match "VRTUAL") {"Virtual - Hyper-V"}
elseif($bios.Version -match "A M I") {"Virtual - Virtual PC"}
elseif($bios.Version -like "*Xen*") {"Virtual - Xen"}
elseif($bios.SerialNumber -like "*VMware*") {"Virtual - VMWare"}
elseif($compsys.manufacturer -like "*Microsoft*") {"Virtual - Hyper-V"}
elseif($compsys.manufacturer -like "*VMWare*") {"Virtual - VMWare"}
elseif($compsys.model -like "VMware*") {"Virtual - VMWare"}
elseif($compsys.model -like "*Virtual*") {"Virtual"}
else {"Physical"}
} #used to tell if the machine is a Virtual Machine
Function get-hostfileaudit{
#get hosts file and output the non-comments
$hostspath="$env:windir\system32\drivers\etc\hosts"
$hosts=get-content $hostspath
foreach ($hostentry in $hosts){
#test if it is a comment
if($hostentry.contains("#") -or [string]::IsNullOrWhiteSpace($hostentry)){
}
else {
#write-host $hostentry
$hostentry
}
}
}
function get-installedSoftware{
$UninstallKey="SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
$Uninstall64Key="SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
$reg=[microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$env:computername)
$regkey=$reg.OpenSubKey($UninstallKey)
#Retrieve an array of strings that contain all the subkey names
$subkeys=$regkey.GetSubKeyNames()
foreach($key in $subkeys){
try{
$thisKey=$UninstallKey+"\\"+$key
$thisSubKey=$reg.OpenSubKey($thisKey)
if($thisSubKey.GetValue("DisplayName"))
{
[ordered]@{
displayname=$thisSubKey.GetValue("DisplayName")
displayversion=$thisSubKey.GetValue("DisplayVersion")
publisher=$thisSubKey.GetValue("Publisher")
Location=$thisSubKey.GetValue("InstallLocation")
uninstallkey=$key
}
}
}catch{}
}
$regkey2=$reg.OpenSubKey($Uninstall64Key)
#Retrieve an array of strings that contain all the subkey names
$subkeys=$regkey2.GetSubKeyNames()
foreach($key in $subkeys){
try{
$thisKey=$UninstallKey+"\\"+$key
$thisSubKey=$reg.OpenSubKey($thisKey)
if($thisSubKey.GetValue("DisplayName"))
{
[ordered]@{
displayname=$thisSubKey.GetValue("DisplayName")
displayversion=$thisSubKey.GetValue("DisplayVersion")
publisher=$thisSubKey.GetValue("Publisher")
Location=$thisSubKey.GetValue("InstallLocation")
uninstallkey=$key
}
}
}catch{}
}
}
function get-firewallrules{
$rules=get-netfirewallrule -policystore activestore |where {$_.direction -eq 'inbound' -and $_.enabled -eq "true" -and $_.action -eq'Allow'}
foreach ($rule in $rules){
$appfilter=get-netfirewallapplicationfilter -AssociatedNetFirewallRule $rule
$portfilter=get-netfirewallportfilter -AssociatedNetFirewallRule $rule
$addressfilter=Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $rule
[ordered]@{
displayname=$rule.displayname
profile=$rule.profile
applicationfilter=$appfilter.program
protocol=$portfilter.protocol
localport=$portfilter.localport
remoteport=$portfilter.remoteport
localaddress=$addressfilter.localaddress
remoteaddress=$addressfilter.remoteaddress
}
}
}
#------------end helper functions
$osinfo=[ordered]@{
name=$computerinfo.windowsproductname
installationdate=$computerinfo.windowsinstalldatefromregistry
LastWindowsUpdateinstall=(New-Object -com "Microsoft.Update.AutoUpdate").Results|select -exp lastinstallationsuccessdate
isvirtual=isVM $computerinfo
}
$serverroles=[ordered]@{
Domaincontroller=if((gwmi win32_computersystem).domainrole -in (4,5)){$true} else {$false}
PrimaryDomainController=if((gwmi win32_computersystem).domainrole -eq 5){$true} else {$false}
DHCP=try{if ((get-dhcpserverv4scope -erroraction silentlycontinue |measure|select -exp count) +
(get-dhcpserverv6scope -erroraction silentlycontinue|measure|select -exp count)){$true}else{$false}}catch{$false};
DNS=try{if(get-service dns -erroraction SilentlyContinue |where {$_.status -eq "running"}|select -exp status ){$true}else{$false}}catch{$false};
SQL=try{if((gwmi -namespace "root\microsoft\sqlserver" -class "__Namespace" -erroraction SilentlyContinue|measure|select -exp count) -gt 0){$true}else{$false}}catch{$false};
Fileserver=try{if((gwmi -class win32_share -filter "type=0" -erroraction SilentlyContinue|measure|select -exp count) -gt 0){$true}else{$false}}catch{$false};
Printserver=try{if((gwmi -class win32_share -filter "type=1" -erroraction SilentlyContinue|measure|select -exp count) -gt 0){$true}else{$false}}catch{$false};
VCenter=try{if(get-service vpxd -erroraction SilentlyContinue |where {$_.status -eq "Running"}){$true}else{$false}}catch{$false};
IIS=try{if($sites=get-childitem IIS:\sites -ErrorAction SilentlyContinue){$true}else{$false}}catch{$false};
NPS=if (Get-Module -ListAvailable -Name nps){$true}else{$false}
}
$hardware=[ordered]@{
manufacturer=$computerinfo.csmodel
model=$computerinfo.csmodel
serialnumber=$computerinfo.biosserialnumber
Processor=@{
Processor=(gwmi win32_processor -filter "deviceid='CPU0'" -property name |select -exp name)
ProcessorCount=(gwmi win32_processor |measure|select -exp count)
ProcessorCores=(gwmi win32_processor -filter "deviceid='CPU0'" -property numberofcores |select -exp numberofcores)
ProcessorSpeed=(gwmi win32_processor -filter "deviceid='CPU0'" -property maxclockspeed |select -exp maxclockspeed)
L2CacheSize=(gwmi win32_processor -filter "deviceid='CPU0'" -property L2CacheSize |select -exp L2CacheSize)
ExternalClock=(gwmi win32_processor -filter "deviceid='CPU0'" -property extclock |select -exp extclock)
}
memory=@{
TotalMemory="$(gwmi -class Win32_PhysicalMemory | Measure-Object -Property capacity -Sum | % {[Math]::Round(($_.sum / 1GB),2)})"+"GB"
MemoryBanks=foreach ($Dimm in (gwmi -class Win32_PhysicalMemory) )
{
@{
tag=$Dimm.tag
size="$([math]::round(($Dimm.capacity/1Gb),2))" +"GB"
speed=$Dimm.configuredclockspeed
}
};
}
disks=foreach ($disk in (get-disk)){
[pscustomobject]@{
disknumber=$disk.number
size="$([math]::round(($disk.size/1GB),2))GB"
manufacturer=$disk.manufacturer
Model=$disk.model
serialnumber=$disk.serialnumber
bootable=$disk.isboot
partitions=try{
foreach ($part in (get-partition -DiskNumber $disk.number -erroraction stop)){
[PSCustomObject]@{
partitionNumber=$part.partitionnumber
driveletter=if($part.driveletter -match "\W+"){"N/A"}else{$part.driveletter}
size=[math]::round(($part.size/1Gb),2)
remaining=[math]::round(((get-volume ($part).DriveLetter -ErrorAction SilentlyContinue).sizeremaining/1Gb),2)
system=$part.issystem
boot=$part.isboot
}
if (-not $part.driveletter -in ('C','D')){break}
}
}catch{$null}
}
}
network=foreach ($Adapter in get-netadapter){
@{
name=$adapter.name
Description=$adapter.interfacedescription
status=$adapter.status
speed=$adapter.linkspeed
MAC=$adapter.macaddress
IPAddress=try{
if((get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv4 -erroraction SilentlyContinue|measure |select -exp count)+(get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv6 -erroraction SilentlyContinue|measure |select -exp count))
{
foreach ($ipAddress in (get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv4))
{
@{
address=$ipaddress.ipaddress
gateway=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).ipv4defaultgateway).nexthop
DNS=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).dnsserver).serveraddresses
}
}
#output ipv6 Addresses
foreach ($ipAddress in (get-netipaddress -InterfaceIndex $adapter.ifindex -AddressFamily IPv6))
{
@{
address=$ipaddress.ipaddress
gateway=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).ipv6defaultgateway).nexthop
DNS=((get-netipconfiguration -InterfaceIndex $ipaddress.interfaceIndex).dnsserver).serveraddresses
}
}
}else{$null}
}
Catch{$null};
}
}
}
$sites=$null
$serverroledetails=[ordered]@{
iisconfig=if($serverroles.iis){
foreach ($Site in $sites) {
@{
Id=$Site.id
name=$site.name
state=$site.state
path=$site.physicalpath
protocol=($site |select name |get-webbinding|select -exp protocol -ErrorAction SilentlyContinue)
binding=($site |select name |get-webbinding|select -exp bindinginformation -ErrorAction SilentlyContinue)
}
}
}else{$null};
dhcp=if($serverroles.dhcp){
if (get-service dhcpserver |where {$_.status -eq "Running"}){
foreach($scope in get-dhcpserverv4scope){
@{
name=$scope.name
ID=$scope.scopeid
subnetmask=$scope.subnetmask
startrange=$scope.startrange
endrange=$scope.endrange
}
}
}else{"disabled"}
}else{$null};
NPS=if($serverroles.NPS){
#check to see if the service is running
if (get-service IAS |where {$_.status -eq "Running"}){
foreach($client in get-npsradiusclient){
@{
name=$client.name
address=$client.address
enabled=$client.enabled
}
}
}
}else{$null};
fileshares=if($serverroles.fileserver){
foreach($share in (gwmi -class win32_share -filter "type=0")){
@{
name=$share.name
path=$share.path
description=$share.description
#note to self, get the ACL for the share permissions here
}
}
}else{$null};
printers=if($serverroles.Printserver){
try{
if(get-printer -erroraction SilentlyContinue|measure|select -exp count){
foreach($printer in get-printer){
@{
name=$printer.Name
type=$printer.Type
drivername=$printer.driverName
portname=$printer.portName
shared=$printer.shared
}
}
}
}
catch{$null}
}else{$null};
}
$Auditinfo=[ordered]@{
fqdn=$env:computername+"."+$env:userdnsdomain
auditdate=(get-date).tostring()
OSinfo=$osinfo
ServerRoles=$serverroles
Hardware=$hardware
serverroledetails=$serverroledetails
windowsFeatures=try{get-windowsfeature |select displayname, depth, installed, installstate}catch{$null};
services=get-service |select status, name, displayname
openInboundFirewallRules=get-firewallrules
VisibleInstalledSoftware=get-installedsoftware
hostentry=get-hostfileaudit
}
return $auditinfo
} #end invoke-command scriptblock
#add to the Array of computers
$auditlist.add($auditinfo)|out-null
} #end Process
End {
return $auditlist
} #end END
}
function format-computeraudit {
[cmdletbinding()]
<#
.SYNOPSIS
formats a computer audit for a given type
.DESCRIPTION
inputs a computer audit object and outputs either formatted HTML or JSON
.PARAMETER Audit
A computer audit object
.PARAMETER format
A string. Valid options are HTML or JSON. Maybe someday I will implement XML
#>
param(
[Parameter(Mandatory=$true,valuefrompipeline)]
[pscustomobject]
$Audit,
[Parameter(Mandatory=$true)]
[validateset("html","json")]
[string]
$format
)
BEGIN{
#initialize the list of audits
$formattedAuditlist=[System.Collections.ArrayList]@()
} #end BEGIN
PROCESS{
switch($format){
'html'{
$output="<head>`n"
$output+="<style>
table {
padding: 0px 0px 0px 0px;
margin: 0px 0px 0px 0px;
border: 0px;
}
h1 {color: #478BF6; font-family: Tahoma, Verdana, Arial, Sans-Serif; font-size: 16pt;}
h2 {color: #478BF6; font-family: Tahoma, Verdana, Arial, Sans-Serif; font-size: 14pt;}
h3 {color: #478BF6; font-family: Tahoma, Verdana, Arial, Sans-Serif; font-size: 12pt;}
th {color: #478BF6; font-family: Tahoma, Verdana, Arial, Sans-Serif; font-style: italic;
border-bottom: solid black 1.5pt;
}
a:link { color: #478BF6; }
a:visited { color: #478BF6; }
li { color: #478BF6; }
td {
color: #478BF6;
font-family: Tahoma, Verdana, Arial, Sans-Serif;
font-size: x-small;
vertical-align:top;
}
strong {color: #478BF6; }
body { background-color: white; font-family: Tahoma, Verdana, Arial, Sans-Serif;}
b { color: gray; }
</style>`n"
$output+="</head>`n"
$output+="<body>`n"
$output+="<div>`n"
$output+="<details>`n"
$output+="<summary>$($audit.fqdn) - Audit Date: $($audit.auditdate)</summary>`n"
$output+="<table><tr><td>`n"
$output+="<h1>Operating System</h1>`n"
$output+=[pscustomobject]$audit.osinfo|convertto-html -fragment -as list
$output+="</td>`n"
$output+="<td>`n" #begin Hardware
$output+="<h1>Hardware</h1>`n"
$output+="<p>Manufacturer: $($Audit.Hardware.manufacturer)`n"
$output+="<p>Model: $($Audit.Hardware.model)`n"
$output+="<p>Serial Number: $($Audit.Hardware.serialnumber)`n"
$output+="</td></tr><tr style='border-top:1px'><td>`n"
#begin processor
$output+="<h2>CPU</h2>`n"
$output+=[pscustomobject]$audit.hardware.processor|convertto-html -fragment -as list
#end processor
$output+="</td><td>`n"
#begin memory
$output+="<h2>Memory</h2>`n"
$output+="<p>Total Memory: $($audit.Hardware.memory.totalmemory)</p>`n"
$output+=[pscustomobject]$audit.Hardware.memory.memorybanks|convertto-html -fragment
#end memory
$output+="</td><td>`n"
#begin disks
$output+="<h2>Storage</h2>`n"
$output+="<table border=1>`n"
foreach ($disk in $audit.Hardware.disks){
$output+="<tr>"
$output+="<td>`n"
$output+="<p>Disk Number: $($disk.disknumber)</p>`n"
$output+="<p>Manufacturer: $($disk.manufacturer)</p>`n"
$output+="<p>Model: $($disk.Model)</p>`n"
$output+="<p>Serial Number: $($disk.Serialnumber)</p>`n"
$output+="<p>Bootable: $($disk.bootable)</p>`n"
$output+="</td><td>`n"
foreach ($part in $disk.partitions){
$output+=[pscustomobject]$part|convertto-html -fragment
}
$output+="</td>`n"
$output+="</tr>`n"
}
$output+="</table>`n"
#end disks
$output+="</td><td>`n"
#begin network
$output+="<h2>Network</h2>`n"
$output+="<table border=1>`n"
foreach ($network in $audit.Hardware.network){
$output+="<tr>"
$output+="<td>`n"
$output+="<p>Name: $($network.name)</p>`n"
$output+="<p>Desc: $($network.description)</p>`n"
$output+="<p>Speed: $($network.speed)</p>`n"
$output+="<p>Status: $($network.status)</p>`n"
$output+="<p>MAC Address: $($network.mac)</p>`n"
$output+="</td><td>`n"
$output+="<table><tr>"
$output+="<th>Address</th><th>Gateway</th><th>DNS</th>`n"
foreach($ip in $network.ipaddress){
$output+="<tr><td>$($ip.address)</td><td>$($ip.gateway)</td><td>$($ip.dns)</td></tr>`n"
}
$output+="</table>`n"
$output+="</td>`n"
$output+="</tr>`n"
}
$output+="</table>`n"
#end network
#$output+="</details>`n" #end hardware
$output+="</td></tr></table>"
$output+="<details>`n"
$output+="<summary>Server Roles</summary>`n"
$output+="<table><tr><td>`n"
$output+=[pscustomobject]$audit.serverroles|convertto-html -fragment -as list
$output+="</td><td>`n"
foreach($detail in $audit.serverroledetails.keys){
$output+="<details>`n"
$output+="<summary>$detail</summary>`n"
#foreach($item in $audit.serverroledetails.$detail){
$output+="<table>"
foreach($key in ($audit.serverroledetails.$detail.keys|select -unique)){
$output+="<th>$key</th>"
}
foreach($item in ($audit.serverroledetails.$detail)){
$output+="<tr>"
foreach($key in ($item.keys|select -unique)){
$output+="<td>$($item.$key)</td>"
}
$output+="</tr>`n"
}
$output+="</table>`n"
#$output+=[pscustomobject]$item|convertto-html -fragment
#}
$output+="</details>`n"
}
$output+="</td></tr></table>"
$output+="</details>`n" #end server roles
$output+="<details>`n" #begin Windows features
$output+="<summary>Windows Features</summary>`n"
$output+="<table><th>Display Name</th><th>Install State</th>`n"
foreach ($feature in $audit.windowsFeatures){
$line="<tr><td>"
$i=1
while($feature.depth -gt $i){
$line+="     "
$i++
}
if ($feature.installed){$line+=" [X] "}else {$line+=" [ ] "}
$line+= $feature.Displayname
$line+="</td><td>"+$feature.installstate+"</td></tr>`n"
$output+=$line
}
$output+="</table>`n"
$output+="</details>`n" #end Windows features
$output+="<details>`n" #begin installed Software
$output+="<summary>Installed Software</summary>`n"
$output+="<table><th>Name</th><th>Version</th><th>Publisher</th><th>Path</th>`n"
foreach($software in $audit.visibleinstalledsoftware){
$output+="<tr><td>"+$software.displayname+"</td><td>"+$software.displayversion+"</td><td>"+$software.publisher+"</td><td>"+$software.location+"</td></tr>`n"
}
$output+="</table>`n"
$output+="</details>`n" #end installed Software
$output+="<details>`n" #begin Services
$output+="<summary>Services</summary>`n"
$output+=[pscustomobject]$audit.services|convertto-html -fragment
$output+="</details>`n" #end Services
$output+="<details>`n" #begin firewall
$output+="<summary>Enabled Inbound Firewall Rules</summary>`n"
$output+="<table><th>Name</th><th>Profile</th><th>Program</th><th>Local Address</th><th>Remote Address</th><th>Protocol</th><th>Local Port</th><th>Remote Port</th>`n"
foreach($rule in $audit.openInboundFirewallRules){
$output+="<tr>"
$output+="<td>"+$rule.displayname+"</td>`n"
$output+="<td>"+$rule.profile+"</td>`n"
$output+="<td>"+$rule.applicationfilter+"</td>`n"
$output+="<td>"+$rule.localaddress+"</td>`n"
$output+="<td>"+$rule.remoteaddress+"</td>`n"
$output+="<td>"+$rule.protocol+"</td>`n"
$output+="<td>"+$rule.localport+"</td>`n"
$output+="<td>"+$rule.remoteport+"</td>`n"
$output+="</tr>`n"
}
$output+="</table>`n"
$output+="</details>`n" #end firewall
$output+="</details>`n"
$output+="</div>`n"
$output+="</body>`n"
}
'json' {$output=convertto-json $audit -depth 10}
'xml'{
$output='<?xml version="1.0" encoding="utf-8"?>'
#Should do this manually since the convertto-xml function inserts a bunch of garbage
#TODO
}
}
$formattedAuditlist.add($output)|out-null
}
END {
return $formattedAuditlist
} #end END
}