Friday, May 8, 2020

Dear azure devops team 2020-05-08


Dear azure devops team,

I would like to report a bug

In the web page for azure devops, everytime I click on a project name, the name move at the top of the list and move all the other items down the list.

It cannot be a feature. No way. Somewhere, someone is laughting very hard.

Or it might also be very serious...

Was it was made by "10 second Tom" from the movie "50 first dates"?

If it is the case I forgive him.

If not, if you ever meet the guy who made every clicked project move to the top of the menu, will you please nickname him: "10 seconds Tom" for me?


Monday, March 9, 2020

powershell recursive html report with jagged array of psobjects

Recursive html report (made with jagged array of psobject)

When you print, header and footer will repeat each page

Requis: google chrome (for printing)

At the moment it scan for windows code and office code and put it in the report


#########################################################################
$title01 = "report psobject html xml jagged recursive colored"
#########################################################################
cls
# this recursive script will:
# take a psobject with sub psobjects that contain data about a report
# put the main psobject in a html table, all sub psobject are replaced with a text
# color any html cell we want to warn about certains things (errors, warnings) in main html/xml hybrid object and all sub html/xml objects
# convert back the main html/xml table into an html report
# insert back all sub html/xml tables (once they were colored)
# generate final html report, printable in google chrome only, with a table format and sheet format compatible for print with header and footer

#########################
# parameters
#########################
# scan office keys
#$computers01 = @("mav03", "mav06", "mav09", "mav10", "mav14", "mav16", "mav18", "mav19")
$computers_local01 = @("localhost","caca")

###################################
# requis
###################################
### Enable-PSRemoting

#powershell.exe -command Enable-PSRemoting -SkipNetworkProfileCheck -force
#Enable-PSRemoting -SkipNetworkProfileCheck -force

#Test-WSMan -ComputerName <IP or host name>

# on HOST ########################################## tooo!
# Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*'

#############################################################################
# color for html report if this string is found, html td color is changed
#############################################################################
# possible colors are defined in $style01
[string[]]$error_message_arr = "WARNING"
[string[]]$error_color_arr = "yellow" # yellow
[string[]]$error_message_Arr+= "ERROR"
[string[]]$error_color_arr+= "red" # red
[string[]]$error_message_arr += "cacasss"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_arr += "00:00:00"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_arr += "inprogress"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_arr += "pending"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_Arr+= "failed"
[string[]]$error_color_arr+= "red" # red
[string[]]$error_message_Arr+= "true"
[string[]]$error_color_arr+= "red" # red
[string[]]$error_message_Arr+= "unknow"
[string[]]$error_color_arr+= "red" # red

###############################
# error management object
###############################
$error01 = New-Object PsObject
$error01 | Add-Member NoteProperty -Name number -value ''
$error01 | Add-Member NoteProperty -Name description -value ''
$error01 | Add-Member NoteProperty -Name error02 -value ''

#######################
# logfile
#######################
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$scriptname = split-path -leaf $MyInvocation.MyCommand.Definition
$Logfilename = $scriptname + "_log.txt"
$logfile = $scriptPath + "\" + $Logfilename
$logall = 1

$msg01 = "START " + $title01
write-host $msg01 -ForegroundColor Green
if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile}catch{}}

#######################################################################
# get domain name to check for hyper-v or esx vmware on each client
#######################################################################
# (Get-WmiObject Win32_ComputerSystem).Domain
$domain01 = (Get-WmiObject Win32_ComputerSystem).Domain

#################################
# function color xml html table
#################################

function color_xml_table([xml]$ttable)
{
    for($trcnt02=1; $trcnt02 -le $ttable.table.tr.count; $trcnt02++)
    {
        for($tdcnt02=1; $tdcnt02 -le $ttable.table.tr[$trcnt02].td.Count; $tdcnt02++)
        {
            #write-host "td" $ttable.table.tr[$trcnt02].td.Count
           
            # having 1 item reverse y and x in a table
            if($ttable.table.tr[$trcnt02].td.Count -eq 1)
            {
                # cannot index element if there is only one
                $item03 = $ttable.table.tr[$trcnt02].td
            }
            else
            {
                $item03 = $ttable.table.tr[$trcnt02].td[$tdcnt02]
            }

            for($errorcnt = 0; $errorcnt -lt $error_message_arr.count; $errorcnt++)
            {
                $error_message = $error_message_arr[$errorcnt]
                if($item03 -like "*$error_message*")
                {
                    #write-host "changed color" $error_color_arr[$errorcnt] -ForegroundColor Yellow
                   
                    # having 1 item reverse y and x in a table
                    if($ttable.table.tr[$trcnt02].td.Count -eq 1)
                    {
                        $ttable.table.tr[$trcnt02].ChildNodes.SetAttribute('class',$error_color_arr[$errorcnt])
                    }
                    else
                    {
                        try
                        {
                        $ttable.table.tr[$trcnt02].ChildNodes[$tdcnt02].SetAttribute('class',$error_color_arr[$errorcnt])
                        }
                        catch{}
                                       
                    }
                }
            }
        }
    }
    return $ttable
}

function ConvertTo-ProductKey
{
    <# 
    .SYNOPSIS 
        Converts registry key value to windows product key.
       
    .DESCRIPTION 
        Converts registry key value to windows product key. Specifically the following keys:
            SOFTWARE\Microsoft\Windows NT\CurrentVersion\DigitalProductId
            SOFTWARE\Microsoft\Windows NT\CurrentVersion\DigitalProductId4
       
    .PARAMETER Registry
        Either DigitalProductId or DigitalProductId4 (as described in the description)
       
    .NOTES 
        Author: Zachary Loeber
        Original Author: Boe Prox
        Version: 1.0
         - Took the registry setting retrieval portion from Boe's original script and converted it
           to this basic conversion function. This is to be used in conjunction with my other
           function, get-remoteregistryinformation
   
    .EXAMPLE
     PS > $reg_ProductKey = "SOFTWARE\Microsoft\Windows NT\CurrentVersion"
     PS > $a = Get-RemoteRegistryInformation -Key $reg_ProductKey -AsObject
     PS > ConvertTo-ProductKey $a.DigitalProductId
   
            XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
           
     PS > ConvertTo-ProductKey $a.DigitalProductId4 -x64
   
            XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
       
        Description
        -----------
        Retrieves the product key information from the local machine and converts it to a readible format.
    #>     
    [cmdletbinding()]
    param (
        [parameter(Mandatory=$True,Position=0)]
        $Registry,
        [parameter()]
        [Switch]$x64
    )
    begin {
        $map="BCDFGHJKMPQRTVWXY2346789"
    }
    process {
        $ProductKey = ""

        $prodkey = $Registry[0x34..0x42]

        for ($i = 24; $i -ge 0; $i--)
        {
            $r = 0
            for ($j = 14; $j -ge 0; $j--)
            {
                $r = ($r * 256) -bxor $prodkey[$j]
                $prodkey[$j] = [math]::Floor([double]($r/24))
                $r = $r % 24
            }
            $ProductKey = $map[$r] + $ProductKey
            if (($i % 5) -eq 0 -and $i -ne 0)
            {
                $ProductKey = "-" + $ProductKey
            }
        }
        $ProductKey
    }
}

###############################################
# get all programs installed on the computer
###############################################
function getallprograms01($vm_name01)
{

    $result01 = @()

    $msg01 = "PROGRAM (getting 64 bits programs) pssession opened"
    if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}

    $remote_command01 = "try {Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, @{Name = 'type'; Expression = {`"64 bits`"}} } catch {throw `$_}"
               
    $remote_command_block01 = [scriptblock]::Create($remote_command01)

    $job_program64 = Invoke-Command -asjob -Session $s2 -ScriptBlock $remote_command_block01 -ErrorAction stop | Get-Job
    $results01 = $job_program64| Wait-Job -timeout $jobtimeout01 | Receive-Job
    $job_program64 | remove-job


    $msg01 = "PROGRAM (getting 32 bits programs) pssession opened"
    if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}

    $remote_command01 = "try {Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, @{Name = 'type'; Expression = {`"32 bits`"}} } catch {throw `$_}"
               
    $remote_command_block01 = [scriptblock]::Create($remote_command01)

    $job_program32 = Invoke-Command -asjob -Session $s2 -ScriptBlock $remote_command_block01 -ErrorAction stop | Get-Job
    $results01 += $job_program32 | Wait-Job -timeout $jobtimeout01 | Receive-Job
    $job_program32 | remove-job

    return $result01

}

#########################################
# clean up bad caracter in psobject
#########################################

function psobject_cleanup01($psobject01)
{
    #write-host "cleaning"
    # get properties names of this object (columns names)
    $properties = $null
    $properties = $psobject01 | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name

    foreach($line in $psobject01)
    {
        for($elecnt02 = 0; $elecnt02 -lt $properties.count; $elecnt02++)
        {
            # when encountering a replaced sub object, we dont care if there is an error because it's a string that will be replaced later anyway by a table
            try{$line.($properties[$elecnt02]) = $line.($properties[$elecnt02]).tostring()}catch{}
            try{$line.($properties[$elecnt02]) = $line.($properties[$elecnt02]) -replace '[^a-zA-Z0-9 ,.!"/$%?&()_+=-]', ''}catch{}
            $line.($properties[$elecnt02]) = $line.($properties[$elecnt02]) -replace '[^a-zA-Z0-9 ,.!"/$%?&()_+=-]', ''
        }
    }
    return $psobject01
}

