Skip to content
Guide on how to make Html reports in PowerShell
Guide on how to make Html reports in PowerShell
One of the strength that makes PowerShell one of the best scripting languages is the flexibility and simplicity it offers in working with objects. That flexibility can be demonstrated by the cmdlets available to convert objects to and from a different type. There is a plethora of conversions that can be done natively in PowerShell.
Table of Contents
Function | Name |
---|---|
Function | ConvertFrom-SddlString |
Cmdlet | ConvertFrom-CIPolicy |
Cmdlet | ConvertFrom-Csv |
Cmdlet | ConvertFrom-Json |
Cmdlet | ConvertFrom-SecureString |
Cmdlet | ConvertFrom-String |
Cmdlet | ConvertFrom-StringData |
Cmdlet | Convert-Path |
Cmdlet | Convert-String |
Cmdlet | ConvertTo-Csv |
Cmdlet | ConvertTo-Html |
Cmdlet | ConvertTo-Json |
Cmdlet | ConvertTo-SecureString |
Cmdlet | ConvertTo-TpmOwnerAuth |
Cmdlet | ConvertTo-Xml |
As you can see one of them is called “ConvertTo-Html” which we will use to produce reports that can be displayed in a web browser. You can find the complete help page of this cmdlet in the Microsoft documentation.
Protect Your Data with BDRSuite
Like any other Convert cmdlet, it can be used by specifying the input object as an argument or by piping it into the convert command. Usually the latter is used as it is easier and quicker that way.
The output of the “ConvertTo-Html” cmdlet is of type string so it can be stored in a file.
TypeName: System.String

Creating a basic HTML report in PowerShell
If you look into the Html file you will see all your processes in the form of an HTML table.

As you can see, the info is here but not very exciting visually speaking. We will see later on how to make it look more appealing.

Narrowing down the output
You most likely do not want to collect every single property of the objects as they are not all relevant to your use case. You can use the ” -Property” parameter of the “ConvertTo-Html” cmdlet rather than piping it into “Select”. The output is the same and it does not make the command run faster but I personally find it cleaner to use the parameters offered by the cmdlet and it shortens the command overall. The shorter the command the cleaner.
Once the set of properties have been specified to the command, the output is narrowed down to those only.

Content customization parameters
Before cracking on with customization, I figured it would be best to review the parameters offered by the “ConvertTo-Html” cmdlet. The parameters listed below are the main ones that you will need to make good reports.
Parameter | Description |
---|---|
-Body | Specifies the text to add after the opening tag. By default, there is no text in that position. |
-CssUri | Specifies the Uniform Resource Identifier (URI) of the cascading style sheet (CSS) that is applied to the HTML file. The URI is included in a style sheet link in the output. |
-Fragment | Generates only an HTML table. The HTML, HEAD, TITLE, and BODY tags are omitted. |
-Head | Specifies the content of the HEAD tag. The default is title HTML TABLE. If you use the Head parameter, the Title parameter is ignored. |
-PostContent | Specifies text to add after the closing TABLE tag. By default, there is no text in that position. |
-PreContent | Specifies text to add before the opening TABLE tag. By default, there is no text in that position. |
-Title | Specifies a title for the HTML file, that is, the text that appears between the TITLE tags. |
Customization
As seen previously, the default layout of the Html table is not so great for a report and the whole thing looks unsatisfactory at best.
In this chapter, we are going to customize the report. We will do a few basic changes but if you want to go further, the website w3schools.com will be your friend as it provides all the needed information about Html and Css syntax.
There are 2 main ways for performing the customization of an Html file. In both cases the styling instructions will be the same, it is where we specify them that will change. Which one to choose will depend on your use case. There is also a third way called Inline Styles which consists of placing the styling inside the tag of the object to style. We will not describe this method here, it may be suitable in some cases but it is not flexible enough and not very clean I think.
Note that you can combine different methods but a priority applies. 1 being the highest priority:
- Inline style (inside an HTML element)
- External and internal style sheets (in the head section)
- Browser default
External Style Sheet (Customization done in a CSS file)
Allows for complex customization while keeping the Html clean.
Centralized configuration, recommended when multiple reports require the same formatting.
If you are planning on producing multiple reports or use many styling options, it will be a good idea to consolidate the customization of your documents into a CSS file (Cascading Style Sheets). It means that instead of configuring the style in each HTML file, you only specify a path to the CSS file which will apply its styles to the report. It also brings the benefit of being able to ensure consistency across reports by editing a single file.
In order to apply a CSS style sheet, we can use the ” -CssUri” parameter of the “ConvertTo-Html” cmdlet. I placed a few processes in a $Process variable to shorten the command and make it easier to read.
Now if we inspect the Html file, we notice that the MyStyleSheet.Css file is configured.

