PowerShell won't iterate through a document - powershell

A PowerShell script won't iterate through a document to change each hyperlink in the document.
The script runs through a document library on SharePoint online and can open each document in the library. Then it should iterate through each document and pull any hyperlinks that it finds and then split the hyperlink into two parts. The script should then add the second half onto a new URL and update the Address to be the new, updated URL.
add-type -AssemblyName "Microsoft.Office.Interop.Word"
$wdunits = “Microsoft.Office.Interop.Word.wdunits” -as [type]
$donotsave = “Microsoft.Office.Interop.Word.wdDoNotSaveChanges” -as [type]
$save = “Microsoft.Office.Interop.Word.wdSaveChanges” -as [type]
$application = New-Object -ComObject Word.Application
$application.Visible = $false
$tenancy = "https://tenancy.sharepoint.com"
$url = "https://tenancy.sharepoint.com/sites/siteName/"
Connect-PnPOnline -Url $url -UseWebLogin
$library = Get-PnPList | Where-Object {$_.Title -eq "libraryName"}
$items = Get-PnPListItem -List $library
foreach ($item in $items) {
if ($item["FileLeafRef"] -match ".doc*") {
Write-Host "File Name: "$item["FileLeafRef"]
$item["FileLeafRef"]
$item["FileRef"]
Write-Host `
$documentLocation = "https://tenancy.sharepoint.com"+$item["FileRef"]
$documentLocation
$document = $application.Documents.Open($documentLocation)
$docURLS = #($document.Hyperlinks)
$docURLS | foreach{
Start-Sleep -Seconds 7
$newURI = ([uri]$_.address).AbsoluteUri
$result = $newURI.Split("=") | Select-Object -Skip 1 -First 1
$result
$newUrl = "https://tenancy.sharepoint.com/sites/siteName/_layouts/DocIdRedir.aspx?ID="+$result
$_.address = $newUrl
Write-Verbose ("Updating {0} to {1}" -f $_.Address,$newUrl) -Verbose
}
$document.save()
$document.close([Ref]$save)
$item.File.Update()
}
}
$application.quit()
Disconnect-PnPOnline
The script can currently iterate through the library and open each document, the issue comes when there are multiple hyperlinks in the document.
It changes the first URL correctly, but every other link after that receives the following errors:
Object has been deleted.
At C:\filepath.ps1 :36 char:5
+ $_.address = $newUrl
The object invoked has disconnected from its clients. (Exception from HRESULT: 0x80010108 (RPC_E_DISCONNECTED))
At C:\filepath.ps1:39 char:9
+ $document.save()
The object invoked has disconnected from its clients. (Exception from HRESULT: 0x80010108 (RPC_E_DISCONNECTED))
At C:\filepath.ps1:40 char:9
+ $document.close([Ref]$save)
You cannot call a method on a null-valued expression.
At C:\filepath.ps1:33 char:5
+ $result = $newURI.Split("=") | Select-Object -Skip 1 -First 1

If the $_.address value like "/sites/team?ID=1", the $newURI will null, then run $newURI.Split("=") | Select-Object -Skip 1 -First 1 will get "You cannot call a method on a null-valued expression".
You can check if the $newURI is null before use $newURI.Split method.
Or we can replace the code below.
$newURI = ([uri]$_.address).AbsoluteUri
$result = $newURI.Split("=") | Select-Object -Skip 1 -First 1
with
if($_.Address)
{
$result = $_.Address.Split("=") | Select-Object -Skip 1 -First 1
}
else
{
$_
}

Related

Script to get vmusage takes way too long and returns empty file

I have a script that used to work perfectly in our old domain. It pulls the last vm usage info on our vmware users. Since we switched to a new domain, I updated the ou line and now the script TAKES FOR EVER (like over an hour) and then winds up erring out at the Export-CSV command. I have the SqlServer module installed and am running with my privileged account that has access to our server and all accounts and persistent disks.
Export-CSV : Cannot process argument because the value of argument "name" is not valid. Change the value of the "name" argument and run the operation again.
At \\.\Get-LastVmUsage2.ps1:88 char:13
+ $CSVArray | Export-CSV $Filename -Append -force
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Export-Csv], PSArgumentException
+ FullyQualifiedErrorId : Argument,Microsoft.PowerShell.Commands.ExportCsvCommand
So tried removing $Filename variable and put in the full path
$CSVArray | Export-CSV -Path c:\temp\lastlogintime.csv -Append -force
Still takes an astronomically long time. Could someone help me figure out why its taking so long and why it leaves the file empty? I'm super new to programming and learning this all on my own. Full script below with identifying info removed
Param (
[string]$Filename,
[int]$InactiveDays
)
try {
Import-Module VMWare.PowerCli
Write-Host "Imported VMware PowerCLI"
}
catch {
Write-Host "Unable to import VMware module, please install it by running 'Install-Module Vmware.PowerCLI -Scope CurrentUser -AllowClobber'" -ForegroundColor Red
Break
}
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false
Connect-VIServer -Server icon-vc.iconid
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
if(!$InactiveDays){
$InactiveDays = 45
}
if(!$Filename){
$SaveDialog = New-Object -Typename System.Windows.Forms.SaveFileDialog
$SaveDialog.Filter = "CSV File (*.csv) | *.csv"
$Result = $SaveDialog.ShowDialog()
if($Result.value__ -ne 1){
Exit
}
$Filename = $SaveDialog.Filename
}
$Computers = Get-ADComputer -filter 'Enabled -eq $True' -SearchBase "OU=Virtual,OU=Computers-Internal,DC=Domain,DC=Com"
class CsvRow {
[object] ${ComputerName}
[object] ${LastUser}
[object] ${LastLogon}
[object] ${Inactive}
}
$CSVArray = #()
$Today = Get-Date
foreach($Computer in $Computers){
$Entry = New-Object CsvRow
Try{
$Data = Get-ChildItem "\\$($computer.Name)\d$\Users" -ErrorAction SilentlyContinue | Sort-Object -Property LastWriteTime -Descending -ErrorAction SilentlyContinue | Select-Object -Property Name, LastWriteTime -First 1 -ErrorAction SilentlyContinue
if($Data.LastWriteTime.AddDays($InactiveDays) -lt $Today){
$Entry.Inactive = "True"
} else {
$Entry.Inactive = "False"
}
} catch {
$Data = #{"Name"="";"LastWriteTime"=""}
}
$Entry.ComputerName = $Computer.Name
$Entry.LastUser = $Data.Name
$Entry.LastLogon = $Data.LastWriteTime
$CSVArray += $Entry
}
$CSVArray | Export-CSV -Path c:\temp\lastlogintime.csv -Append -force
A big timeloss is the way you expand your array.
This creates a new empty Array:
$CSVArray = #()
Inside your loop you keep adding to that array:
$CSVArray += $Entry
Because an array has a fixed size by design, powershell copies the entire array to a new array with an index greater by 1 compared to the old array.
And since you are doing this inside a loop, it does this for each loop, and it keeps getting slower each time.
if you have to add to an array, use a linked list:
# declaration
$CSVArray = New-Object System.Collections.ArrayList
# Adding Entry to List
[void]$CSVArray.Add($Entry)
The .Add() method returns the current Count of the List.
To suppress this, is use [void] infront of the command.
This can save valueable time compared to the powershell CmdLet Out-Null,
especially in a big loop.
Here is a piece of code that shows the difference in executiontime:
$ArrayList = New-Object System.Collections.ArrayList
$Array = #()
Measure-Command -Expression { 0..10000 | % { [void]$ArrayList.Add($_) } } |
Select-Object -ExpandProperty TotalMilliseconds |
ForEach-Object { "ArrayList with [void] finished in:`t`t`t$($_) ms" }
Measure-Command -Expression { 0..10000 | % { $ArrayList.Add($_) | Out-Null } } |
Select-Object -ExpandProperty TotalMilliseconds |
ForEach-Object { "ArrayList piped to Out-Null finished in:`t$($_) ms" }
Measure-Command -Expression { 0..10000 | % { $Array += $_ } } |
Select-Object -ExpandProperty TotalMilliseconds |
ForEach-Object { "Array expanding with += finished in:`t`t$($_) ms" }
ArrayList with [void] finished in:               48.0285 ms
ArrayList piped to Out-Null finished in:   274.6679 ms
Array expanding with += finished in:      1766.2551 ms
EDIT
Do not Import all of VmWare.PowerCli.
The import is rather slow, instead execute each PowerCLI command that you use in the script in a new clean shell.
Afterwards run
Get-Module Vmware*
to see witch modules were auto imported and are realy needed.
Then import only those Modules at the start of your script:
Import-Module VmWare.Vim.Core,VmWare.Vim.Sdk,VMWare.WHATEVER...

Powershell - trying to merge 2 result in 1 txt/csv

I'm trying to make a daily script to check status of list of URLS and pinging servers.
I've tried to combine the csv, however, the output of $status code is different from the one in csv
$pathIn = "C:\\Users\\test\\Desktop\\URLList.txt"
$URLList = Get-Content -Path $pathIn
$names = gc "C:\\Users\\test\\Desktop\\hostnames.txt"
#status code
$result = foreach ($uri in $URLList) {
try {
$res = Invoke-WebRequest -Uri $uri -UseBasicParsing -DisableKeepAlive -Method Head -TimeoutSec 5 -ErrorAction Stop
$status = [int]$res.StatusCode
}
catch {
$status = [int]$_.Exception.Response.StatusCode.value__
}
# output a formatted string to capture in variable $result
"$status - $uri"
}
$result
#output to log file
$result | Export-Csv "C:\\Users\\test\\Desktop\\Logs.csv"
#ping
$output = $()
foreach ($name in $names) {
$results = #{ "Host Name" = $name }
if (Test-Connection -Computername $name -Count 5 -ea 0) {
$results["Results"] = "Up"
}
else {
$results["Results"] = "Down"
}
New-Object -TypeName PSObject -Property $results -OutVariable nameStatus
$output += $nameStatus
}
$output | Export-Csv "C:\\Users\\test\\Desktop\\hostname.csv"
#combine the 2 csvs into 1 excel file
$path = "C:\\Users\\test\\Desktop" #target folder
cd $path;
$csvs = Get-ChildItem .\*.csv
$csvCount = $csvs.Count
Write-Host "Detected the following CSV files: ($csvCount)"
foreach ($csv in $csvs) {
Write-Host " -"$csv.Name
}
Write-Host "--------------------"
$excelFileName = "daily $(get-Date -Format dd-MM-yyyy).xlsx"
Write-Host "Creating: $excelFileName"
foreach ($csv in $csvs) {
$csvPath = ".\" + $csv.Name
$worksheetName = $csv.Name.Replace(".csv", "")
Write-Host " - Adding $worksheetName to $excelFileName"
Import-Csv -Path $csvPath | Export-Excel -Path $excelFileName -WorkSheetname $worksheetName
}
Write-Host "--------------------"
cd $path;
Get-ChildItem \* -Include \*.csv -Recurse | Remove-Item
Write-Host "Cleaning up"
Output in PowerShell
200 - https://chargebacks911.com/play-404/
200 - https://www.google.com/
500 - httpstat.us/500/
Host Name Results
----------------
x.x.x.x Down
x.x.x.x Up
Detected the following CSV files: (2)
- daily 26-03-2022.csv
- Logs.csv
--------------------
Creating: daily26-03-2022.xlsx
- Adding daily 26-03-2022 to daily26-03-2022.xlsx
- Adding Logs to daily26-03-2022.xlsx
--------------------
Cleaning up
\----------------------------------
result in excel
\#Hostname
Host Name Results
x.x.x.x Down
x.x.x.x Up
\#Logs
Length
42
29
22
I would like to know
how to correct the output in "Logs" sheet
if there's anyway to simplify my script to make it cleaner
Welcome to SO. You're asking for a review or refactoring of your complete script. I think that's not how SO is supposed be used. Instead you may focus on one particular issue and ask about a specific problem you have with it.
I will focus only on the part with the query of the status of your servers. You should stop using Write-Host. Instead you should take advantage of PowerShells uinique feature - working with rich and powerful objects instead of stupid text. ;-)
I'd approach the task of querying a bunch of computers like this:
$ComputernameList = Get-Content -Path 'C:\Users\test\Desktop\hostnames.txt'
$Result =
foreach ($ComputerName in $ComputernameList) {
[PSCustomObject]#{
ComputerName = $ComputerName
Online = Test-Connection -ComputerName $ComputerName -Count 1 -Quiet
}
}
$result
Now you have a PowerShell object you can pipe to Export-Csv for example or use it for further steps.
For example filter for the offline computers:
$result | Where-Object -Property Online -NE -Value $true
If you insist to have a visual control during the runtime of the script you may use Write-Verbose or Write-Debug. This way you can switch on the output if needed but omit it when the script runs unattended.