function report_object_replacements01
{
    param($object01)
    # will detect object within array
    # substitute them a text
    # at final step, with a xml manipulation, replace all text with a html table so the report can have subtables
   
    $global:recurse_level01 = 0
    $global:item_to_replace_index01 = 0
    $global:replace_arr01 = @()

    #convert to html
    #$object01 = $args[0]

    $file10 = $scriptPath + "\" + $scriptname + ".htm"

    $msg01 = "Type level 0: " + $object01.GetType().Name
    #write-host $msg01
    if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}       
   
    if($object01.GetType().Name -eq "Object[]")
    {
        $object_total = 0
        for($elecnt = 0; $elecnt -lt $object01.count; $elecnt++)
        {
            #$elecnt
       
            $msg01 = ""
            if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}       
       
            if($object01[$elecnt].GetType().Name -eq "Object[]")
            {
                $object_total++
                $msg01 = "Object, calling recurse from main loop"
                write-host $msg01 -ForegroundColor Yellow
                $object01[$elecnt] = report_object_replacements_recursive01($object01[$elecnt])
            }
            else
            {
                #write-host "cleaning: " $object01[$elecnt] -ForegroundColor Magenta
                $object01[$elecnt] = psobject_cleanup01($object01[$elecnt])
                # this is not an object, we do not recurse
                #$msg01 = "not an object"; write-host $msg01 -ForegroundColor Green
            }
        }
       
        #if($object_total -eq 0)
        #{
        #    write-host "cleanup non-recursive"
        #    $object01 = psobject_cleanup01($object01)
        #}
    }
    else
    {
        write-host "ERROR report_html_with_subtables The first param is not an object[]" -ForegroundColor Red
    }


    $htmlfile01 = $object01 | ConvertTo-Html -Head $header
    $htmlfile01 | Out-File $file10
    #write-host "type: " $htmlfile01.GetType().Name
    #write-host $file10
    return $file10

}

function report_object_replacements_recursive01($object02)
{
    $msg01 = "In recursive level: " + $global:recurse_level01
    write-host $msg01
    if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}
   
    if($object02.GetType().Name -eq "Object[]")
    {
        $global:recurse_level01++
        $object_total = 0

        for($elecnt01 = 0; $elecnt01 -lt $object02.count; $elecnt01++)
        {
            if($object02[$elecnt01].GetType().Name -eq "Object[]")
            {
                $object_total++
                $msg01 = "Object, calling recurse from recurse"
                write-host $msg01 -ForegroundColor Yellow
                #$global:replace_arr01+= $object02[$elecnt01]
                $object02[$elecnt01] = report_object_replacements_recursive01($object02[$elecnt01])
                #$object02[$elecnt01] = psobject_cleanup01($object02[$elecnt01])
            }
            else
            {
                #write-host "cleaning recursive: " $object02[$elecnt01] -ForegroundColor Magenta
                $object02[$elecnt01] = psobject_cleanup01($object02[$elecnt01])
                #$msg01 = "not an object"; write-host $msg01 -ForegroundColor Green
                #$object02[$elecnt01] = psobject_cleanup01($object02[$elecnt01])
            }
        }

        $replacement_text01 = "replace with" + $global:item_to_replace_index01
        $replacement01 = New-Object PsObject
        $replacement01 | Add-Member NoteProperty -Name id -value $replacement_text01

        $msg01 = "Replacing sub table with replacement text: " + $replacement_text01
        #write-host $msg01 -ForegroundColor Cyan
        $global:replace_arr01+= ,$object02
        $object02 = $replacement01
        $global:item_to_replace_index01++

        #write-host $object02 -ForegroundColor Magenta
    }
    else
    {
        $global:recurse_level01--
        write-host "not an object RECURSE CALLED FOR NOTHING" -ForegroundColor Green
    }
    return $object02

}

function replace_sub_table_xml($xml02)
{
    for($trcnt=0;$trcnt -lt $xml02.table.tr.count;$trcnt++)
    {
        ################################################################
        # change color if certain keywords are found in html cell
        ################################################################
        for($tdcnt=0;$tdcnt -lt $xml02.table.tr[$trcnt].td.Count;$tdcnt++)
        {
            # cannot index element if there is only one
            if($xml02.table.tr[$trcnt].td.Count -eq 1)
            {
                # cannot index element if there is only one
                $item02 = $xml02.table.tr[$trcnt].td
            }
            else
            {
                $item02 = $xml02.table.tr[$trcnt].td[$tdcnt]
            }


            ####################
            # color item
            ####################
            for($errorcnt = 0; $errorcnt -lt $error_message_arr.count; $errorcnt++)
            {
                $error_message = $error_message_arr[$errorcnt]
                if($item02 -like "*$error_message*")
                {
                    #write-host "changed color" $error_color_arr[$errorcnt] -ForegroundColor Yellow
                   
                    # cannot index element if there is only one
                    if($xml02.table.tr[$trcnt].td.Count -eq 1)
                    {
                        $xml02.table.tr[$trcnt].ChildNodes.SetAttribute('class',$error_color_arr[$errorcnt])
                    }
                    else
                    {
                        try
                        {
                        $xml02.table.tr[$trcnt].ChildNodes[$tdcnt].SetAttribute('class',$error_color_arr[$errorcnt])
                        }
                        catch{}
                                       
                    }
                }
            }

            if($item02 -like "*replace with*")
            {
                #write-host "....... xml" -ForegroundColor Red
                $replace_index01 = $item02.split("replace with")[$item02.split("replace with").GetUpperBound(0)]
                #write-host "replace index01: " $replace_index01 -ForegroundColor Blue
                #write-host "replacement: " $global:replace_arr01[$replace_index01]
                #$test = $global:replace_arr01[$replace_index01] | ConvertTo-Html -fragment
                [xml]$dummyxml = $global:replace_arr01[$replace_index01] | ConvertTo-Html -fragment
                [xml]$dummyxml = replace_sub_table_xml_recurse([xml]$dummyxml)

                # RECURSE HERE if there is a sub text for replacement
                $xml02.table.tr[$trcnt].ChildNodes[$tdcnt].innertext = ""
                $imported = $xml02.ImportNode($dummyxml.DocumentElement, $true)
                ### prod (sub table in column 1)
                #$xml02.table.tr[$trcnt].ChildNodes[$tdcnt+1].AppendChild($imported) | out-null
                ### test (sub table in last column)
                $xml02.table.tr[$trcnt].ChildNodes[($xml02.table.tr[$trcnt].td.Count)-1].AppendChild($imported) | out-null
            }

        }
    }
    return $xml02
}

function replace_sub_table_xml_recurse($xml01)
{
    for($trcnt=0;$trcnt -lt $xml01.table.tr.count;$trcnt++)
    {
        #write-host "recurse xml" -ForegroundColor Red
        ################################################################
        # change color if certain keywords are found in html cell
        ################################################################
        for($tdcnt=0;$tdcnt -lt $xml01.table.tr[$trcnt].td.Count;$tdcnt++)
        {
            # cannot index element if there is only one
            if($xml01.table.tr[$trcnt].td.Count -eq 1)
            {
                $item03 = $xml01.table.tr[$trcnt].td
            }
            else
            {
                $item03 = $xml01.table.tr[$trcnt].td[$tdcnt]
            }

            ####################
            # color item
            ####################
            for($errorcnt = 0; $errorcnt -lt $error_message_arr.count; $errorcnt++)
            {
                $error_message = $error_message_arr[$errorcnt]
                if($item03 -like "*$error_message*")
                {
                    #write-host "changed color" $error_color_arr[$errorcnt] -ForegroundColor Yellow
                   
                    # cannot index element if there is only one
                    if($xml01.table.tr[$trcnt].td.Count -eq 1)
                    {
                        $xml01.table.tr[$trcnt].ChildNodes.SetAttribute('class',$error_color_arr[$errorcnt])
                    }
                    else
                    {
                        try
                        {
                        $xml01.table.tr[$trcnt].ChildNodes[$tdcnt].SetAttribute('class',$error_color_arr[$errorcnt])
                        }
                        catch{}
                                       
                    }
                }
            }

            if($item03 -like "*replace with*")
            {
                #write-host "recurse xml" -ForegroundColor Red
                #write-host "recurse xml" -ForegroundColor Red
                $replace_index01 = $item03.split("replace with")[$item03.split("replace with").GetUpperBound(0)]
                #write-host "replace index01: " $replace_index01 -ForegroundColor Blue
                #write-host "replacement: " $global:replace_arr01[$replace_index01]
                #$test = $global:replace_arr01[$replace_index01] | ConvertTo-Html -fragment
                [xml]$dummyxml = $global:replace_arr01[$replace_index01] | ConvertTo-Html -fragment
                [xml]$dummyxml = replace_sub_table_xml_recurse([xml]$dummyxml)

                # RECURSE HERE if there is a sub text for replacement
                $xml01.table.tr[$trcnt].ChildNodes[$tdcnt].innertext = ""
                $imported = $xml01.ImportNode($dummyxml.DocumentElement, $true)
                #$xml01.table.tr[$trcnt].ChildNodes[$tdcnt+1].AppendChild($imported) | out-null
                ### test (sub table in last column)
                $xml01.table.tr[$trcnt].ChildNodes[($xml01.table.tr[$trcnt].td.Count)-1].AppendChild($imported) | out-null
            }

        }
    }
    return $xml01
 
}

