Initial commit: Windmill workspace sync

Scripts, flows, apps, resources and resource types from the Windmill workspace.
API token excluded via .gitignore (config/).
This commit is contained in:
Sebastian Serfling
2026-04-24 09:06:07 +02:00
commit 2b5d29ef67
302 changed files with 9229 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
summary: null
display_name: Reporting
extra_perms:
sebastianserfling@stines.de: true
owners:
- sebastianserfling@stines.de
@@ -0,0 +1,44 @@
{
"dependencies": {
"mysql2": "latest"
}
}
//bun.lock
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"mysql2": "latest",
},
},
},
"packages": {
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
"mysql2": ["mysql2@3.21.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-CYNKIuhnalXHTa4gonZ+KhzLESKllvo1qQIDYUVuatpN4NgMk+lsA3WwHYno5AS4PACUiD2qEmiVD9pr3bXWOw=="],
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
}
}
@@ -0,0 +1,39 @@
type Mysql = {
host: string;
port: number;
user: string;
password: string;
database: string;
};
type LoginRecord = {
username: string;
lastaccess: string;
ipaddress: string;
groups: string;
};
export async function main(
database: Mysql,
record: LoginRecord
): Promise<{ inserted: boolean }> {
const mysql2 = await import("mysql2/promise");
const conn = await mysql2.createConnection({
host: database.host,
port: database.port,
user: database.user,
password: database.password,
database: database.database,
});
try {
await conn.execute(
"INSERT INTO `bronze.services.reporting` (username, lastaccess, ipaddress, add_date, memberof) VALUES (?, ?, ?, NOW(), ?)",
[record.username, record.lastaccess, record.ipaddress, record.groups]
);
return { inserted: true };
} finally {
await conn.end();
}
}
+97
View File
@@ -0,0 +1,97 @@
summary: RDP Logins Collector
description: >
Lädt alle aktiven RDS-Server aus bronze.server, gleicht sie mit den
verbundenen rport.io Clients ab (per Hostname oder IP), führt auf jedem den
PS-Login-Collector aus und schreibt die Ergebnisse in
bronze.services.reporting.
value:
modules:
- id: find_rds_clients
summary: RDS-Server mit rport.io Clients abgleichen
value:
type: rawscript
content: '!inline rds-server_mit_rport.io_clients_abgleichen.ts'
input_transforms:
database:
type: static
value: $res:u/sebastianserfling/fascinating_mysql
rportio_api_token:
type: static
value: $var:f/Reporting/rportio_api_token
rportio_base_url:
type: static
value: $var:f/Reporting/rportio_base_url
rportio_username:
type: static
value: $var:f/Reporting/rportio_username
lock: '!inline rds-server_mit_rport.io_clients_abgleichen.lock'
language: bun
- id: process_servers
summary: Pro Server Logins sammeln und speichern
value:
type: forloopflow
modules:
- id: execute_ps
summary: PowerShell via rport.io ausführen
value:
type: rawscript
content: '!inline powershell_via_rport.io_ausführen.ts'
input_transforms:
client_id:
type: javascript
expr: flow_input.iter.value.rport_client_id
hours_back:
type: javascript
expr: flow_input.hours_back ?? 1
rportio_api_token:
type: static
value: $var:f/Reporting/rportio_api_token
rportio_base_url:
type: static
value: $var:f/Reporting/rportio_base_url
rportio_username:
type: static
value: $var:f/Reporting/rportio_username
server_ip:
type: javascript
expr: flow_input.iter.value.ipaddress
lock: '!inline powershell_via_rport.io_ausführen.lock'
language: bun
- id: insert_logins
summary: Login-Einträge in MySQL speichern
value:
type: forloopflow
modules:
- id: insert_login
summary: Einzelnen Login-Eintrag einfügen
value:
type: rawscript
content: '!inline einzelnen_login-eintrag_einfügen.ts'
input_transforms:
database:
type: static
value: $res:u/sebastianserfling/fascinating_mysql
record:
type: javascript
expr: flow_input.iter.value
lock: '!inline einzelnen_login-eintrag_einfügen.lock'
language: bun
iterator:
type: javascript
expr: results.execute_ps
parallel: false
skip_failures: false
iterator:
type: javascript
expr: results.find_rds_clients
parallel: false
skip_failures: true
schema:
$schema: https://json-schema.org/draft/2019-09/schema
type: object
properties:
hours_back:
type: integer
description: 'Wie viele Stunden zurückschauen (Standard: 1)'
default: 1
required: []
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,133 @@
type LoginRecord = {
username: string;
lastaccess: string;
ipaddress: string;
groups: string;
};
export async function main(
rportio_base_url: string,
rportio_username: string,
rportio_api_token: string,
client_id: string,
server_ip: string,
hours_back: number = 1
): Promise<LoginRecord[]> {
const psScript = `
$ErrorActionPreference = 'SilentlyContinue'
$startTime = (Get-Date).AddHours(-${hours_back})
$endTime = Get-Date
$filterHashTable = @{
LogName = 'Security'
Id = 4624
StartTime = $startTime
EndTime = $endTime
}
$events = Get-WinEvent -FilterHashtable $filterHashTable -ErrorAction SilentlyContinue
$userLogins = @{}
if ($events) {
foreach ($event in $events) {
$eventDetails = [xml]$event.ToXml()
$timeCreated = $event.TimeCreated
$username = $eventDetails.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' } | Select-Object -ExpandProperty '#text'
$logonType = $eventDetails.Event.EventData.Data | Where-Object { $_.Name -eq 'LogonType' } | Select-Object -ExpandProperty '#text'
if ($logonType -ne '10' -or $username -like 'DWM*' -or $username -like '*UMFD*') { continue }
$ipaddress = '${server_ip}'
$formattedTime = $timeCreated.ToString('yyyy-MM-dd HH:mm:ss')
if (-not $userLogins.ContainsKey($username) -or $userLogins[$username]._raw -lt $timeCreated) {
$userLogins[$username] = [PSCustomObject]@{
lastaccess = $formattedTime
username = $username
ipaddress = $ipaddress
_raw = $timeCreated
}
}
}
}
$result = [System.Collections.Generic.List[object]]::new()
foreach ($entry in $userLogins.GetEnumerator()) {
$u = $entry.Value
$adGroups = $null
try {
$adGroups = @(Get-ADPrincipalGroupMembership -Identity $u.username -ErrorAction Stop | Select-Object -ExpandProperty Name)
} catch {}
if (-not $adGroups -or $adGroups.Count -eq 0) { $adGroups = @('G-RDP-User') }
foreach ($group in $adGroups) {
$result.Add([PSCustomObject]@{
username = $u.username
lastaccess = $u.lastaccess
ipaddress = $u.ipaddress
groups = $group
})
}
}
if ($result.Count -eq 0) { Write-Output '[]' } else { $result | ConvertTo-Json -Depth 3 -Compress }
`.trim();
// rport.io powershell interpreter executes the script directly as PS code
const command = psScript;
// API token is used as Basic Auth password (bypasses 2FA)
const auth = Buffer.from(`${rportio_username}:${rportio_api_token}`).toString("base64");
const headers: Record<string, string> = {
Authorization: `Basic ${auth}`,
"Content-Type": "application/json",
};
const tlsOpts = { tls: { rejectUnauthorized: false } };
// Submit command to rport.io
// @ts-ignore - Bun-specific TLS option for self-signed certificates
const execResp = await fetch(
`${rportio_base_url}/api/v1/clients/${client_id}/commands`,
{
method: "POST",
headers,
body: JSON.stringify({ command, interpreter: "powershell", timeout_sec: 120 }),
...tlsOpts,
}
);
if (!execResp.ok) {
const text = await execResp.text();
throw new Error(`rport.io execute failed [${execResp.status}]: ${text}`);
}
const execData = await execResp.json();
const jid: string = execData?.data?.jid;
if (!jid) throw new Error(`No job ID from rport.io: ${JSON.stringify(execData)}`);
// Poll until finished (max 120s)
let cmdResult: Record<string, unknown> | null = null;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 2000));
// @ts-ignore - Bun-specific TLS option
const statusResp = await fetch(
`${rportio_base_url}/api/v1/clients/${client_id}/commands/${jid}`,
{ headers, ...tlsOpts }
);
if (!statusResp.ok) continue;
const statusData = await statusResp.json();
const cmd = statusData?.data as Record<string, unknown>;
if (cmd?.finished_at) {
cmdResult = cmd;
break;
}
}
if (!cmdResult) throw new Error("Timeout waiting for rport.io command result");
const status = cmdResult.status as string;
if (status === "failed" || status === "unknown") {
const result = cmdResult.result as Record<string, string> ?? {};
throw new Error(`PowerShell failed [${status}]: ${cmdResult.error ?? result.stderr ?? ""}`);
}
const result = cmdResult.result as Record<string, string> ?? {};
const stdout = (result.stdout ?? "").trim();
if (!stdout || stdout === "[]") return [];
try {
const parsed = JSON.parse(stdout);
return Array.isArray(parsed) ? parsed : [parsed];
} catch {
throw new Error(`Failed to parse PowerShell JSON output: ${stdout}`);
}
}
@@ -0,0 +1,44 @@
{
"dependencies": {
"mysql2": "latest"
}
}
//bun.lock
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"mysql2": "latest",
},
},
},
"packages": {
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
"mysql2": ["mysql2@3.21.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-CYNKIuhnalXHTa4gonZ+KhzLESKllvo1qQIDYUVuatpN4NgMk+lsA3WwHYno5AS4PACUiD2qEmiVD9pr3bXWOw=="],
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
}
}
@@ -0,0 +1,88 @@
type Mysql = {
host: string;
port: number;
user: string;
password: string;
database: string;
};
type RportClient = {
rport_client_id: string;
hostname: string;
ipaddress: string;
};
export async function main(
database: Mysql,
rportio_base_url: string,
rportio_username: string,
rportio_api_token: string
): Promise<RportClient[]> {
const mysql2 = await import("mysql2/promise");
// 1. Query MySQL for all active RDS servers
const conn = await mysql2.createConnection({
host: database.host,
port: database.port,
user: database.user,
password: database.password,
database: database.database,
});
const [rows] = await conn.execute(
"SELECT hostname, privat_ipaddress FROM `bronze.server` WHERE services LIKE '%RDS%' AND (disable_date IS NULL OR disable_date > NOW())"
);
await conn.end();
const dbServers = rows as Array<{ hostname: string; privat_ipaddress: string }>;
if (dbServers.length === 0) return [];
// Build lookup maps for fast matching (hostname → DB row)
const byHostname = new Map(dbServers.map((s) => [s.hostname.toLowerCase(), s]));
const byIp = new Map(dbServers.map((s) => [s.privat_ipaddress, s]));
// 2. Query rport.io for all connected clients
const auth = Buffer.from(`${rportio_username}:${rportio_api_token}`).toString("base64");
const headers = {
Authorization: `Basic ${auth}`,
"Content-Type": "application/json",
};
// @ts-ignore - Bun-specific TLS option to allow self-signed certificates
const resp = await fetch(
`${rportio_base_url}/api/v1/clients?filter[connection_state]=connected&fields[clients]=id,name,hostname,ipv4&page[limit]=500`,
{ headers, tls: { rejectUnauthorized: false } }
);
if (!resp.ok) {
const err = await resp.text();
throw new Error(`rport.io clients list failed [${resp.status}]: ${err}`);
}
const data = await resp.json();
const clients = data?.data ?? [];
// 3. Match rport.io clients against DB server list (hostname or IP)
const matched: RportClient[] = [];
for (const client of clients) {
const rportHostname = (client.hostname ?? "").toLowerCase();
const rportIps: string[] = client.ipv4 ?? [];
const dbRow = byHostname.get(rportHostname)
?? rportIps.map((ip) => byIp.get(ip)).find(Boolean);
if (dbRow) {
matched.push({
rport_client_id: client.id,
hostname: client.hostname ?? client.name,
ipaddress: dbRow.privat_ipaddress, // use IP from DB, not from PS script
});
}
}
console.log(
`Found ${dbServers.length} RDS servers in DB, ${clients.length} connected rport.io clients, ${matched.length} matched`
);
return matched;
}