As an example, we are adding basic borders to the table by configuring in the Css file.


Internal style sheet (Customization done in the HTML file)
Good for basic formatting of a single report.
Simpler as it does not require interacting with an external Css file.
In this section we are not using a Css file, meaning we specify the Css code within the Html file. I like this approach for basic reports that require a unique style or if I have only a couple of them. I won’t bother with a CSS file unless it brings something useful to the table.
As a best practice, internal styles are defined within the < style > element, inside the head section of an Html page. To achieve this, we are going to use the ” -Head” parameter of the “ConvertTo-Html” cmdlet to include our styles. Whatever is passed to the parameter will be placed right below the Head tag. In short, you populate this variable with what would have been the content of the Css file. To demonstrate this we will add borders to our sad table as we did earlier.
The Html file now includes our styles as pictured below.

The table now looks the exact same as in the previous example.

Title and extra content
If you want your reports to look as professional as possible, there are a few little tweaks that will make a difference to the end result. These are especially useful when there is a collection of reports being produced to keep them ordered.
To add a title, simply use the ” -Title” argument of the “ConvertTo-Html” cmdlet. Just like any other command, you can work with variables to make it relevant.
The title is added to the Html content.

The title now appears in the browser tab.

Along with the table, you may want to add some text or other content to your reports. One use case could be a description of what is depicted in the table and how to react in accordance to it. But then again, your imagination is the limit, you could, for instance, use the content of the given table to set conditions to adapt the text to the situation.
To add extra content to an Html report, we are using the ” -PreContent” and ” -PostContent” parameters of the “ConvertTo-Html” cmdlet. In the example below we list the Chrome processes.
PS D:\> $precontent = “List of processes on $env:COMPUTERNAME.”
PS D:\> $postcontent= “There are $($chrome.count) chrome processes running.”
The PreContent is added after the body tag and the PostContent is added before the /body tag.

The report obviously shows these new changes in the browser.

Now, this was a very simple example but one can think of various usages for those. For instance, you could imagine adding the content of a log file or appending another table to the existing one (more on the “-Fragment” parameter below).
Change the color of the first row
I wanted to add this section to demonstrate how to change the color of the first row of the table. Although it may sound gimmicky I find it quite useful to know the status of a report at a glance and it is fairly easy to do with a tiny bit of Css. Note that if you use internal style sheets (which we are about to do) you can use it in emails.
For this example, we are listing the datastores in PowerCLI. According to the amount of free space of the most used datastore, it will change the top row to blue, yellow or red. The PowerShell code below is self-explanatory to understand how this works. As you can see we use the style section to change the color of the top row according to whether a datastore is found with less than 30% (red), 40% (yellow). If no datastore exist in either, the color will be blue. These thresholds are that high to make it go red, you are fine with 30%.
$red = 30
$PreContent = “Free space thresholds: Yellow 40% ; Red 30%”
$Datastore = Get-Datastore
Here is what it looks like when a datastore is found with less than 30% of free space in this instance. In this case “Datastore-X3”.

The “Fragment” parameter
I decided to dedicate a whole chapter to this parameter because I find it very useful and can be used in multiple cases. The purpose of this parameter is to exclude the HTML, HEAD, TITLE, and BODY tags from the output. Which means only the Html code of the table itself will be in the output. In this section, we are going to review a few of these use cases and see what we can do with it.
Multiple tables in one report
In the below example we are stacking a table of processes and a table of services. We use the ” -PostContent” parameter to append the service table with a br tag to leave some space in between.