Powershell Error handling not not working as expected with functions

Looking for advice on error handling in Powershell. I think I understand the concept behind using Try/Catch but I'm struggling on where to utilize this in my scripts or how granular I need to be.
For example, should I use the try/catch inside my functions and if so, should I insert the actions of my function inside the try or do I need to break it
down further? OR, should I try to handle the error when I call my function? Doing something like this:
Try{
Get-MyFunction
} catch{ Do Something"
}
Here's an example of a script I wrote which is checking for some indicators of compromise on a device. I have an application that will launch this script and capture the final output. The application requires the final output to be in the following format so any failure should generate this.
[output]
result=<0 or 1>
msg= <string>
Which I'm doing like this:
Write-Host "[output]"
Write-Host "result=0"
Write-Host "msg = $VariableContainingOutput -NoNewline
Two of my functions create custom objects and then combine these for the final output so I'd like to capture any errors in this same format. If one function generates an error, it should record these and continue.
If I just run the code by itself (not using function) this works but with the function my errors are not captured.
This needs to work on PowerShell 2 and up. The Add-RegMember and Get-RegValue functions called by this script are not shown.
function Get-ChangedRunKey {
[CmdletBinding()]
param()
process
{
$days = '-365'
$Run = #()
$AutoRunOutput = #()
$RunKeyValues = #("HKLM:\Software\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run",
"HKU:\S-1-5-21-*\Software\Microsoft\Windows\CurrentVersion\Run",
"HKU:\S-1-5-21-*\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run"
)
Try{
$Run += $RunKeyValues |
ForEach-Object {
Get-Item $_ -ErrorAction SilentlyContinue |
Add-RegKeyMember -ErrorAction SilentlyContinue |
Where-Object {
$_.lastwritetime -gt (Get-Date).AddDays($days)
} |
Select-Object Name,LastWriteTime,property
}
if ($Run -ne $Null)
{
$AutoRunPath = ( $Run |
ForEach-Object {
$_.name
}
) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:"
$AutoRunValue = $AutoRunPath |
Where-Object {
$_ -and $_.Trim()
} |
ForEach-Object {
Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
}
}
#Build Custom Object if modified Run keys are found
if($AutorunValue -ne $null)
{
foreach ($Value in $AutoRunValue) {
$AutoRunOutput += New-Object PSObject -Property #{
Description = "Autorun"
path = $Value.path
value = $Value.value
}
}
}
Write-Output $AutoRunOutput
}catch{
$AutoRunOutput += New-Object PSObject -Property #{
Description = "Autorun"
path = "N/A"
value = "Error accessing Autorun data. $($Error[0])"
}
}
}
}
function Get-ShellIOC {
[CmdletBinding()]
param()
process
{
$ShellIOCOutput = #()
$ShellIOCPath = 'HKU:\' + '*' + '_Classes\*\shell\open\command'
Try{
$ShellIOCValue = (Get-Item $ShellIOCPath -ErrorAction SilentlyContinue |
Select-Object name,property |
ForEach-Object {
$_.name
}
) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:"
$ShellIOCDetected = $ShellIOCValue |
ForEach-Object {
Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
} |
Where-Object {
$_.value -like "*cmd.exe*" -or
$_.value -like "*mshta.exe*"
}
if($ShellIOCDetected -ne $null)
{
foreach ($ShellIOC in $ShellIOCDetected) {
$ShellIOCOutput += New-Object PSObject -Property #{
Description = "Shell_IOC_Detected"
path = $ShellIOC.path
value = $ShellIOC.value
}
}
}
Write-Output $ShellIOCOutput
}catch{
$ShellIOCOutput += New-Object PSObject -Property #{
Description = "Shell_IOC_Detected"
path = "N/A"
value = "Error accessing ShellIOC data. $($Error[0])"
}
}
}
}
function Set-OutputFormat {
[CmdletBinding()]
param()
process
{
$FormattedOutput = $AutoRunOutput + $ShellIOCOutput |
ForEach-Object {
"Description:" + $_.description + ',' + "Path:" + $_.path + ',' + "Value:" + $_.value + "|"
}
Write-Output $FormattedOutput
}
}
if (!(Test-Path "HKU:\")){
try{
New-PSDrive -PSProvider Registry -Root HKEY_USERS -Name HKU -ErrorAction Stop | Out-Null
}catch{
Write-Output "[output]"
Write-Output "result=0"
Write-Host "msg = Unable to Connect HKU drive" -NoNewline
}
}
$AutoRunOutput = Get-ChangedRunKey
$ShellIOCOutput = Get-ShellIOC
$FormattedOutput = Set-OutputFormat
Write-Output "[output]"
if ($FormattedOutput -eq $Null)
{
Write-Output "result=0"
Write-Host "msg= No Items Detected" -NoNewline
}
else
{
Write-Output "result=1"
Write-Host "msg=Items Detected: $($FormattedOutput)" -NoNewline
}
You have to know that there are 2 error types in PowerShell:
Terminating Errors: Those get caught automatically in the catch block
Non-Terminating Error: If you want to catch them then the command in question needs to be execution using -ErrorAction Stop. If it is not a PowerShell command but an executable, then you need to check stuff like the exit code or $?. Therefore I suggest wrapping your entire action in an advanced function on which you then call using -ErrorAction Stop.
Apart from that I would like to remark that PowerShell version 2 has already been deprecated. The reason for why non-terminating errors exists is because there are cases like for example processing multiple objects from the pipeline where you might not want it to stop just because it did not work for one object. And please do not use Write-Host, use Write-Verbose or Write-Output depending on the use case.

Unable to generate HTML table from Powershell function

I have got a powershell function that gets all errors and warnings from the event logs, counts the occurance of each Event ID and returns a Powershell table. THe code that does this is as follows -
function Get-EventLogs {
[System.Collections.ArrayList]$results = #()
#Do you want to enable transcript for logging all output to file?
$enableLogging = $FALSE
$ExportEnabled = $FALSE
# logging
# if you need detailed logging for troubleshooting this script, you can enable the transcript
# get the script location path and use it as default location for storing logs and results
$log = $MyInvocation.MyCommand.Definition -replace 'ps1','log'
$resultPath = $PSScriptRoot + '\'
Push-Location $resultPath
if ($enableLogging)
{
Start-Transcript -Path $log -ErrorAction SilentlyContinue
Write-Host "Logging enabled..."
Write-Host
Write-Host "Powershell version"
$PSVersionTable.PSVersion
$Host.Version
(Get-Host).Version
Write-host
}
$currentCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
# Configuration
# selected Event log sources
# limit the feedback only to the following logs
$EventLogList = 'System','Application'
#for FIM Health Check the Security log has less signification value and is skipped
#add it to the eventlog list when you need it
#,'Security'
# if the event logs are containing too much data,
#collect x years of logs
$startdate = ((Get-Date).AddDays(-7))
$Count = 0
$Activity = "Checking log properties"
$allEvents = New-Object System.Collections.Hashtable
$Count = 0
$Activity = "Checking log detaills"
foreach ($eventlog in $EventLogList)
{
#$Count += 1
$pct = ($Count / $EventLogList.Count * 100)
$status = $EventLog + " (" + $Count + "/" + $EventLogList.Count +")"
Write-Progress -Activity $Activity -Status $status -PercentComplete $pct
write-host $count"." $eventlog
[System.Threading.Thread]::CurrentThread.CurrentCulture = New-Object "System.Globalization.CultureInfo" "en-US"
# query the event log
# store the data in a hashtable, to avoid new queries
$allEvents = $Null
$allEvents = Get-WinEvent -FilterHashtable #{logname=$eventlog;StartTime=$startdate;level=0,1,2,3,4,5} -ErrorAction SilentlyContinue
#DEBUG if ($eventlog -eq "Application") { $allevents}
if ($allEvents.count -eq 0)
{
$message = "No events for " + $eventlog + "log since "+ $startdate + "."
Write-Host $message
Write-Host
# no data to process, skip processing for current loop
Continue
}
#For events detailed reporting we're only interested in error events
#not interested in informational events (level 0 and 4)
$evtStats = $allEvents | where -Property level -Notin -Value 0,4 | Group-Object id | Sort-Object Count -Descending
$allevents = $Null
#prep export
$exportfile = $resultPath + "_EventIDStats"+ $eventlog + ".csv.txt"
if ($exportEnabled) {$export | Export-Csv $exportfile -NoTypeInformation}
# evtStats has number and ID attribute
# other attributes must be added:
# - errortype name
# - Source
# - errortype name
# for each event id in the event statistics
# display the most recent event
$Activity = "Looking up last event occurrence..."
$i= 0
foreach ($item in $evtStats)
{
$i += 1
$pct = ($i / $evtStats.Count * 100)
$eventID = $item.Name
$status = "EventID: "+ $item.Name
Write-Progress -Activity $Activity -Status $status -PercentComplete $pct
$customobj = "" | select Count,ErrorID,ErrorType,Message
$customobj.Count = $item.Count
$customobj.ErrorID = $item.Name
#get most recent event from the eventID
$id = $item.Name.ToInt32($Null)
[System.Threading.Thread]::CurrentThread.CurrentCulture = New-Object "System.Globalization.CultureInfo" "en-US"
$lastevent = get-winevent -FilterHashtable #{LogName=$eventlog;Id=$id} -MaxEvents 1 -ErrorAction SilentlyContinue
#depending on local settings, query might fail, if it fails reset to local culture
if ($lastevent.LevelDisplayName.Length -eq 0)
{
[System.Threading.Thread]::CurrentThread.CurrentCulture = $currentCulture
$lastevent = get-winevent -FilterHashtable #{LogName=$eventlog;Id=$id} -MaxEvents 1
}
$customobj.ErrorType = $lastevent.LevelDisplayName
$customobj.Message = $lastevent.Message
#prep EventID export
$exportfile = $resultPath + $eventlog +'_EventID_' + $customobj.ErrorID + ".csv.txt"
if ($exportEnabled)
{
$customobj | Export-Csv $exportfile -NoTypeInformation
}
$results += $customobj
}
#display with format
$results | Format-Table -AutoSize
return ,$results
if ($exportEnabled)
{
$exportfile = $resultPath + "_lastEvents_short_" + $eventlog + ".txt"
$results| Format-Table -AutoSize | out-file $exportfile
$exportfile = $resultPath + "_lastEvents_detail_" + $eventlog + ".txt"
$results | out-file $exportfile
}
}
#Write-Output $results | Format-Table -AutoSize
}
I then need to format the output of this function as a HTML table, so I can show it in my report.
$elogs = (Get-EventLogs) | Out-String
The code above outputs it as a long string (to be expected). But I cannot figure out how to format it as a HTML table. THe code that finally generates the report is as follows -
$Report = ConvertTo-Html -Title "$Computername" `
-Head "<center><h1>Server Report for <br><br>$Computername</h1></center><br /><br />" `
-Body "$Heading $Hardware $elogs $PercentFree $Output $Restarted $Services $Stopped $Css" }
Is there a way to output this as a table?
ConvertTo-Html is able to take an object and render that object as a HTML table. For simplicity's sake, I'll include the -Fragment switch that will ONLY return a HTML table (not all the HTML header, etc.).
Take a simple object, such as some basic process data, and you can create a HTML table:
[PS] $p = Get-Process | Select -First 3 -Property Name,ID
[PS] $p | ConvertTo-Html -Fragment
<table>
<colgroup><col/><col/></colgroup>
<tr><th>Name</th><th>Id</th></tr>
<tr><td>Process1</td><td>1888</td></tr>
<tr><td>Process2</td><td>1536</td></tr>
<tr><td>Process3</td><td>13920</td></tr>
</table>
Note that we don't need to specify a -Body with all the variables; we just need to make sure that we have all the data collected in an object (with the appropriate property names) and the cmdlet does the rest.
Going back to your script, there are a few things to look at:
Don't use Write-Host. If you want debug messages, consider using Write-verbose and Write-Debug. You'll need to set the $VerbosePreference and $DebugPreference variables to "Continue" to make this output visible.
Similarly, avoid using Format-Table
Your script returns an object ,$results. You should be able to pipe this into ConvertTo-Html with additional header and body content.
Note finally that the -Body switch doesn't format your object; it just includes extra HTML in front of the HTML table.

