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+=" &nbsp &nbsp "
                    $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
}