function papersize01($size01, $orientation01)
{
    #write-host "orientation: " $orientation01 -ForegroundColor Yellow
    $paper01 = New-Object PsObject
    $paper01 | Add-Member NoteProperty -Name orientation -value ''
    $paper01 | Add-Member NoteProperty -Name height -value ''
    $paper01 | Add-Member NoteProperty -Name width -value ''
    $paper01 | Add-Member NoteProperty -Name size -value ''

    If($size01 -eq "ledger" -Or $size01 -eq "11x17" -Or $size01 -eq "tabloid" -Or $size01 -eq "tabloïd")
    #Tabloid / US, B / ANSI B   11 x 17 279 x 432
    {
        $paper01.size = "tabloid"
        If ($orientation01 -eq "portrait")
        {
            $paper01.orientation = "portrait"
            $paper01.height01 = "432"
            $paper01.width01 = "279"
        }
        Else
        {
            $paper01.orientation = "landscape"
            $paper01.height = "279"
            $paper01.width = "432"
        }
    }
    ElseIf($size01 -eq "letter" -Or $size01 -eq "lettre" -Or $size01 -eq "us letter")
    {
        $paper01.size = "us letter"
       
        If ($orientation01 -eq "portrait")
        {
            $paper01.orientation = "portrait"
            $paper01.height = "297"
            $paper01.width = "210"
        }
        Else
        {
           
            $paper01.orientation = "landscape"
            $paper01.height = "210"
            $paper01.width = "297"
        }
    }
    ElseIf($size01 -eq "legal")
    {
        $paper01.size = "legal"
        If ($orientation01 -eq "portrait")
        {
            $paper01.orientation = "portrait"
            $paper01.height = "356"
            $paper01.width = "210"
        }
        Else
        {
            $paper01.orientation = "landscape"
            $paper01.height = "210"
            $paper01.width = "356"
        }
    }
    Else
    {
        $paper01.size = "us letter"
        If( $orientation01 -eq "portrait")
        {
            $paper01.orientation = "portrait"
            $paper01.height = "297"
            $paper01.width = "210"
        }
        Else
        {
            $paper01.orientation = "landscape"
            $paper01.height = "210"
            $paper01.width = "297"
        }
    }
    return $paper01
}