How can I modify this script to update Taxonomy Fields in SharePoint 2010 with a CSV?

Here is the script I have so far. I know it is close. I am trying to update a list of items using a csv file. I cannot however update the Taxonomy field. I am trying to avoid requiring the guid and term ID for it to run.
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction Inquire
#Read the CSV file
$CSVData = Import-CSV -path "C:\files\testimport.csv"
#Get the Web
$web = Get-SPWeb -identity "http://site/Policies/"
$web.AllowUnsafeUpdates = $true;
Write-Host ( $web.Lists | Format-Table | Out-String)
#Get the Target List
$list = $web.Lists["Documents"]
$item = $list.GetItemById(4)
#$newTerm = "Test Data|074bbed9-da60-4a0c-9691-344436b5f96a"
$metadataField = ($item["Metadata"] -as [Microsoft.SharePoint.Taxonomy.TaxonomyFieldValueCollection])
#$metadataValue = New-Object -TypeName Microsoft.SharePoint.Taxonomy.TaxonomyFieldValueCollection -ArgumentList ""
#$metadataValue.PopulateFromLabelGuidPair("TestData");
#$metadataField.Add($metadataValue);
forEach ($line in $CSVData){
$item["Department"] = $($line.Department);
$item["Topics"] = $($line.Topics);
$item["History"] = $($line.History);
$metadataField = "Test Program|074bbed9-da60-4a0c-9691-344436b5f96a";
}
Write-Host ($item | Format-List | Out-String)
Write-Host ($metadata | Format-List | Out-String)
$item.Update();
#$list.Update();
You need to get the reference to the term and then set the value of the taxonomy field using TaxonomyField.SetFieldValue method. Please refer to this article for more details - Updating SharePoint Managed Metadata Columns with PowerShell