Using an Html template file
This method is a little less obvious but can be useful if you want to place the table inside an Html file that you use as a template. Typically you would have a complete Html file with all the bells and whistles that you need, for example, there could be a link back to an index file listing all the reports and pictures of cats. The pros of this method is that, like with a Css file for the styling, you can modify the layout of all your reports from a single location.
In this “Template” file you place a special string of characters that you will then replace with the table generated by “ConvertTo-Html”. The ” -Fragment” parameter is useful here because the Html is already built and only the table is missing.
- Import content of Html template in a Powershell variable
- Replace the special string with the Html table in the variable
- Output the content of the variable into the Html file of the report. New or existing with ” -Force”
Let’s call the Html template file “Report-Template.Html” which will look something similar to this. It will be a more advanced template in an actual use case but it is simplified for the sake of the argument. The special string mentioned earlier is “REPLACE-ME” in the example below.

$ReplaceString = “REPLACE-ME”
$Report = Get-Content $Template
$Report = $Report -replace $ReplaceString,$Table
Presentation
Now that you have a nice web report it would not be nice to leave it somewhere on your computer and open it locally. Well, it’s actually ok but we can do better.
Once you are ready to use PowerShell for actual reporting purpose, a good idea is to use a dedicated server with a scheduled task per PowerShell scripts. You can then write your scripts in such a way that they store the Html file in a folder served by IIS to have them available from anywhere. This will allow you to have your reports run automatically and have them ready when you come to work in the morning.
If you don’t want to bother opening a browser and clicking on the bookmark, which is totally understandable, you may want to have the report brought to you in an email. No problem with that. Instead of storing the Html code generated by PowerShell in a file, you just need to pass it as the ” -Body” parameter of the “Send-MailMessage” and set the ” -BodyAsHtml” switch. Like so. Note that what was described in the previous chapters also applies here.
Note that you can add attachments with this cmdlet. This could be useful to attach a log file or the full report and only display the top 10 rows if it is too long for an email.
Conclusion
Reporting and monitoring are two areas of the data center that can make a real difference in your response time to an event, be it reactive to proactive. Being able to make html reports in PowerShell will open a world of possibilities in these regards.
There is so much more to be said about this topic that I could go on about it for hours. If you are interested in more advanced reports with filter, sort and all kind of other features I recommend that you check out the Tablesorter plugin for JQuery.
It is now up to you to make your own use cases and start increasing the reporting in your environment.
Related Posts:
Convert VHD to VHDX using Hyper-V Manager and Powershell
How to Merge Hyper-V Checkpoints using Hyper-V Manager and Powershell
Why use PowerShell Jobs?
Virtualization Trends Series: ChatGPT and PowerShell: Part 4
Try BDRSuite for Free!
You can check his blog at www.vxav.fr
Schedule a live demo with one of our product experts
Start your full-featured 30-day free trial
Explore detailed pricing, editions & features
Все привет! В продолжение статьи о возможностях PowerShell, хочу поделиться несложной реализацией создания REST API и простого Web-сервера, используя только PowerShell на базе класса .NET HttpListener. Такая реализация позволяет настроить endpoint-ы (конечные точки) для обработки GET и POST запросов, которые принимают параметры в заголовке запроса, использовать Basic-авторизацию (на основе Base64), обрабатывать любые коды возврата, а так же без лишнего кода, отдавать информацию в разных форматах: json, xml, html, csv. Хочу акцентировать, что данное решение, возможно, не является самым правильным, тем не менее успешно помогло мне покрыть потребности нескольких задач и хотел лишний раз продемонстрировать возможности языка.
Для начала расскажу, кому и зачем данная реализация может понадобиться, далее приведу пример работы с готовым решение и разберем основные моменты для создания своего сервера. Стоит сразу упомянуть, что существует кросс-платформенный Web Framework Pode, и это не единственное решение, успел попробовать как минимум три, но, пожалуй, самое интересное, поддерживаемое и задокументированное. Мне же хотелось иметь свою реализацию, где будут отсутствовать сторонние зависимости, и немного больше понимать, что происходит на стороне сервера во время его работы, в частности, для отладки.
Кому и зачем данная реализация может понадобиться? Приведу свой пример, у меня была задача удаленно реализовать доступ к десктопному приложению, у которого для специфического взаимодействия с ним была возможность выполнять только локальные команды через консоль. Из условий, не было возможности настроить и использовать на машинах WinRM и OpenSSH, ввиду ограничений в компании со стороны ИБ, в то же самое время HTTP был валидным и стандартизированным (после HTTPS) решением. В результате, выполнение и вывод команд получилось автоматизировать, а в дополнение к этому, добавить настройки реестра, чистку temp и логов, что расширило возможности и позволило инженеру DevOps внедрить их в свой Pipeline, используя привычный интерфейс.
Во-вторых, работая системным администратором, в качестве интерфейса для автоматизации задач я использовал WinForms, редко Telegram. Тогда мне очень хотелось попробовать реализовать свой Web-интерфейс для визуализации подобных задач. Важно заметить, что не обладаю сильными познаниями в области систем CI/CD, и конечно, рациональнее использовать, например, интерфейс Jenkins для подобных целей. Тем не менее подобное решение имеет место, т.к. Jenkins все таки не содержит такой кастомизации, как собственный интерфейс.
В-третьих. У меня есть небольшой проект, целью которого является поиск и доставка контента из конкретного torrent-трекера Кинозал до телевизора с Plex (на эту тему у меня есть отдельная статья на Habr). Так сложилось, что за основу я выбрать язык Bash, т.к. планировал запускать бота удаленно и использовать только REST API интерфейс для взаимодействия со всеми сервисами. В течении всего времени эксплуатации, мне не хватало такого функционала, как остановка и повторный запустить torrent-клиента (qBittorrent), или просматривать свободное место на диске, узнать размер конкретных директорий и файлов, а так же возможности их удаления. По аналогии с другими сервисами, мне хотелось использовать единый интерфейс (REST API), в попытках найти готовое решение в виде десктопного приложения для Windows, вспоминая, что уже взаимодействовал с Open Hardware Monitor, используя его как клиент (в режиме HTTP), уже писал модуль для получения метрик через REST API (с возможностью отправки их в InfluxDB и визуализацией в Grafana). Но этого было мало, например для просмотра и удаления файлов можно настроить сервер Everything (который тоже имеет HTTP-сервер). Тут я понял, для покрытия нескольких специфических и не сложных задачи устанавливать дополнительно 2-3 сервиса нерационально, по этому решил написать отдельное решение.
Далее, речь пойдет про WinAPI, решение, с помощью которого у меня получилось покрыть все мои потребности, а конкретно: удаленная остановка и запуск служб и процессов, вывод метрик, максимально приближенных к диспетчеру задач (список физических и логических дисков, показатели IOps, потребление RAM, нагрузка CPU, количество запущенных процессов, потоков, дескрипторов и т.п.), а так же просматривать список директорий и файлов, их размер и количество, с возможностью удаления.
Как это выглядит на практике. Для установки и запуска данного сервиса я попробовал реализовать два решения. Запуск в виде исполняемого файла (используя модуль ps2exe), в таком варианте можно запускается отдельная консоль, где можно наблюдать весь лог и завершать процесс при закрытии консоли. Второй вариант, это запуск фонового процесса, в таком случае для чтения лога используется файл. Но такое решение не самое удачное, т.к. модуль имеет ограничение, которое позволяет запускать любой скрипт только в PowerShell 5.1 и командлеты из версии Core попросту не буду работать.
Второй вариант, это запуск в качестве службы, процесс установки получилось так же автоматизировать, достаточно на любой машине, где есть доступ в интернет запустить этот скрипт. Настроить свои данные для авторизации и задать свой номер порта (предварительно открыть его в firewall) в конфигурационном файле, после чего, начать взаимодействовать на любой системе, используя Invoke-RestMethod или Curl:

