sync: Windmill-State übernehmen + neue Reporting-Flows

- Dateien nach Windmill-Naming-Konvention umbenannt (ssh-key_aus_db_testen,
  flow-fehler_per_nextcloud_talk_melden, bitwarden_(fallback))
- testpause-Schritt aus flow.yaml entfernt (Debugging abgeschlossen)
- Neue Flows: f/Reporting/exchange_logins, f/Reporting/run_sql_events
- mail_to_talk: Dateinamen nach Windmill-Konvention synchronisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Serfling
2026-05-07 13:13:35 +02:00
parent 4e19c41cd2
commit d22ef502ed
40 changed files with 1077 additions and 89 deletions
@@ -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,44 @@
{
"dependencies": {
"mysql2": "latest"
}
}
//bun.lock
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"mysql2": "latest",
},
},
},
"packages": {
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"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.22.3", "", { "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-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA=="],
"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.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
}
}
@@ -0,0 +1,59 @@
summary: ''
description: ''
lock: '!inline
f/Reporting/exchange_logins__flow/einzelnen_login-eintrag_einfügen.script.lock'
kind: script
schema:
$schema: https://json-schema.org/draft/2020-12/schema
type: object
properties:
database:
type: object
description: ''
default: null
format: resource-mysql
properties:
database:
type: string
description: ''
originalType: string
host:
type: string
description: ''
originalType: string
password:
type: string
description: ''
originalType: string
port:
type: number
description: ''
user:
type: string
description: ''
originalType: string
record:
type: object
description: ''
default: null
format: resource-login_record
properties:
ipaddress:
type: string
description: ''
originalType: string
lastlogon:
type: string
description: ''
originalType: string
memberof:
type: string
description: ''
originalType: string
username:
type: string
description: ''
originalType: string
required:
- database
- record
@@ -0,0 +1,39 @@
type Mysql = {
host: string;
port: number;
user: string;
password: string;
database: string;
};
type LoginRecord = {
username: string;
lastaccess: string;
ipaddress: string;
memberof: 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.memberof]
);
return { inserted: true };
} finally {
await conn.end();
}
}
@@ -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 Exchange 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 '%EX%' 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,
});
}
}
console.log(
`Found ${dbServers.length} EX servers in DB, ${clients.length} connected rport.io clients, ${matched.length} matched`
);
return matched;
}
@@ -0,0 +1,44 @@
{
"dependencies": {
"mysql2": "latest"
}
}
//bun.lock
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"mysql2": "latest",
},
},
},
"packages": {
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"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.22.3", "", { "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-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA=="],
"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.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
}
}
@@ -0,0 +1,53 @@
summary: ''
description: ''
lock: '!inline f/Reporting/exchange_logins__flow/ex-server_mit_rport.script.lock'
kind: script
schema:
$schema: https://json-schema.org/draft/2020-12/schema
type: object
properties:
database:
type: object
description: ''
default: null
format: resource-mysql
properties:
database:
type: string
description: ''
originalType: string
host:
type: string
description: ''
originalType: string
password:
type: string
description: ''
originalType: string
port:
type: number
description: ''
user:
type: string
description: ''
originalType: string
rportio_api_token:
type: string
description: ''
default: null
originalType: string
rportio_base_url:
type: string
description: ''
default: null
originalType: string
rportio_username:
type: string
description: ''
default: null
originalType: string
required:
- database
- rportio_base_url
- rportio_username
- rportio_api_token
@@ -0,0 +1,91 @@
summary: Exchange Logins Collector
description: >
Lädt alle aktiven EX-Server aus bronze.server, gleicht sie mit den
verbundenen rport.io Clients ab (per Hostname oder IP), fragt auf jedem
Server per PowerShell die AD-Gruppe G-Exchange-User ab (username ohne
E-Mail, lastlogon, ipaddress vom Server) und schreibt die Ergebnisse in
bronze.services.reporting.
value:
modules:
- id: find_ex_clients
summary: EX-Server mit rport.io Clients abgleichen
value:
type: rawscript
content: '!inline ex-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 ex-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
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_ex_clients
parallel: false
skip_failures: true
schema:
$schema: https://json-schema.org/draft/2019-09/schema
type: object
properties: {}
required: []
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,119 @@
type LoginRecord = {
username: string;
lastaccess: string;
ipaddress: string;
memberof: string;
};
export async function main(
rportio_base_url: string,
rportio_username: string,
rportio_api_token: string,
client_id: string,
server_ip: string
): Promise<LoginRecord[]> {
const psScript = `
$ErrorActionPreference = 'SilentlyContinue'
$groupName = 'G-Exchange-User'
$serverIp = '${server_ip}'
$firstOfMonth = (Get-Date -Day 1).ToString('yyyy-MM-dd 00:00:00')
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn -ErrorAction SilentlyContinue
$members = @()
try {
$members = Get-ADGroupMember -Identity $groupName -Recursive -ErrorAction Stop |
Where-Object { $_.objectClass -eq 'user' }
} catch {}
$result = [System.Collections.Generic.List[object]]::new()
foreach ($m in $members) {
$mailbox = $null
try {
$mailbox = Get-Mailbox -Identity $m.SamAccountName -RecipientTypeDetails UserMailbox -ErrorAction Stop
} catch { continue }
$lastaccess = $firstOfMonth
try {
$stats = Get-MailboxStatistics -Identity $mailbox.Identity -ErrorAction Stop
if ($stats.LastLogonTime) {
$lastaccess = $stats.LastLogonTime.ToString('yyyy-MM-dd HH:mm:ss')
}
} catch {}
$result.Add([PSCustomObject]@{
username = $mailbox.SamAccountName
lastaccess = $lastaccess
ipaddress = $serverIp
memberof = $groupName
})
}
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;
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,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,39 @@
summary: ''
description: ''
lock: '!inline f/Reporting/exchange_logins__flow/powershell_via_rport.script.lock'
kind: script
schema:
$schema: https://json-schema.org/draft/2020-12/schema
type: object
properties:
client_id:
type: string
description: ''
default: null
originalType: string
rportio_api_token:
type: string
description: ''
default: null
originalType: string
rportio_base_url:
type: string
description: ''
default: null
originalType: string
rportio_username:
type: string
description: ''
default: null
originalType: string
server_ip:
type: string
description: ''
default: null
originalType: string
required:
- rportio_base_url
- rportio_username
- rportio_api_token
- client_id
- server_ip
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,30 @@
export async function main(results_array: any[], event_name: string = 'all_create', subject_prefix: string = '') {
const total = Array.isArray(results_array) ? results_array.length : 0
const failed = Array.isArray(results_array) ? results_array.filter((r: any) => r && r.success === false).length : 0
const succeeded = total - failed
const subject = `${subject_prefix ? subject_prefix + ' ' : ''}Ergebnisse für Event ${event_name}: ${succeeded}/${total} erfolgreich`
const lines: string[] = []
lines.push(`Event: ${event_name}`)
lines.push(`Erfolgreich: ${succeeded}/${total}`)
lines.push('')
for (let i = 0; i < total; i++) {
const r: any = results_array[i]
if (!r) continue
lines.push(`CALL ${i + 1}: ${r.call}`)
if (r.success) {
let snippet = ''
try {
snippet = JSON.stringify(r.result)
if (snippet.length > 1000) snippet = snippet.slice(0, 1000) + '...'
} catch { snippet = '[unserialisierbar]' }
lines.push(' Status: OK')
lines.push(` Result: ${snippet}`)
} else {
lines.push(' Status: FEHLER')
lines.push(` Error: ${r.error}`)
}
lines.push('')
}
const body = lines.join('\n')
return { subject, body }
}
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,18 @@
export async function main(event_definition: string) {
if (!event_definition || typeof event_definition !== 'string') return []
// Some MySQL store event body as full CREATE EVENT ... DO ...; we only need inside DO ...
let def = event_definition
const doMatch = def.match(/\bDO\s+(BEGIN[\s\S]*END|[^;]+);?/i)
if (doMatch) {
def = doMatch[1]
}
let body = def
.replace(/DELIMITER\s+[^\n]+/gi, ' ')
.replace(/\bBEGIN\b/gi, ' ')
.replace(/\bEND\b/gi, ' ')
.replace(/\s+/g, ' ')
const regex = /CALL\s+[^;]+;/gi
const matches = body.match(regex) || []
return matches.map((s) => s.trim())
}
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,21 @@
export async function main(rows: any) {
// Normalize possible hub script return shapes
let arr: any[] = []
if (Array.isArray(rows)) arr = rows
else if (rows && Array.isArray(rows.rows)) arr = rows.rows
else if (rows && Array.isArray(rows.result)) arr = rows.result
else if (rows && Array.isArray(rows.data)) arr = rows.data
let def = ''
try {
if (arr.length > 0) {
const r = arr[0]
def = r.EVENT_DEFINITION || r.event_definition || ''
if (!def && r.EVENT_BODY) {
// Some MySQL variants expose body differently; just in case
def = r.EVENT_BODY
}
}
} catch {}
return { event_definition: def, row_count: arr.length }
}
+105
View File
@@ -0,0 +1,105 @@
summary: Run SQL Events for Reporting
description: ''
value:
modules:
- id: get_event_def_rows
summary: Liest EVENT_DEFINITION für das MySQL-Event (Zeilen vom Hubskript)
value:
type: script
input_transforms:
mysql_conn:
type: static
value: $res:u/sebastianserfling/fascinating_mysql
query:
type: javascript
expr: "`SELECT EVENT_DEFINITION FROM information_schema.EVENTS WHERE EVENT_NAME
= '${flow_input.event_name ?? 'all_create'}' ORDER BY
(EVENT_SCHEMA = DATABASE()) DESC LIMIT 1`"
path: hub/17540/mysql/execute_query
- id: extract_event_def
summary: Extrahiert EVENT_DEFINITION-String aus den Zeilen
value:
type: rawscript
content: '!inline extrahiert_event_definition-string_aus_den_zeilen.ts'
input_transforms:
rows:
type: javascript
expr: results.get_event_def_rows
lock: '!inline extrahiert_event_definition-string_aus_den_zeilen.lock'
language: bun
- id: parse_calls
summary: Extrahiert alle CALL-Statements aus dem EVENT_DEFINITION-Body
value:
type: rawscript
content: '!inline
extrahiert_alle_call-statements_aus_dem_event_definition-body.ts'
input_transforms:
event_definition:
type: javascript
expr: results.extract_event_def.event_definition
lock: '!inline
extrahiert_alle_call-statements_aus_dem_event_definition-body.lock'
language: bun
- id: loop_calls
summary: Führt jede CALL einzeln aus und sendet bei Fehler Pushover
value:
type: forloopflow
modules:
- id: run_call
summary: Führt eine einzelne CALL aus, sendet bei Fehler Pushover und liefert
Ergebnis
value:
type: rawscript
content: '!inline
führt_eine_einzelne_call_aus,_sendet_bei_fehler_pushover_und_liefert_ergebnis.ts'
input_transforms:
call_sql:
type: javascript
expr: flow_input.iter.value
database_resource_path:
type: static
value: $res:u/sebastianserfling/fascinating_mysql
pushover_token:
type: javascript
expr: flow_input.pushover_token
pushover_user:
type: javascript
expr: flow_input.pushover_user
lock: '!inline
führt_eine_einzelne_call_aus,_sendet_bei_fehler_pushover_und_liefert_ergebnis.lock'
language: bun
iterator:
type: javascript
expr: results.parse_calls || []
parallel: false
skip_failures: true
squash: false
- id: compose_email
summary: Erstellt Betreff und Body mit allen Ergebnissen der CALLs
value:
type: rawscript
content: '!inline erstellt_betreff_und_body_mit_allen_ergebnissen_der_calls.ts'
input_transforms:
event_name:
type: javascript
expr: flow_input.event_name ?? 'all_create'
results_array:
type: javascript
expr: results.loop_calls
subject_prefix:
type: javascript
expr: flow_input.email_subject_prefix ?? ''
lock: '!inline erstellt_betreff_und_body_mit_allen_ergebnissen_der_calls.lock'
language: bun
groups: []
schema:
$schema: https://json-schema.org/draft/2020-12/schema
type: object
order:
- event_name
properties:
event_name:
type: string
description: Name des MySQL Events
default: all_create
required: []
@@ -0,0 +1,19 @@
{
"dependencies": {
"windmill-client": "latest"
}
}
//bun.lock
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"windmill-client": "latest",
},
},
},
"packages": {
"windmill-client": ["windmill-client@1.614.0", "", {}, "sha512-skpKI9mJXhUdwc9gZbounVclw22HTpXlhCAkGnkEAqys3xkRzA7v8/NY2Xt2WQrWV7GIN4ZGkOAgv2rk1r36hg=="],
}
}
@@ -0,0 +1,34 @@
import * as wmill from 'windmill-client'
export async function main(database_resource_path: string, call_sql: string, pushover_token?: string, pushover_user?: string) {
let success = false
let result: any = null
let errorMsg = ''
try {
// Pass the resource path string directly so the hub script can resolve it itself
const rows = await wmill.runScriptByPath('hub/17540/mysql/execute_query', {
mysql_conn: database_resource_path,
query: call_sql
})
success = true
result = rows
} catch (err: any) {
errorMsg = err?.message || String(err)
if (pushover_token && pushover_user) {
try {
const body = new URLSearchParams({
token: String(pushover_token),
user: String(pushover_user),
title: 'MySQL CALL fehlgeschlagen',
message: `Fehler bei CALL: ${call_sql}\nFehlermeldung: ${errorMsg}`
})
await fetch('https://api.pushover.net/1/messages.json', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
})
} catch {}
}
}
return { call: call_sql, success, result, error: success ? null : errorMsg }
}