function web_report($wrp02)
{
    ########################
    # web report
    ########################

    $paper01 = papersize01 "letter" "portrait"

    $paper01size = $paper01.size
    $paper01orientation = $paper01.orientation
    $paper01height = $paper01.height + "mm"
    $paper01width = $paper01.width + "mm"

$pagesetup01 = @"
@page
{
margin: 0.2in 0.2in 0.2in 0in;
size:$paper01size $paper01orientation;
}
HTML body
{
height:$paper01height;width:$paper01width;margin: 0.2in 0in 0in 0.2in;
}
"@

    $leftsidewidth01 = "15"
    $rightsidewidth01 = "15"
    [string]$middlewidth01 = 100 - $leftsidewidth01 - $rightsidewidth01
    ######################
    # thead
    ######################
#$thead01 = @"
#<THEAD><TR><th>
#<table width=""100%"" BORDERCOLOR=""black"" border=1 CELLSPACING=1 cellpadding=2 style='border-collapse:collapse;border:none;$wrp02.header_title_line_style01'>"
#<tr>
#<td width=""" + $leftsidewidth01 + "%""><img style='"
#"@

    $thead01 = ""
    $thead01 = $thead01 + "<THEAD>" + "`n"
    $thead01 = $thead01 + "<TR id=""t01""><th id=""t01"">" + "`n"
    #=== table inside header only
    #$thead01 = $thead01 + "<table id=""tr02"" width=""100%"" BORDERCOLOR=""black"" border=1 CELLSPACING=1 cellpadding=2 style='border-collapse:collapse;border:none;" + $wrp02.header_title_line_style01 + "'>" + "`n"
    $thead01 = $thead01 + "<table id=""tr02"" width=""100%"" CELLSPACING=1 cellpadding=2 style='" + $wrp02.header_title_line_style01 + "'>" + "`n"
    $thead01 = $thead01 + "<tr>" + "`n"
    #=== left side image
    $thead01 = $thead01 + "<td width=""" + $leftsidewidth01 + "%""><img style='"
    If($wrp02.header_left_image_percenty01 -gt 0)
    {
        $thead01 = $thead01 + "height: " + $wrp02.header_left_image_percenty01 + "%;"
    }
    If($wrp02.header_left_image_percentx01 -gt 0)
    {
        $thead01 = $thead01 + "width: " + $wrp02.header_left_image_percentx01 + "%;"
    }
    $thead01 = $thead01 + "object-fit: contain' "
    $thead01 = $thead01 + "src=""" + $wrp02.header_left_image_path01 + """ ></td>" + "`n"
    #=== title
    $thead01 = $thead01 + "<td width=""" + $middlewidth01 + "%"">"
    $thead01 = $thead01 + "<font size=""5"">" + $wrp02.header_title_line01 + "</font><br>"
    #=== second title
    $thead01 = $thead01 + "<font size=""3"">" + $wrp02.header_title_line02 + "</font></td>" + "`n"
    #=== right side image
    $thead01 = $thead01 + "<td width=""" + $rightsidewidth01 + "%""><img style='"
    If($wrp02.header_right_image_percenty01 -gt 0)
    {
        $thead01 = $thead01 + "height: " + $wrp02.header_right_image_percenty01 + "%;"
    }

    If($wrp02.header_right_image_percentx01 -gt 0)
    {
        $thead01 = $thead01 + "width: " + $wrp02.header_right_image_percentx01 + "%;"
    }
    $thead01 = $thead01 + "Object-fit: contain' "
    $thead01 = $thead01 + "src=""" + $wrp02.header_right_image_path01 + """></td>" + "`n"
    $thead01 = $thead01 + "</tr>"
    $thead01 = $thead01 + "</table>" + "`n" #=== header
    $thead01 = $thead01 + "</th></TR>" + "`n"
    $thead01 = $thead01 + "</THEAD>" + "`n"
    ############################
    # tfoot
    ############################
    $tfoot01 = ""
    $tfoot01 = $tfoot01 + "<TFOOT style='" + $wrp02.footer_style01 + "'>`n"
    $tfoot01 = $tfoot01 + "<TR id=""t01""><th id= ""t01"">"
    $tfoot01 = $tfoot01 + "<table id=""t01"" width=""100%"" BORDERCOLOR=""black"" border=1 CELLSPACING=1 cellpadding=2 style='border-collapse:collapse;border:none;" + $wrp02.footer_table_style01 + "'>" + "`n"
    #$tfoot01 = $tfoot01 + "<tr style = 'page-break-inside: avoid;'>"
    $tfoot01 = $tfoot01 + "<td width=""25%"" style='text-align: center;'>" + $wrp02.footer_left01 + "</td>" + "`n"
    $tfoot01 = $tfoot01 + "<td width=""50%"" style='text-align: center;'>" + $wrp02.footer_middle01 + "</td>" + "`n"
    $tfoot01 = $tfoot01 + "<td width=""25%"" style='text-align: center;'>" + $wrp02.footer_right01 + "</td>" + "`n"
    $tfoot01 = $tfoot01 + "</tr>"
    $tfoot01 = $tfoot01 + "</table>"
    $tfoot01 = $tfoot01 + "</th></TR>"
    $tfoot01 = $tfoot01 + "</TFOOT>" + "`n"

    ############################
    # html header
    ############################

# not used anymore
#$header = @"
#<style>$pagesetup01
#BODY{background-color:white;}
#TABLE{page-break-after: always;border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}
#TH{border-width: 1px;padding: 5px;border-style: solid;border-color: black;foreground-color: black;background-color: LightBlue}
#TD{border-width: 1px;padding: 5px;border-style: solid;border-color: black;foreground-color: black;background-color: white}
#.green{background-color:#d5f2d5}
#.blue{background-color:#e0eaf1}
#.red{background-color:#ffd7de}
#.yellow{background-color:#ffff00}
#.orange{background-color:#ffa500}
#</style>
#"@

#tr02{border-bottom:1px solid black;border-right:1px solid black;background-color: LightBlue;border-width: 1px;padding: 5px;border-style: solid;border-color: black;}
#td02{border-bottom:1px solid black;border-right:1px solid black;background-color: LightBlue;border-width: 1px;padding: 5px;border-style: solid;border-color: black;}

#tr02{border-top: 1px solid #FFFFFF;padding: 5px;border-style: solid;border-color: black;background-color: LightBlue;border-width: 1px;border-spacing: -1px;}
#td02{border-top: 1px solid #FFFFFF;padding: 5px;border-style: solid;border-color: black;background-color: LightBlue;border-width: 1px;border-spacing: -1px;}


$style01 = @"
$pagesetup01
#t01{width: 100%; border-collapse:collapse;border:none;background-color: White}
BODY{background-color:white;}
TABLE{border-spacing:0;page-break-after: always;border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}
TH{border-width: 1px;padding: 5px;border-style: solid;border-color: black;foreground-color: black;background-color: LightBlue}
TD{border-width: 1px;padding: 5px;border-style: solid;border-color: black;foreground-color: black;background-color: White}
TR{page-break-inside: avoid;}
#tr02{padding: 5px;background-color: white;border-width: 1px;border-style: solid;border-color: black;position: relative;}
.green{background-color:#d5f2d5}
.blue{background-color:#e0eaf1}
.red{background-color:#ffd7de}
.yellow{background-color:#ffff00}
.orange{background-color:#ffa500}
"@

#<table id=""t01"" width=""100%"" BORDERCOLOR=""black"" border=0 CELLSPACING=1 cellpadding=2>
#$style01 = ""

$body = @"
<style>
$style01
</style>
<table id="t01" CELLSPACING=1 cellpadding=2>
$thead01
<tbody id="DATA">
<tr id="t01"><td id="t01">
$($wrp02.data)
</td></tr>
</tbody>
$tfoot01
</table>
"@
    return $body
}

######################################################################################################################################################
# main
######################################################################################################################################################

########################################################################
# elevation Self-elevate the script (pop-up will ask for elevation)
########################################################################
if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator'))
{
    if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000)
    {
        $CommandLine = "-File `"" + $MyInvocation.MyCommand.Path + "`" " + $MyInvocation.UnboundArguments
        Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine
        Exit
    }
}

$level0_arr = @()
$level1_arr = @()
$level2_arr = @()

###############
# level 0
###############
$level0_obj = New-Object PsObject
$level0_obj | Add-Member NoteProperty -Name id -value '1'
$level0_obj | Add-Member NoteProperty -Name test -value 'test1'

$level0_arr += $level0_obj.PSObject.Copy()

$level0_obj = New-Object PsObject
$level0_obj | Add-Member NoteProperty -Name id -value '2'
$level0_obj | Add-Member NoteProperty -Name test -value 'test2'

$level0_arr += $level0_obj.PSObject.Copy()

    ############# level 1
    $level1_obj = New-Object PsObject
    $level1_obj | Add-Member NoteProperty -Name id -value '3'
    $level1_obj | Add-Member NoteProperty -Name test -value 'sub 0 test3'

    $level1_arr += $level1_obj.PSObject.Copy()

    $level1_obj = New-Object PsObject
    $level1_obj | Add-Member NoteProperty -Name id -value '4'
    $level1_obj | Add-Member NoteProperty -Name test -value 'sub 0 test4'

    $level1_arr += $level1_obj.PSObject.Copy()

        ############# level 2
        $level2_obj = New-Object PsObject
        $level2_obj | Add-Member NoteProperty -Name id -value '5'
        $level2_obj | Add-Member NoteProperty -Name test -value 'sub 1 test5'

        $level2_arr += $level2_obj.PSObject.Copy()

        $level2_obj = New-Object PsObject
        $level2_obj | Add-Member NoteProperty -Name id -value '6'
        $level2_obj | Add-Member NoteProperty -Name test -value 'sub 1 test6'

        $level2_arr += $level2_obj.PSObject.Copy()

        $level1_arr += ,$level2_arr

#############
$level0_arr +=,$level1_arr

######################################
# pssession parameters
######################################
[string]$dummy = $null
$jobtimeout01 = 3*60 #( 3 minutes)
$OpenTimeoutMSec01 = 5000
$OperationTimeoutMSec01 = 180000 # 3 minutes
$PSSessionOption01 = New-PSSessionOption -OpenTimeout $OpenTimeoutMSec01 -OperationTimeout $OperationTimeoutMSec01

$prod01 = 1
if($prod01 -eq 1)
{
    ####################################
    # get windows key from bios
    ####################################
    #$msg01 = "sub array value...: " + $level0_arr[2][2][1][0].test #= id 6 sub1 test 6
    #$msg01 = "sub array value...: " + $level0_arr[2][0][0] #= id 6 sub1 test 6
    #Format-Custom -InputObject $level0_arr
    #write-host $msg01 -ForegroundColor Magenta
   
   
    $level0_arr = @()
    $id01 = 0
   
    if ($domain01 -eq "workgroup")
    {
        # not in a domain, use a local login for pssession
        $Username = "$vm_name01\adminlocal"
        $Password = 'Lamaisondelapizza'
        try
        {
            $SecureString = ConvertTo-SecureString -AsPlainText $Password -Force
        }catch{}

        # LOCAL login and password
        $MySecureCreds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username,$SecureString

        ###################
        $computers01 = $computers_local01
    }   
   
    foreach($computer01 in $computers01)
    {
        ########################################
        # open pssession to ask each computer
        ########################################
        $error01.number = 0
        try
        {
            if($domain01 -eq "workgroup")
            {
                $s2 = New-PSSession -ComputerName $computer01 -SessionOption $PSSessionOption01 -Credential $MySecureCreds -erroraction stop
            }
            else
            {
                write-host "Pssession openning: " $computer01 -ForegroundColor Magenta
                $s2 = New-PSSession -ComputerName $computer01 -SessionOption $PSSessionOption01 -erroraction stop
            }
        }
        catch
        {
            #$error01.number = 1
            #write-host "ERROR Pssession openning: " $computer01 -ForegroundColor red
        }
       
        if($s2 -ne $null)
        {
            #######################################
            # oscaption (operation system name)
            #######################################
            $remote_command01 = "try {(Get-WMIObject Win32_OperatingSystem -erroraction stop).caption} catch {throw `$_}"
            $remote_command_block01 = [scriptblock]::Create($remote_command01)

            $job_caption = Invoke-Command -asjob -Session $s2 -ScriptBlock $remote_command_block01 -ErrorAction stop | Get-Job
            $results01 = $job_caption | Wait-Job -timeout $jobtimeout01 | Receive-Job
            $job_caption | remove-job
               
            $oscaption = $results01


            $level0_obj = New-Object PsObject
            $level0_obj | Add-Member NoteProperty -Name id -value $id01
            $level0_obj | Add-Member NoteProperty -Name computer_netbios_name -value $computer01
            $level0_obj | Add-Member NoteProperty -Name type -value $oscaption
       
            $level0_arr += $level0_obj.PSObject.Copy()

                #################################
                # licence windows from bios
                #################################
                $level1_obj = New-Object PsObject
                $level1_obj | Add-Member NoteProperty -Name id -value '1'
                $level1_obj | Add-Member NoteProperty -Name data -value "licences"
                $level1_arr = @()
                $level1_arr += $level1_obj.PSObject.Copy()

                    #################################
                    # licence windows from bios
                    #################################
                    $level2_obj = New-Object PsObject
                    $level2_obj | Add-Member NoteProperty -Name id -value 'windows from bios'
               
                    ################################################
                    # bios windows code (or other codes in bios)
                    ################################################
               
                    $remote_command01 = "try {(Get-WmiObject -query 'select * from SoftwareLicensingService').OA3xOriginalProductKey} catch {throw `$_}"
                    $remote_command_block01 = [scriptblock]::Create($remote_command01)

                    $job01 = Invoke-Command -asjob -Session $s2 -ScriptBlock $remote_command_block01 -ErrorAction stop | Get-Job
                    $results01 = $job01 | Wait-Job -timeout $jobtimeout01 | Receive-Job
                    $job01 | remove-job
               
                    $bios_keys = $results01
                                       
                    $level2_obj | Add-Member NoteProperty -Name data -value $bios_keys
                    $level2_arr = @()
                    $level2_arr += $level2_obj.PSObject.Copy()

                    ####################################
                    # windows key from registry
                    ####################################
                    $level2_obj = New-Object PsObject
                    $level2_obj | Add-Member NoteProperty -Name id -value 'windows from digitalid'

                    $path01 = "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion"
                    #$a = Get-RemoteRegistryInformation -Key $reg_ProductKey -AsObject
                   
                    $remote_command01 = "try {get-ItemProperty -Path `"$path01`" -erroraction stop} catch {throw `$_}"
                    $remote_command_block01 = [scriptblock]::Create($remote_command01)

                    $job01 = Invoke-Command -asjob -Session $s2 -ScriptBlock $remote_command_block01 -ErrorAction stop | Get-Job
                    $results01 = $job01 | Wait-Job -timeout $jobtimeout01 | Receive-Job
                    $job01 | remove-job
                    $a= $null
                    $a = $results01
                                       
                    #$a = get-ItemProperty -Path $path01 -erroraction stop
                    $digital_id_key =""
                    $digital_id_key = ConvertTo-ProductKey $a.DigitalProductId
                    #ConvertTo-ProductKey $a.DigitalProductId4 -x64

                    $level2_obj | Add-Member NoteProperty -Name data -value $digital_id_key
                    $level2_arr += $level2_obj.PSObject.Copy()

                    ###################################
                    # office find ospp.vbs to find key
                    ###################################
                    # 15 = office 2016
                    [string[]]$office_all_ospp01 = "C:\Program Files\Microsoft Office\Office15\OSPP.VBS"
                    [string[]]$office_all_ospp01+= "C:\Program Files (x86)\Microsoft Office\Office15\OSPP.VBS"
                    [string[]]$office_all_ospp01+= "C:\Program Files\Microsoft Office\Office16\OSPP.VBS"
                    [string[]]$office_all_ospp01+= "C:\Program Files (x86)\Microsoft Office\Office16\OSPP.VBS"
                   
                    $office_ospp_found01 = -1
                    for($osppcnt = 0; $osppcnt -lt $office_all_ospp01.count; $osppcnt++)
                    {
                        # check if ospp.vbs exist
                        $office_ospp01 = $office_all_ospp01[$osppcnt]

                        $msg01 = "Checking if path exist: " + $office_ospp01
                        write-host $msg01
                       
                        $remote_command01 = "try {Test-Path `"$office_ospp01`"} catch {throw `$_}"
                        $remote_command_block01 = [scriptblock]::Create($remote_command01)

                        $job01 = Invoke-Command -asjob -Session $s2 -ScriptBlock $remote_command_block01 -ErrorAction stop | Get-Job
                        $results01 = $job01 | Wait-Job -timeout $jobtimeout01 | Receive-Job
                        #write-host "job status: " $job01.StatusMessage -ForegroundColor Yellow
                        $job01 | remove-job

                        #write-host "result...: " $results01

                        if ($results01 -eq $true)
                        {
                            $office_ospp_found01 = $osppcnt
                        }
                       
                    }
                    $office_ospp02 = $office_all_ospp01[$office_ospp_found01]
                   
                    if($office_ospp_found01 -ne -1)
                    {
                        ##################################
                        # office key(s) last five digits
                        ##################################
                        #$productKey=cscript "C:\Program Files (x86)\Microsoft Office\Office15\OSPP.VBS" /dstatus | Select-String -Pattern $regex -AllMatches | % { $_.Matches } | % {$_.Value}
                        #$productKey=cscript $office_ospp01 /dstatus
                       
                        #c:\windows\system32\cscript.exe "C:\Program Files\Microsoft Office\Office16\OSPP.VBS" /dstatus
                        $remote_command01 = "try {c:\windows\system32\cscript.exe `"$office_ospp02`" /dstatus} catch {throw `$_}"
                        $remote_command_block01 = [scriptblock]::Create($remote_command01)

                        $job01 = Invoke-Command -asjob -Session $s2 -ScriptBlock $remote_command_block01 -ErrorAction stop | Get-Job
                        $results01 = ""
                        $results01 = $job01 | Wait-Job -timeout $jobtimeout01 | Receive-Job
                        #write-host "job status: " $job01.StatusMessage -ForegroundColor Yellow
                        $job01 | remove-job
                   
                        #$job01
                        #break all
                   
                        $alllines = $results01.split("`n")
                        $i=0
                        [string[]]$productid = @()
                        $skuid01 = @()
                        $licensename01 = @()
                        $keys01 = @()
                        foreach($line01 in $alllines)
                        {
                            #write-host $i " " $line01
                            #$i++
                            if($line01 -like "*PRODUCT ID*")
                            {
                                $line01
                                $productid += $line01.Split(" ")[$line01.Split(" ").GetUpperBound(0)]
                            }
                            if($line01 -like "*sku id*")
                            {
                                $line01
                                $skuid01 += $line01.Split(" ")[$line01.Split(" ").GetUpperBound(0)]
                            }
                            $tag01 = "LICENSE NAME: "
                            if($line01 -like "*$tag01*")
                            {
                                $line01
                                $licensename01 += $line01.replace($tag01, "")
                            }
                   
                            if($line01 -like "*Last 5 characters of installed product key*")
                            {
                                $line01
                                $keys01 += $line01.Split(" ")[$line01.Split(" ").GetUpperBound(0)]
                            }

                        }

                        #$productid
                        #$skuid01
                        #$licensename01
                        #$key01

                        #$regex='\b([A-Z1-9]{5}$)\b'
                        #$office_key = $results01 | Select-String -Pattern $regex -AllMatches | % { $_.Matches } | % {$_.Value}

                        $level22_arr = @()
                        for($liccnt01=0; $liccnt01 -lt $keys01.count; $liccnt01++)
                        {
                            $level22_obj = New-Object PsObject
                            $level22_obj | Add-Member NoteProperty -Name id -value 'office'
                            $level22_obj | Add-Member NoteProperty -Name productid -value $productid[$liccnt01]
                            $level22_obj | Add-Member NoteProperty -Name skuid -value $skuid01[$liccnt01]
                            $level22_obj | Add-Member NoteProperty -Name licensename -value $licensename01[$liccnt01]
                            $level22_obj | Add-Member NoteProperty -Name data -value $keys01[$liccnt01]
                            $level22_arr += $level22_obj.PSObject.Copy()
                        }
                    }
                    else
                    {
                        $msg01 = "ERROR did not find ospp.vbs"
                        write-host $msg01 -ForegroundColor Red
                       
                        $level22_arr = @()
                        $level22_obj = New-Object PsObject
                        $level22_obj | Add-Member NoteProperty -Name id -value 'office'
                        $level22_obj | Add-Member NoteProperty -Name ERROR -value $msg01
                        $level22_arr += $level22_obj.PSObject.Copy()
                    }       
            #$TEST = New-Object PsObject
            #$test | Add-Member NoteProperty -Name id -value '1111'
            #$test
            ######################################
            # 1 line for each type of licences
            ######################################
            $level1_arr += ,$level2_arr
            $level1_arr += ,$level22_arr
            $level0_arr += ,$level1_arr
            #$level0_arr+="test"

            #$level0_arr[1][1].data = "test"
            #$level0_arr[1].data = "test" #$level1_obj #.PSObject.Copy()

            #$msg01 = "sub array value...: " + $level0_arr[1].data
            #write-host $msg01 -ForegroundColor Magenta

            #$level0_arr[1][1] = "test"

            #$level0_arr =,$level1_arr
        } # pssession not null
        else
        {
            $msg01 = "ERROR could not open a pssession on " + $computer01 + ". Try powershell enable-psremoting on the machine"
            if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}
            write-host $msg01 -ForegroundColor Red

            $level0_obj = New-Object PsObject
            $level0_obj | Add-Member NoteProperty -Name id -value $id01
            $level0_obj | Add-Member NoteProperty -Name computer_netbios_name -value $computer01
            $level0_obj | Add-Member NoteProperty -Name type -value $msg01
       
            $level0_arr += $level0_obj.PSObject.Copy()
           
        }
        $id01++
        try{$s2.close}catch{}
        $s2 = $null
    } # for each computer01 in computers01

    #$level0_arr += ,$level1_obj

    #$level0_arr = get-wmiobject -class win32_logicaldisk | select deviceid, freespace, size, volumename
} # prod01
##############################
# get installed programs
##############################
#$properties = @('DisplayName', 'DisplayVersion', 'Publisher', 'InstallDate', "EstimatedSize") #, 'DisplayVersion', 'Publisher', 'InstallDate'
#$level0_arr = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object $properties

#write-host "before" -ForegroundColor Yellow

####################################
# test content of jagged array
####################################
#$level0_arr[2][2][1] = id 6 sub1 test 6

###################################################################
# replace sub object with texts to be able to generate html table
###################################################################
$file01 = report_object_replacements01($level0_arr)

$msg01 = "Number of items to replace: " + $global:item_to_replace_index01 + " count: " + $global:replace_arr01.count
#write-host $msg01
if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}

# inspect elements
for($elecnt02 = 0; $elecnt02 -lt $global:replace_arr01.count; $elecnt02++)
{
    $msg01 = "Ele: " + $elecnt02 + " " + $global:replace_arr01[$elecnt02]
    #write-host $msg01
    if ($logall -eq 1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}
}

########################################
# main html table
########################################
[xml]$level0_arr_xml01 = $level0_arr | ConvertTo-Html -fragment

#############################################
# put back each object in a html sub table
#############################################
$level0_arr_xml01  = replace_sub_table_xml($level0_arr_xml01)

$body = ""
$htmlfile02 = ""

####################################################
# page setup for web print thead tfoot with logo
####################################################

$wrp02 = New-Object PsObject # stand for web report parameters

$wrp02 | Add-Member NoteProperty -Name header_title_line01 -value 'Audit microsoft licenses'
$wrp02 | Add-Member NoteProperty -Name header_title_line02 -value 'Rapport'

$wrp02 | Add-Member NoteProperty -Name paper01 -value 'letter'
$wrp02 | Add-Member NoteProperty -Name orientation01 -value 'portrait'

$wrp02 | Add-Member NoteProperty -Name header_title_line_style01 -value ''

$wrp02 | Add-Member NoteProperty -Name header_left_image_percentx01 -value '70'
$wrp02 | Add-Member NoteProperty -Name header_left_image_percenty01 -value '0'
#$wrp02 | Add-Member NoteProperty -Name header_left_image_path01 -value 'https://www.tremblaycie.com/wp-content/uploads/2017/11/cropped-tremblay-cie.png'
$wrp02 | Add-Member NoteProperty -Name header_left_image_path01 -value 'https://devicom365.sharepoint.com/sites/TableaudeBordDevicom/_api/GroupService/GetGroupImage?id=%279d43061d-8e7c-43dd-a94a-f9bea7433034%27&hash=637193715108549624'

$wrp02 | Add-Member NoteProperty -Name header_right_image_percentx01 -value '70'
$wrp02 | Add-Member NoteProperty -Name header_right_image_percenty01 -value '0'
$wrp02 | Add-Member NoteProperty -Name header_right_image_path01 -value 'https://devicom365.sharepoint.com/sites/TableaudeBordDevicom/_api/GroupService/GetGroupImage?id=%279d43061d-8e7c-43dd-a94a-f9bea7433034%27&hash=637193715108549624'

$wrp02 | Add-Member NoteProperty -Name footer_style01 -value ''
$wrp02 | Add-Member NoteProperty -Name footer_table_style01 -value ''
$wrp02 | Add-Member NoteProperty -Name footer_left01 -value ''
$wrp02 | Add-Member NoteProperty -Name footer_middle01 -value ''
$date01 = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$wrp02 | Add-Member NoteProperty -Name footer_right01 -value $date01

# object containing jagged objects
$wrp02 | Add-Member NoteProperty -Name data -value $level0_arr_xml01.innerxml

$body = web_report($wrp02)

$htmlfile02 = ConvertTo-Html -Body $body

$htmlfile02 | Out-File $file01
& $file01

# futur search jquery
#var allRows = $("tr");
#$("input#search").on("keydown keyup", function() {
#  allRows.hide();
#  $("tr:contains('" + $(this).val() + "')").show();
#});

Wednesday, February 19, 2020

EVENTS scan powershell

Bonjour,

Ceci est une formation pour utiliser un script powershell qui scanne les événements, locaux ou distants, avec un filterxpath ou avec une recherche par chaîne.

Requis:
windows, server ou client
savoir le nom du log à scanner
windows à jour 2020-02
powershell ISE

Requis:
Pour lire un log à distance, le port suivant doit être ouvert dans le firewall windows:
Donc sur l'ordinateur client distant (dans le même réseau local), appliquer la commande powershell suivante:
Enable-NetFirewallRule -DisplayGroup 'Remote Event Log Management'


Comment trouver un log d'événements quand on ne sait pas le nom du log:
Pour trouver par nom un log, exemple: applocker (utilitaire qui bloque les application louches comme les crypto lockers)
Commande à faire dans powershell ISE:
Get-WinEvent -ListLog *exe* -force | select logname

Résultat:
LogName                                           
-------                                           
Microsoft-Windows-AppLocker/EXE and DLL           
Microsoft-Windows-AppLocker/Packaged app-Execution

PS C:\Users\serge.fournier\DÉVICOM INC\Prog Devicom - Document Prog\event_list_ps>

Le log voulu s'appelle donc:
Microsoft-Windows-AppLocker/EXE and DLL 

Au début du script, plusieurs object powershell ont été créé en tant que paramètres pour l'exécution du script:
# what events to check   0 = not check, 1 =check
$events_log_to_check01 = New-Object PsObject
$events_log_to_check01 | Add-Member NoteProperty -Name system -value '1'     # hardware
$events_log_to_check01 | Add-Member NoteProperty -Name security -value '0'   # logins
$events_log_to_check01 | Add-Member NoteProperty -Name updates -value '0'    # windows update
$events_log_to_check01 | Add-Member NoteProperty -Name dpmbackups -value '0' # backup azure microsoft
$events_log_to_check01 | Add-Member NoteProperty -Name applocker -value '0'  # bloqueur d'applications contre crypto virus

Par défaut, dans le script, plusieurs events log sont déjà prévu pour être scannés.
C'est-à-dire qu'il y a déjà des paramètres pour les scanner, mais ils sont à "off"
Pour qu'un log soit scanné, il faut que sa variable soit à 1.
Exemple:
$events_log_to_check01 | Add-Member NoteProperty -Name system -value '1'     # hardware

Le log system sera scanné

Par la suite une matrice (array) de paramètres est définie pour chaque log voulu:
Première matrice, les ordinateurs à scanner:
################################################################
# computers
################################################################
[string[]]$eventlogs_name_device_arr01 = "localhost"   # computer name to scan
#[string[]]$eventlogs_name_device_arr01+= "sql2008" # computer name to scan

Dans le rapport, chaque log sera un après l'autre, séparé par une ligne orange
Et chaque ordinateur scanné à la suite, à la fin du premier log
Ainsi, avec le bon id ou mot clef, on peut scanner les erreurs hardware d'un parc complet d'ordinateurs.

Deuxième matrice, les paramètres pour un log à scanner:
le nom du log
le provider (utilisé rarement)
le niveau d'erreur (en chiffre)
un mot à chercher qu'il faut mettre entre *. Exemple: *shut*, va trouver les shutdown
le nombre maximum d'événements
le nombre de jours à mettre dans le rapport
des filtres de ID à inclure (parfois on veut juste les login dans security)
des filtres de ID à NE PAS inclure (parfois il y a des id qui reviennent trop souvent pour rien (erreur com))


if($events_log_to_check01.system -eq 1)
{
    $n++
    [string[]]$eventslog_name_arr01[$n] = "system"
    [string[]]$eventslog_provider_arr01[$n] = "*"
    [string[]]$eventslog_level_arr01[$n] = "2"         # -1 = all 2 = ERROR, 3 = warning 4 = information
    [string[]]$eventslog_keywordtosearch01[$n] = "*"   # always use *word* (asterix at start and end)
    [string[]]$eventslog_maxevents_arr01[$n] = 100
    [string[]]$eventslog_daysback_arr01[$n] = 30       # -1 = no datetime filter

    [string[]]$eventslog_idtoget_arr01 = @()    # put other line in comment # for all events (0 count = no search by id)
    #[string[]]$eventslog_idtoget_arr01+= "107" # system wake from sleep
    #[string[]]$eventslog_idtoget_arr01+= "19"  # system update and description
    #[string[]]$eventslog_idtoget_arr01+= "22"  # system network loss
    #[string[]]$eventslog_idtoget_arr01+= "13"  # system shutdown
    $eventslog_idtoget_jarr01[$n] = $eventslog_idtoget_arr01

    $eventslog_idtoignore_arr01 = @()          # put other line in comment # for all events
    $eventslog_idtoignore_arr01+= "10016"      # com object error
    $eventslog_idtoignore_arr01+= "10028"      # com object error
    $eventslog_idtoignore_arr01+= "103"        # connectwise
    $eventslog_idtoignore_jarr01[$n] = $eventslog_idtoignore_arr01

    if($eventslog_daysback_arr01 -ne -1)
    {
        [DateTime[]]$eventslog_datetime_min_arr01[$n] = (Get-Date).AddDays(-$eventslog_daysback_arr01[$n])
        [DateTime[]]$eventslog_datetime_max_arr01[$n] = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}

Résultat du scan:

event search by keyword mot recherché:

timecreatedMachineNamelognamelevelLeveldisplaynameuserididmessage
GoBackDays: 30MaxEvents: 100system2StringSearch: *IdNotWanted: 10016 10028 103IDWanted:system orange01
2020-02-19 09:56:23dev47.ad.devicom.comsystem2Erreur7000Le service IntelHaxm n'a pas pu démarrer en raison de l'erreur : Un périphérique attaché au système ne fonctionne pas correctement.

Script complet:

####################################################################################################################
$Title01 = "Event search with filterxpath (level, date) and string search multiple computers multiple event log"
####################################################################################################################
cls; write-host $title01 -ForegroundColor green
###############################
# requis
###############################
# security powershell execution
# windows 7  #   Set-ExecutionPolicy RemoteSigned
# windows 10 #   Set-ExecutionPolicy -scope currentuser RemoteSigned #   Set-ExecutionPolicy -scope localmachine RemoteSigned
################## find an event log by keyword
# Enable-NetFirewallRule -DisplayGroup 'Remote Event Log Management'
### search for an event log in all logs
# Get-WinEvent -ListLog *exe* -force | select logname

################### parameters
$method01 = 0   # 0 = use xfilterpath   1 = use string search in xml (outdated)

# what events to check   0 = not check, 1 =check
$events_log_to_check01 = New-Object PsObject
$events_log_to_check01 | Add-Member NoteProperty -Name system -value '1'     # hardware
$events_log_to_check01 | Add-Member NoteProperty -Name security -value '0'   # logins
$events_log_to_check01 | Add-Member NoteProperty -Name updates -value '0'    # windows update
$events_log_to_check01 | Add-Member NoteProperty -Name dpmbackups -value '0' # backup azure microsoft
$events_log_to_check01 | Add-Member NoteProperty -Name applocker -value '0'  # bloqueur d'applications contre crypto virus

# get the total of array we need to store events
[int]$log_tot = 0
foreach($object_properties in $events_log_to_check01.PsObject.Properties)
{
    #$object_properties.Name
    $log_tot += $object_properties.Value
}

$eventslog_name_arr01 = New-Object 'string[]' $log_tot
$eventslog_provider_arr01 = New-Object 'string[]' $log_tot
$eventslog_level_arr01 = New-Object 'string[]' $log_tot
$eventslog_keywordtosearch01 = New-Object 'string[]' $log_tot
$eventslog_maxevents_arr01 = New-Object 'string[]' $log_tot
$eventslog_daysback_arr01 = New-Object 'string[]' $log_tot
$eventslog_idtoget_jarr01 = New-Object 'object[]' $log_tot
$eventslog_idtoignore_jarr01 = New-Object 'Object[]' $log_tot
$eventslog_datetime_min_arr01 = New-Object 'string[]' $log_tot
$eventslog_datetime_max_arr01 = New-Object 'string[]' $log_tot

#Installation events can have an Event ID of 11707 or 1033. The InstallOperation field of these events indicate Installation completed.
#Uninstallation events can have an Event ID of 11724 or 1034. The InstallOperation field of these events indicate Removal completed.
#http://juventusitprofessional.blogspot.com/2015/07/windows-2012-dfs-dfs-r-part-5-list-of.html

################################################################
# computers
################################################################
[string[]]$eventlogs_name_device_arr01 = "localhost"   # computer name to scan
#[string[]]$eventlogs_name_device_arr01+= "sql2008" # computer name to scan
$n = -1

if($events_log_to_check01.system -eq 1)
{
    $n++
    [string[]]$eventslog_name_arr01[$n] = "system"
    [string[]]$eventslog_provider_arr01[$n] = "*"
    [string[]]$eventslog_level_arr01[$n] = "2"         # -1 = all 2 = ERROR, 3 = warning 4 = information
    [string[]]$eventslog_keywordtosearch01[$n] = "*"   # always use *word* (asterix at start and end)
    [string[]]$eventslog_maxevents_arr01[$n] = 100
    [string[]]$eventslog_daysback_arr01[$n] = 30       # -1 = no datetime filter

    [string[]]$eventslog_idtoget_arr01 = @()    # put other line in comment # for all events (0 count = no search by id)
    #[string[]]$eventslog_idtoget_arr01+= "107" # system wake from sleep
    #[string[]]$eventslog_idtoget_arr01+= "19"  # system update and description
    #[string[]]$eventslog_idtoget_arr01+= "22"  # system network loss
    #[string[]]$eventslog_idtoget_arr01+= "13"  # system shutdown
    $eventslog_idtoget_jarr01[$n] = $eventslog_idtoget_arr01

    $eventslog_idtoignore_arr01 = @()          # put other line in comment # for all events
    $eventslog_idtoignore_arr01+= "10016"      # com object error
    $eventslog_idtoignore_arr01+= "10028"      # com object error
    $eventslog_idtoignore_arr01+= "103"        # connectwise
    $eventslog_idtoignore_jarr01[$n] = $eventslog_idtoignore_arr01

    if($eventslog_daysback_arr01 -ne -1)
    {
        [DateTime[]]$eventslog_datetime_min_arr01[$n] = (Get-Date).AddDays(-$eventslog_daysback_arr01[$n])
        [DateTime[]]$eventslog_datetime_max_arr01[$n] = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}
##############################
if($events_log_to_check01.security -eq 1)
{
   
    $n++
    [string[]]$eventslog_name_arr01[$n] = "security"
    [string[]]$eventslog_provider_arr01[$n] = "*"
    [string[]]$eventslog_level_arr01[$n] = "*"         # -1 = all 2 = ERROR, 3 = warning 4 = information
    [string[]]$eventslog_keywordtosearch01[$n] = "*serge*"   # always use *word* (asterix at start and end)
    [string[]]$eventslog_maxevents_arr01[$n] = 100
    [string[]]$eventslog_daysback_arr01[$n] = 30       # -1 = no datetime filter

    [string[]]$eventslog_idtoget_arr01 = @()    # put other line in comment # for all events (0 count = no search by id)
    #[string[]]$eventslog_idtoget_arr01+= "4624" # logon
    $eventslog_idtoget_jarr01[$n] = $eventslog_idtoget_arr01

    $eventslog_idtoignore_arr01 = @()          # put other line in comment # for all events
    #$eventslog_idtoignore_arr01+= "10016"      # com object error
    $eventslog_idtoignore_jarr01[$n] = $eventslog_idtoignore_arr01

    if($eventslog_daysback_arr01 -ne -1)
    {
        [DateTime[]]$eventslog_datetime_min_arr01[$n] = (Get-Date).AddDays(-$eventslog_daysback_arr01[$n])
        [DateTime[]]$eventslog_datetime_max_arr01[$n] = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}
##############################
if($events_log_to_check01.updates -eq 1)
{
    #[string[]]$log_arr01+= "setup (installation, updates)"
    #[string[]]$eventid00 = "2 (update windows sans description, voir system pour celui avec description)"
   
    $n++
    [string[]]$eventslog_name_arr01[$n] = "setup (installation, updates)"
    [string[]]$eventslog_provider_arr01[$n] = "*"
    [string[]]$eventslog_level_arr01[$n] = "*"         # -1 = all 2 = ERROR, 3 = warning 4 = information
    [string[]]$eventslog_keywordtosearch01[$n] = "*"   # always use *word* (asterix at start and end)
    [string[]]$eventslog_maxevents_arr01[$n] = 25
    [string[]]$eventslog_daysback_arr01[$n] = 30       # -1 = no datetime filter

    [string[]]$eventslog_idtoget_arr01 = @()    # put other line in comment # for all events (0 count = no search by id)
    #[string[]]$eventslog_idtoget_arr01+= "4624" # logon
    $eventslog_idtoget_jarr01[$n] = $eventslog_idtoget_arr01

    $eventslog_idtoignore_arr01 = @()          # put other line in comment # for all events
    #$eventslog_idtoignore_arr01+= "10016"      # com object error
    $eventslog_idtoignore_jarr01[$n] = $eventslog_idtoignore_arr01

    if($eventslog_daysback_arr01 -ne -1)
    {
        [DateTime[]]$eventslog_datetime_min_arr01[$n] = (Get-Date).AddDays(-$eventslog_daysback_arr01[$n])
        [DateTime[]]$eventslog_datetime_max_arr01[$n] = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}
###################################
if($events_log_to_check01.dpmbackups -eq 1)
{
    $n++
    [string[]]$eventslog_name_arr01[$n] = "dpm backup events"
    [string[]]$eventslog_provider_arr01[$n] = "*"
    [string[]]$eventslog_level_arr01[$n] = "3"         # -1 = all 2 = ERROR, 3 = warning 4 = information
    [string[]]$eventslog_keywordtosearch01[$n] = "*"   # always use *word* (asterix at start and end)
    [string[]]$eventslog_maxevents_arr01[$n] = 25
    [string[]]$eventslog_daysback_arr01[$n] = 14       # -1 = no datetime filter

    [string[]]$eventslog_idtoget_arr01 = @()    # put other line in comment # for all events (0 count = no search by id)
    #[string[]]$eventslog_idtoget_arr01+= "4624" # logon
    $eventslog_idtoget_jarr01[$n] = $eventslog_idtoget_arr01

    $eventslog_idtoignore_arr01 = @()          # put other line in comment # for all events
    #$eventslog_idtoignore_arr01+= "10016"      # com object error
    $eventslog_idtoignore_jarr01[$n] = $eventslog_idtoignore_arr01

    if($eventslog_daysback_arr01 -ne -1)
    {
        [DateTime[]]$eventslog_datetime_min_arr01[$n] = (Get-Date).AddDays(-$eventslog_daysback_arr01[$n])
        [DateTime[]]$eventslog_datetime_max_arr01[$n] = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    }
}

# reference ID for events
[string[]]$log_arr01 = "application (msi installer)"
[string[]]$eventid00 = "11707 (install completed)"
[string[]]$eventid00+= "1033 (install completed)"
[string[]]$eventid00+= "11724 (uninstall completed)"
[string[]]$eventid00+= "1034 (uninstall completed)"

############################################### display date
$date = get-date $eventslog_datetime_min_arr01[$n] -Format "yyyy-MM-dd HH:mm:ss"

########################################
# know events that are recurrents
########################################
# http://www.eventid.net/display-eventid-36887-source-Schannel-eventno-10676-phase-1.htm

########################################
# log file
########################################
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$scriptname = split-path -leaf $MyInvocation.MyCommand.Definition
$Logfilename = $scriptname + "_log.txt"
$logfile = $scriptPath + "\" + $Logfilename
$logall = 1

####################################
# color for thml report
####################################
[string[]]$error_message_arr = "WARNING"
[string[]]$error_color_arr = "yellow" # yellow
[string[]]$error_message_arr+= "avertissement"
[string[]]$error_color_arr+= "yellow" # yellow
[string[]]$error_message_Arr+= "ERROR"
[string[]]$error_color_arr+= "red" # red
[string[]]$error_message_Arr+= "ERReuR"
[string[]]$error_color_arr+= "red" # red
[string[]]$error_message_arr += "1900-01-01 00:00:00"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_arr += "00:00:00"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_arr += "inprogress"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_arr += "pending"
[string[]]$error_color_arr += "yellow" # yellow
[string[]]$error_message_Arr+= "failed"
[string[]]$error_color_arr+= "red" # red
[string[]]$error_message_Arr+= "true"
[string[]]$error_color_arr+= "red" # red
[string[]]$error_message_Arr+= "orange01"
[string[]]$error_color_arr+= "orange" # red

# if we search a keyword, the result cell will be orange if found
if($keywordtosearch01 -ne "*")
{
    #[string[]]$error_message_arr += "$keywordtosearch01"
    #[string[]]$error_color_arr += "orange" # yellow
}

#################################
# HTML formatting
#################################
$header = @"
<style>
BODY{background-color:white;}
TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}
TH{border-width: 1px;padding: 5px;border-style: solid;border-color: black;foreground-color: black;background-color: LightBlue}
TD{border-width: 1px;padding: 5px;border-style: solid;border-color: black;foreground-color: black;background-color: white}
.green{background-color:#d5f2d5}
.blue{background-color:#e0eaf1}
.red{background-color:#ffd7de}
.yellow{background-color:#ffff00}
.orange{background-color:#ffa500}
</style>
"@

$msg01 = $title01
if ($logall=1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile}catch{}}

# loop computers

# agregation of all computers and logs for each computers
$events_jobs_arr01 = @()

foreach($eventlog_computer in $eventlogs_name_device_arr01)
{
    $msg01 = "Loop computers: " + $eventlog_computer
    write-host  -ForegroundColor Cyan
    if ($logall=1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}
   
    # loop logs (ex: system, application etc.)
    for($log_cnt =0; $log_cnt -lt $log_tot; $log_cnt++)
    {
        $msg01 = "Log: " + $eventslog_name_arr01[$log_cnt]
        if ($logall=1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}

        ##########################################
        # FILTERXPATH fabrication (faster filter)
        ##########################################
        #$eventid01 = @()
   
        #[DateTime]$date_time_max00 = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        #[DateTime]$date_time_min00 = (Get-Date).AddDays(-$gobackdays01)

        # using filterxpath to filter stuff is faster, the event are filtered BEFORE the result is sent to powershell
        # http://www.powershellish.com/blog/2014-12-09-get-winevent-filterxpath

        # filter datetime at source for filterxpath parameter
        $milliseconds01 = New-TimeSpan -Days $eventslog_daysback_arr01[$log_cnt] | Select-Object -ExpandProperty TotalMilliseconds

        #"*[System[EventID=4624 and TimeCreated[timediff(@SystemTime) <= 86400000]] and EventData[Data[@Name='TargetUserName']='jdoe']]"
   
        $filterxpath01 = "*[System["
        $filterxpath01+= "TimeCreated[timediff(@SystemTime) <= $milliseconds01]"
   
        # level of error
        if($eventslog_level_arr01[$log_cnt] -ne -1 -and $eventslog_level_arr01[$log_cnt] -ne "*")
        {
            $level01 = $eventslog_level_arr01[$log_cnt]
            $filterxpath01+= " and Level=$level01"
        }

        # id wanted
        if($eventslog_idtoget_jarr01[$log_cnt].count -gt 1) { $filterxpath01+= " and (" }
        if($eventslog_idtoget_jarr01[$log_cnt].count -eq 1) { $filterxpath01+= " and" }
        for($idcnt=0;$idcnt -lt $eventslog_idtoget_jarr01[$log_cnt].count; $idcnt++)
        {
            $id01 = $eventslog_idtoget_jarr01[$log_cnt][$idcnt]
            if ($id01.length -gt 0)
            {
                # add OR at second element and more
                if($idcnt -gt 0) {$filterxpath01+= " or " }
                $filterxpath01+= " EventID=$id01"
            }
        }
        if($eventslog_idtoget_jarr01[$log_cnt].count -gt 1) { $filterxpath01+= ")" }

        # id NOT wanted
        for($idcnt=0;$idcnt -lt $eventslog_idtoignore_jarr01[$log_cnt].count; $idcnt++)
        {
            $id01 = $eventslog_idtoignore_jarr01[$log_cnt][$idcnt]
            if ($id01.length -gt 0)
            {
                $filterxpath01+= " and EventID!=$id01" # com error
            }
        }
       
        $filterxpath01+= "]" # close system related variable group
        #$filterxpath01+= " and EventData[Data[@Name='Utilisateur']='devicom']"
        #[EventData[Data='Sens' and Data='Logoff']]
        #$filterxpath01+= " and EventData[Data='windows']"
        $filterxpath01+= "]" # close main

        ###########################
        # main
        ###########################

        #[string]$date_time_min = $date_time_min00.tostring("yyyy-MM-dd HH:mm:ss")
        #[string]$date_time_max = $date_time_max00.tostring("yyyy-MM-dd HH:mm:ss")
        write-host "------------------------------------"
        write-host "Computer.........: " $eventlog_computer
        write-host "log..............: " $eventslog_name_arr01[$log_cnt]
        write-host "provider01.......: " $eventslog_provider_arr01[$log_cnt]
        write-host "level............: " $eventslog_level_arr01[$log_cnt]
        write-host "id to get........: " $eventslog_idtoget_jarr01[$log_cnt]
        write-host "id to ignore.....: " $eventslog_idtoignore_jarr01[$log_cnt]
        write-host "keywordtosearch01: " $eventslog_keywordtosearch01[$log_cnt]
        write-host "date_time_min....: " $eventslog_datetime_min_arr01[$log_cnt] #.tostring("yyyy-MM-dd HH:mm:ss")
        write-host "date_time_max....: " $eventslog_datetime_max_arr01[$log_cnt] #.tostring("yyyy-MM-dd HH:mm:ss")
        write-host "maxevents........: " $eventslog_maxevents_arr01[$log_cnt]

        #############################################
        # result psobject
        #############################################
        $events_job_obj01 = New-Object PsObject
        $events_job_obj01 | Add-Member NoteProperty -Name timecreated -value ''
        $events_job_obj01 | Add-Member NoteProperty -Name MachineName -value ''
        $events_job_obj01 | Add-Member NoteProperty -Name logname -value ''
        $events_job_obj01 | Add-Member NoteProperty -Name level -value ''
        $events_job_obj01 | Add-Member NoteProperty -Name Leveldisplayname -value ''
        $events_job_obj01 | Add-Member NoteProperty -Name userid -value ''
        $events_job_obj01 | Add-Member NoteProperty -Name id -value ''
        $events_job_obj01 | Add-Member NoteProperty -Name message -value ''

        ########################################
        # second header 1st line for each log
        ########################################
        $events_job_obj01.timecreated = "GoBackDays: " + $eventslog_daysback_arr01[$log_cnt]
        $events_job_obj01.MachineName = "MaxEvents: " + $eventslog_maxevents_arr01[$log_cnt]
        $events_job_obj01.logname = $eventslog_name_arr01[$log_cnt]
        $events_job_obj01.level = $eventslog_level_arr01[$log_cnt]
        $events_job_obj01.Leveldisplayname = "StringSearch: " + $eventslog_keywordtosearch01[$log_cnt]
        $events_job_obj01.userid = "IdNotWanted: " + $eventslog_idtoignore_jarr01[$log_cnt]
        $events_job_obj01.id = "IDWanted: " + $eventslog_idtoget_jarr01[$log_cnt]
        $events_job_obj01.message = $eventslog_name_arr01[$log_cnt] + " orange01"
        $events_jobs_arr01 += $events_job_obj01.PSObject.Copy()

        $usefilterxpath01 = 1
        if($usefilterxpath01 -eq 1)
        {
            [psobject]$global:events_report01 = $null
           
            if($eventslog_keywordtosearch01[$log_cnt] -ne "*")
            {
                # string search
                $command01 = {[psobject]$global:events_report01 = Get-WinEvent -ComputerName $eventlog_computer -LogName $eventslog_name_arr01[$log_cnt] `
                -MaxEvents $eventslog_maxevents_arr01[$log_cnt] -FilterXPath $FilterXPath01 -ErrorAction Stop | Where-Object {$_.Message -like $eventslog_keywordtosearch01[$log_cnt]}}
            }
            else
            {
                # no string search
                $command01 = {[psobject]$global:events_report01 = Get-WinEvent -ComputerName $eventlog_computer -LogName $eventslog_name_arr01[$log_cnt] `
                -MaxEvents $eventslog_maxevents_arr01[$log_cnt] -FilterXPath $FilterXPath01 -ErrorAction Stop}
            }
           
            write-host "Filterxpath...: $FilterXPath01" -ForegroundColor Yellow
            #write-host $command01 -ForegroundColor Yellow
            #$command02 = [scriptblock]::Create($command01)
           
            try
            {
                &$command01
                #[psobject]$events_report01 = Get-WinEvent -ComputerName $eventlog_computer -LogName $eventslog_name_arr01[$log_cnt] `
                #-MaxEvents $eventslog_maxevents_arr01[$log_cnt] -FilterXPath $FilterXPath01 -ErrorAction Stop
                $events_job_obj01 = $global:events_report01 | select Timecreated, MachineName, @{Name = 'Logname'; Expression = {$eventslog_name_arr01[$log_cnt]}} , level, Leveldisplayname,  userid, id, message
            }
            catch
            {
                write-host "error 0 results" -ForegroundColor Red
                $events_job_obj01.logname = $eventslog_name_arr01[$log_cnt]
               
                $msg01 = "ERROR getting events"
                $msg01+= $eventslog_name_arr01[$log_cnt]
                $msg01+= $_.exception.message

                $events_job_obj01.message = $msg01
                #write-host $_.ScriptStackTrace
                write-host $_.exception.message -ForegroundColor Red
            }
            #$global:events_report01
            #| format-table Timecreated, Leveldisplayname, message, id #-auto -wrap #| Out-String
        }
        else
        {
            # OLD search by TEXT, no xfilter
            [psobject]$events_report01 = Get-WinEvent -ComputerName $eventlog_computer -LogName $log01 -MaxEvents $maxevents01 `
            | Where-Object {`
            $_.providername -like $provider01 `
            -and $_.Message -like $keywordtosearch01 `
            -and $_.timecreated -gt $date_time_min `
            -and $_.level -eq 2 `
            -and $_.id -ne 10016 `
            -and $_.timecreated -le $date_time_max}
            #-and $_.Message -notlike "*Group Policy failed*" `
            #-and $_.level -eq 2 `
        }
        #$events_job_obj01 = $global:events_report01 | select Timecreated, MachineName, @{Name = 'Logname'; Expression = {$eventslog_name_arr01[$log_cnt]}} , level, Leveldisplayname,  userid, id, message

        $events_jobs_arr01 += $events_job_obj01.PSObject.Copy()
        #$events_jobs_arr01
    } # loop logs

} # loop computers

[xml]$event_xml01 = $events_jobs_arr01 | ConvertTo-Html -fragment

# xml for futur database
#$event_xml02 = ConvertTo-Xml -As "Document" -InputObject ($events_jobs_arr01  | select Timecreated, level, Leveldisplayname, MachineName, userid, message, id) #-Depth 3

# Parse XML object and set colour class according to value in last column ("Age")

$msg01 = "xml loop start"
if ($logall=1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}

for($trcnt=1;$trcnt -le $event_xml01.table.tr.count-1;$trcnt++)
{
    ################################################################
    # change color if certain keywords are found in html cell
    ################################################################
   
    for($tdcnt=1;$tdcnt -le $event_xml01.table.tr[$trcnt].ChildNodes.Count-1;$tdcnt++)
    {
        #write-host $tdcnt "   " $event_xml01.table.tr[$trcnt].td[$tdcnt]
        $item02 = $event_xml01.table.tr[$trcnt].td[$tdcnt]
        for($errorcnt = 0; $errorcnt -lt $error_message_arr.count; $errorcnt++)
        {
            $error_message = $error_message_arr[$errorcnt]
            if($item02 -like "*$error_message*")
            {
                $event_xml01.table.tr[$trcnt].ChildNodes[$tdcnt].SetAttribute('class',$error_color_arr[$errorcnt])
            }
        }
    }
}
$msg01 = "xml loop end"
if ($logall=1) {try{(Get-Date -Format "yyyy-MM-dd HH:mm:ss") + " " + $msg01 | Out-File $logfile -append}catch{}}

# Define body and append the modified XML object
$body = @"
<H2>event search by keyword mot recherché: $keywordtosearch01</H2>
$($event_xml01.innerxml)
"@

#####################
# html
#####################
$file03 = $scriptPath + "\event_list_array.htm"
# Convert to HTML and save the file
ConvertTo-Html -Head $header -Body $body | Out-File $file03

#########################
# xml
#########################
#$file04 = $scriptPath + "\event_list.xml"
#$event_xml02.save($file04)

if($events_jobs_arr01.count -gt 0)
{
    & $file03
}
Write-Host "Saved to:" $file03 -ForegroundColor Green