lifailon@hv-devops-01:~$ user="rest"
lifailon@hv-devops-01:~$ pass="api"
lifailon@hv-devops-01:~$ curl -s -X GET -u $user:$pass http://192.168.3.100:8443/api/service/winrm # запрашиваем статус службы WinRM
{
"Name": "WinRM",
"DisplayName": "Служба удаленного управления Windows (WS-Management)",
"Status": "Stopped",
"StartType": "Automatic"
}
lifailon@hv-devops-01:~$ curl -s -X POST -u $user:$pass --data '' http://192.168.3.100:8443/api/service/winrm -H "Status: Start" # запускаем службу
{
"Name": "winrm",
"DisplayName": "Служба удаленного управления Windows (WS-Management)",
"Status": "Running",
"StartType": "Automatic"
}
Пример простого Web-сервера был скорее эксперимент, чем необходимость (очень уж хотелось закрыть старый гештальт). Тем не менее выглядит решение так:

Естественно, для обработки кнопок в браузере не обошлось без JavaScript, особых познаний языка тут не требуется, нашел буквально первый пример в интернете как создать кнопки и обработать их действие при нажатии, ознакомившись с основами HTML-синтаксиса и додумав логику, все получилось сделать достаточно просто. Вот пример с комментариями:
# Типовое условие для проверки вхождения на соответствие метода (GET) и конечной точки (/service)
elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/service") {
# Получаем массив из списока служб, используя кастомную функцию для вывода с подробной информацией
$Services = Get-ServiceDescription *
# Формируем текст HTML-документа, задаем заголовок страницы и открываем тело страницы
$GetService = "<html><head><title>Service</title></head><body>"
# Добавляем заготовленные кнопки, которые перенаправляет на другие url
$GetService += $BodyButtons
# Указываем на создание таблицы и задаем имена столбцов
$GetService += "<table border='1'>"
$GetService += "<tr><th>Name</th><th>Status</th><th>Action</th><th>Start Type</th></tr>"
# Передаем в цикл список служб и забираем значения
foreach ($Service in $Services) {
$name = "<b>$($Service.Name)</b>"
$status = $Service.Status
# Проверяем статус службы, если работает, красим в зеленый цвет
if ($status -eq "Running") {
$status = "<font color='green'><b>$status</b></font>"
} else {
$status = "<font color='red'><b>$status</b></font>"
}
$StartType = $Service.StartType
# Заполняем значения столбцов, по анологии с наименованием столбцов (в блоке <tr>)
$GetService += "<tr><td>$name</td><td>$status</td>"
# Создаем кпноки, которые при нажатии ссылаются на функции startService и stopService, которые в качестве параметра передают наименование службы
$GetService += "<td><button onclick='startService(""$($Service.Name)"")'>Start</button> "
$GetService += "<button onclick='stopService(""$($Service.Name)"")'>Stop</button></td>"
$GetService += "<td>$StartType</td></tr>"
}
$GetService += "</table>"
$GetService += '
# Формируем в блоке <script> функции, для обработки нажатия на кнопки
<script>
function startService(serviceName) {
sendServiceAction("Start", serviceName);
}
function stopService(serviceName) {
sendServiceAction("Stop", serviceName);
}
# Данная функция принимает действие и отправляет соответствующий POST-запрос, для его обработки другой конечной точкой
function sendServiceAction(action, serviceName) {
var request = new XMLHttpRequest();
request.open("POST", "/api/service/" + serviceName, true);
# В заголовок запроса передаем статус с содержимым действия (Status: <Stop/Start>) и обновляем страницу (reload)
request.setRequestHeader("Status", action);
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status === 200) {
console.log("True");
location.reload();
}
};
request.send();
}
</script>
</body></html>
'
# Передаем сформированные данные и код ответа в функцию, для отправки ответва клиенту
Send-Response -Data $GetService -Code 200 -v2
}
Для сравнения интерфейса, приведу пример управления службами, используя простой Jenkins Pipeline. Из явных преимуществ, такой интерфейс универсален для обеих систем (Windows и Linux), логика преимущественно на PowerShell и Bash (в моем случае), а доступ настраивается централизованно через Ansible, где в свою очередь используя ssh и winrm. Такой доступ можно заменить на REST-запросы, при наличии подобного сервера на каждой удаленной машине (например, в виде установленной службы). Безусловно, это более современное и правильное решение, но не взаимозаменяемое, речь только про интерфейс взаимодействия, где мы можем в одном интерфейсе управлять сразу с несколькими машинами.

По аналогии со службами, обработал остановку и запуск процессов.

Из интересного на мой взгляд, написал простую функцию для поиска исполняемого файла в системе, который отвечает за запуск процесса конкретного приложения. Если такой процесс не получается найти, то мы получим в ответ код 400: Bad Request. Process <$ProcessName> could not be found. В таком случае, можно воспользоваться заголовком Path, который принимает путь до исполняемого файла.
function Find-Process {
param (
$ProcessName
)
$ProcessPath = (Get-ChildItem "C:\Program Files" | Where-Object Name -match $ProcessName).FullName
if ($null -eq $ProcessPath) {
$ProcessPath = (Get-ChildItem "C:\Program Files (x86)" | Where-Object Name -match $ProcessName).FullName
}
if ($null -eq $ProcessPath) {
$ProcessPath = (Get-ChildItem "$home\AppData\Roaming" | Where-Object Name -match $ProcessName).FullName
}
$ProcessNameExec = "$ProcessName"+".exe"
(Get-ChildItem $ProcessPath -Recurse | Where-Object Name -eq $ProcessNameExec).FullName
}
> Find-Process qbittorrent
C:\Program Files\qBittorrent\qbittorrent.exe
> Find-Process nmap
C:\Program Files (x86)\Nmap\nmap.exe
Find-Process telegram
C:\Users\lifailon\AppData\Roaming\Telegram Desktop\Telegram.exe
Для сбора метрик используется CIM (Common Information Model). Сам скрипт сервера, описание с примерами, как и набор функций опубликованы на GitHub.

Так как PowerShell Core является кросс-платформенным решением, class System.Net.HttpListener работает и в системе Linux, используя такую же логику и возможности сразу нескольких языков (например, Bash), можно управлять службами на платформе Windows через systemctl используя REST API.
Что важно, при возникновении ошибки, мне хотелось, что бы данный сервер только логировал ее, но при этом продолжал функционировать (фактически, перезапускался). Для это достаточно вынести слушателя с циклом в отдельную функцию и запускать ее внутри еще одного бесконечного цикла, где присутствует дополнительная обработка ошибок в блоках try-catch-finally.
Вот базовый пример, без лишнего кода с описанием:
# Заполняем переменны с номером порта и данными для авторизации
$port = 8443
$user = "rest"
$pass = "api"
# Формируем строку Base64 из данных логина и пароля
$cred = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("${user}:${pass}"))
# Функция для логирования запросов
function Get-Log {
### Debug (Get all Request, Headers and Response parameters):
# Используя содержимое запросов (Request), чтение передаваемых заголовков и ответа (Response), можно расширить возможности логирования для отладки процесса
# $context.Request | Out-Default
# foreach ($header in $context.Request.Headers) {
# Write-Host "$header = $($context.Request.Headers[$header])"
# }
# $context.Response | Out-Default
# Забираем содержимое из запроса: адрес клиента, наименование агента, метод и url конечной точки
$remote_host = $context.Request.RemoteEndPoint
$client_agent = $context.Request.UserAgent
$method = $context.Request.HttpMethod
$endpoint = $context.Request.RawUrl
$response_code = $context.Response.StatusCode
$date = Get-Date -Format "dd.MM.yyyy hh:mm:ss"
# Выводим в консоль или в файл
"$date $remote_host $client_agent => $method $endpoint => $response_code"
# "$date $remote_host $client_agent => $method $endpoint => $response_code" | Out-File $Log_Path -Encoding utf8 -Append
}
# Функция для ответа клиенту
function Send-Response {
param (
$Data,
[int]$Code
)
# Проверяем код ответа, если он равен 200 (успех), то конвертируем данные перед отправкой клиенту
if ($Code -eq 200) {
# Дополнительно можем проверить название агента на клиентской стороне, который может выступать в роли браузера или явно задан тип данных HTML
if (($context.Request.UserAgent -match "Chrome") -or ($context.Request.ContentType -match "html")) {
# Конвертируем полученные данные в HTML и указываем тип контента в ответе
$Data = $Data | ConvertTo-Html
$context.Response.ContentType = "text/html; charset=utf-8"
}
# Далее проверяем только тип контента из заголовка (если он задан явным образом), и конвертируем вывод в соответствующий тип данных
elseif ($context.Request.ContentType -match "xml") {
$Data = ($Data | ConvertTo-Xml).OuterXml
$context.Response.ContentType = "text/xml; charset=utf-8"
}
elseif ($context.Request.ContentType -match "csv") {
$Data = $Data | ConvertTo-Csv
$context.Response.ContentType = "text/csv; charset=utf-8"
}
# По умолчанию, конвертируем в JSON
else {
$Data = $Data | ConvertTo-Json
$context.Response.ContentType = "text/json; charset=utf-8"
}
}
# Указываем код статуса для ответа
$context.Response.StatusCode = $Code
# Преобразуем данные в массив байтов, используя кодировку UTF-8 (особенно важно, при передачи в формате HTML)
$buffer = [System.Text.Encoding]::UTF8.GetBytes($Data)
# Выполняем функцию логирования
Get-Log
# Забираем число количества байт буффера для записи в поток, который передается в параметр ответа. Это является важным условием, что все даныне были переданы и прочитаны на стороне клиента.
$context.Response.ContentLength64 = $buffer.Length
# Передаем массив байтов (наш буффер ответа с данными) в поток ответа, обязательно нужно передать параметры смешения (если бы нужно было начать запись с определенного места в массиве) и длинны буффера
$context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
# Данный метод обновляет буфер вывода, убеждаясь, что все данные из буфера отправлены клиенту
$context.Response.OutputStream.Flush()
# Закрываем поток ответа
$context.Response.OutputStream.Close()
}
# Создаем сокет слушателя
Add-Type -AssemblyName System.Net.Http
$http = New-Object System.Net.HttpListener
# Указываем адрес слушателя (+ что бы слушать на всех интерфейсах) и порт
$http.Prefixes.Add("http://+:$port/")
# Указываем использование базового метода аутентификации
$http.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Basic
# Запускаем сокет (начинаем слушать запросы на указанном порту)
$http.Start()
# Обработчик try-finally нужен для закрытия сокета в случае его непредвиденного завершения
try {
# Отправляем в бесконечный цикл прослушивание входящих запросов, пока свойство IsListening объекта $http равно true
while ($http.IsListening) {
# Используем асинхронный режим, для ожидания новых запросов
$contextTask = $http.GetContextAsync()
# Синхронно ожидает завершения асинхронной задачи, чтобы дождаться завершения асинхронной операции, прежде чем продолжить выполнение кода
while (-not $contextTask.AsyncWaitHandle.WaitOne(200)) { }
# Получение результата асинхронной задачи
$context = $contextTask.GetAwaiter().GetResult()
# Проверяем полученные данные авторизации (в формате Base64) из заголовка запроса на соответветствие переменной $cred
$CredRequest = $context.Request.Headers["Authorization"]
# Write-Host $CredRequest
$CredRequest = $CredRequest -replace "Basic\s"
if ( $CredRequest -ne $cred ) {
# Если авторизационные данные не прошли проверку (неверно передан логин или пароль), передаем в функцию ответа параметры с текстом ошибки и кодом возравата 401
$Data = "Unauthorized (login or password is invalid)"
Send-Response -Data $Data -Code 401
}
else {
# Если авторизация прошла, проверяем метод и url конечной точки на соответветствие, что бы его обработать
if ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/service") {
$GetService = Get-Service -ErrorAction Ignore
Send-Response -Data $GetService -Code 200
}
# Дальше по аналогии дополнительными условиями (elseif) добавляем обработку других конечных точек
elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/process") {
$GetService = Get-Process
Send-Response -Data $GetService -Code 200
}
# Если не одно из методов не прошел соответветствие, отправляем ответ с кодом 405
elseif ($context.Request.HttpMethod -ne "GET") {
$Data = "Method not allowed"
Send-Response -Data $Data -Code 405
}
# Если не одно из условий не подошло, отправляем ответ с кодом 404
else {
$Data = "Not found endpoint"
Send-Response -Data $Data -Code 404
}
}
}
}
finally {
# Освобождаем сокет
$http.Stop()
}

Итог. Взяв за основу такой скелет и доработав под себя логику, можно автоматизировать процессы конкретного приложения, у которого не предусмотрен внешний интерфейс для подобных задач. В свою очередь, на стороне клиента взаимодействовать с ним, используя любой удобный интерфейс с REST-клиентом.