@continuoussecuritytooling/keycloak-reporter 1.1.9 → 1.2.0-3090

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -74,7 +74,7 @@ jobs:
74
74
 
75
75
  - name: Create kind cluster
76
76
  if: steps.list-changed.outputs.changed == 'true'
77
- uses: helm/kind-action@v1.13.0
77
+ uses: helm/kind-action@v1.14.0
78
78
 
79
79
  - name: Run chart-testing (install - no further args)
80
80
  if: steps.list-changed.outputs.changed == 'true'
@@ -88,7 +88,7 @@ jobs:
88
88
  --from-literal=clientSecret=test
89
89
  ct install --target-branch ${{ github.event.repository.default_branch }} --namespace kc-reporter --helm-extra-set-args "-f charts/keycloak-reporter/ci.values.yaml"
90
90
 
91
- - uses: actions/upload-artifact@v6
91
+ - uses: actions/upload-artifact@v7
92
92
  with:
93
93
  name: dist-folder
94
94
  path: dist
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # [1.2.0](https://github.com/ContinuousSecurityTooling/keycloak-reporter/compare/v1.1.9...v1.2.0) (2026-03-10)
2
+
3
+
4
+ ### Features
5
+
6
+ * **Webhooks:** Adding generic webhook support ([c79ca91](https://github.com/ContinuousSecurityTooling/keycloak-reporter/commit/c79ca917c4debd0796bcd10117f3c7b5924b5806)), closes [#240](https://github.com/ContinuousSecurityTooling/keycloak-reporter/issues/240)
7
+
8
+
9
+
1
10
  ## [1.1.9](https://github.com/ContinuousSecurityTooling/keycloak-reporter/compare/v1.1.8...v1.1.9) (2026-02-14)
2
11
 
3
12
 
package/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM node:24.13.1-slim
1
+ FROM node:24.14.0-slim
2
2
 
3
3
  ARG BUILD_DATE
4
4
  ARG APP_VERSION
package/README.md CHANGED
@@ -75,7 +75,7 @@ You can also provider a config file via env var `CONFIG_FILE` and then just prov
75
75
  CONFIG_FILE==$(pwd)/e2e/fixtures/config.json kc-reporter listClients
76
76
  ```
77
77
 
78
- ### Post to Slack or Teams
78
+ ### Post to Slack, Teams or Generic Webhook
79
79
 
80
80
  When using this command:
81
81
  ```
@@ -90,3 +90,28 @@ kc-reporter listUsers <Keycloak_Root_URL> <Client_ID> <Client_Secret> --format=j
90
90
  ```
91
91
  the following entry in slack will be created:
92
92
  ![Team Sample](.docs/webhook-teams-sample.png)
93
+
94
+ #### Generic Webhook
95
+
96
+ The `generic` webhook type posts a plain JSON payload via HTTP POST to any URL, making it compatible with custom integrations, automation platforms, or any HTTP endpoint.
97
+
98
+ ```sh
99
+ kc-reporter listUsers <Keycloak_Root_URL> <Client_ID> <Client_Secret> --format=json --output=webhook --webhookType=generic --webhookUrl=$WEBHOOK_URL
100
+ ```
101
+
102
+ The JSON payload sent to the endpoint has the following structure:
103
+
104
+ ```json
105
+ {
106
+ "title": "Users Report",
107
+ "date": "10-3-2026",
108
+ "reportContent": "{...}",
109
+ "message": "Optional message (only included when --webhookMessage is provided)"
110
+ }
111
+ ```
112
+
113
+ An optional message can be included:
114
+
115
+ ```sh
116
+ kc-reporter listUsers <Keycloak_Root_URL> <Client_ID> <Client_Secret> --format=json --output=webhook --webhookType=generic --webhookUrl=$WEBHOOK_URL --webhookMessage="Scheduled report"
117
+ ```
@@ -15,14 +15,14 @@ type: application
15
15
  # This is the chart version. This version number should be incremented each time you make changes
16
16
  # to the chart and its templates, including the app version.
17
17
  # Versions are expected to follow Semantic Versioning (https://semver.org/)
18
- version: 1.4.10
18
+ version: 1.4.11
19
19
 
20
20
  # This is the version number of the application being deployed. This version number should be
21
21
  # incremented each time you make changes to the application. Versions are not expected to
22
22
  # follow Semantic Versioning. They should reflect the version the application is using.
23
23
  # It is recommended to use it with quotes.
24
24
  # renovate: datasource=docker depName=ContinuousSecurityTooling/keycloak-reporter
25
- appVersion: 1.1.9
25
+ appVersion: 1.2.0
26
26
  maintainers:
27
27
  # Martin Reinhardt
28
28
  - name: hypery2k
@@ -1,6 +1,6 @@
1
1
  # keycloak-reporter
2
2
 
3
- ![Version: 1.4.10](https://img.shields.io/badge/Version-1.4.10-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.1.9](https://img.shields.io/badge/AppVersion-1.1.9-informational?style=flat-square)
3
+ ![Version: 1.4.11](https://img.shields.io/badge/Version-1.4.11-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.2.0](https://img.shields.io/badge/AppVersion-1.2.0-informational?style=flat-square)
4
4
 
5
5
  Keycloak user and client reporting tool for automated regular access checks.
6
6
 
@@ -45,7 +45,7 @@
45
45
  "type": "array",
46
46
  "items": {
47
47
  "type": "string",
48
- "enum": ["slack", "teams"]
48
+ "enum": ["slack", "teams", "generic"]
49
49
  },
50
50
  "description": "Type of webhook"
51
51
  },
@@ -45,7 +45,7 @@
45
45
  "type": "array",
46
46
  "items": {
47
47
  "type": "string",
48
- "enum": ["slack", "teams"]
48
+ "enum": ["slack", "teams", "generic"]
49
49
  },
50
50
  "description": "Type of webhook"
51
51
  },
@@ -4,6 +4,7 @@ var WebhookType;
4
4
  (function (WebhookType) {
5
5
  WebhookType["SLACK"] = "slack";
6
6
  WebhookType["TEAMS"] = "teams";
7
+ WebhookType["GENERIC"] = "generic";
7
8
  })(WebhookType || (WebhookType = {}));
8
9
  export async function post2Webhook(type, url, title, reportContent, text) {
9
10
  const date = new Date();
@@ -59,6 +60,27 @@ export async function post2Webhook(type, url, title, reportContent, text) {
59
60
  }
60
61
  ]
61
62
  });
63
+ case WebhookType.GENERIC.toString(): {
64
+ const payload = {
65
+ title,
66
+ date: `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`,
67
+ reportContent
68
+ };
69
+ if (text != null) {
70
+ payload.message = text;
71
+ }
72
+ const response = await fetch(url, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify(payload)
76
+ });
77
+ if (!response.ok) {
78
+ const error = new Error(`Generic webhook request failed with status ${response.status}`);
79
+ error.code = 'generic_webhook_http_error';
80
+ throw error;
81
+ }
82
+ return response;
83
+ }
62
84
  // defaulting to Slack
63
85
  default:
64
86
  // eslint-disable-next-line no-case-declarations
@@ -1 +1 @@
1
- {"version":3,"file":"output.js","sourceRoot":"","sources":["../../lib/output.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../lib/output.ts"],"names":[],"mappings":"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"}
@@ -36,6 +36,9 @@ export async function convert(cfg) {
36
36
  case 'slack_webhook_http_error':
37
37
  logger.error('Invalid Slack Webhook Payload. Check your params.');
38
38
  throw new Error('Invalid Slack Payload');
39
+ case 'generic_webhook_http_error':
40
+ logger.error('Generic Webhook request failed. Check your URL and params.');
41
+ throw new Error('Invalid Generic Webhook request');
39
42
  default:
40
43
  logger.error(`Error during sending webhook.(${e === null || e === void 0 ? void 0 : e.code})`, e === null || e === void 0 ? void 0 : e.original);
41
44
  throw e;
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../../lib/report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,GAAkB;IAC9C,IAAI,aAAqB,CAAC;IAC1B,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,KAAK;YACR,aAAa,GAAG,CAAC,MAAM,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC7D,MAAM;QACR,qBAAqB;QACrB;YACE,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QACxB,aAAa,CACX,IAAI,CAAC,IAAI,CACP,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,EAC1B,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,CACjH,EACD,aAAa,CACd,CAAC;IACJ,CAAC;IACD,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;gBACpB,MAAM,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;gBAC3C,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACnE,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,iCAAiC,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,CAAC;gBACrE,MAAM,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACzG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC/B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,QAAQ,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;oBAC5B,KAAK,qCAAqC;wBACxC,MAAM,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;wBAClE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;oBAC3C,KAAK,0BAA0B;wBAC7B,MAAM,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;wBAClE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;oBAC3C;wBACE,MAAM,CAAC,KAAK,CAAC,iCAAiC,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,IAAI,GAAG,EAAE,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,QAAQ,CAAC,CAAC;wBACvE,MAAM,CAAC,CAAC;gBACZ,CAAC;YACH,CAAC;YACD,MAAM;QACR,6BAA6B;QAC7B;YACE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../lib/report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,GAAkB;IAC9C,IAAI,aAAqB,CAAC;IAC1B,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,KAAK;YACR,aAAa,GAAG,CAAC,MAAM,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC7D,MAAM;QACR,qBAAqB;QACrB;YACE,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QACxB,aAAa,CACX,IAAI,CAAC,IAAI,CACP,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,EAC1B,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,CACjH,EACD,aAAa,CACd,CAAC;IACJ,CAAC;IACD,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;gBACpB,MAAM,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;gBAC3C,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACnE,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,iCAAiC,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,CAAC;gBACrE,MAAM,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACzG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC/B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,QAAQ,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;oBAC5B,KAAK,qCAAqC;wBACxC,MAAM,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;wBAClE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;oBAC3C,KAAK,0BAA0B;wBAC7B,MAAM,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;wBAClE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;oBAC3C,KAAK,4BAA4B;wBAC/B,MAAM,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;wBAC3E,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;oBACrD;wBACE,MAAM,CAAC,KAAK,CAAC,iCAAiC,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,IAAI,GAAG,EAAE,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,QAAQ,CAAC,CAAC;wBACvE,MAAM,CAAC,CAAC;gBACZ,CAAC;YACH,CAAC;YACD,MAAM;QACR,6BAA6B;QAC7B;YACE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
@@ -15,12 +15,12 @@ test('Should post message to Teams', { timeout: 10000 }, (t) => {
15
15
  '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
16
16
  '--output=webhook',
17
17
  '--webhookType=teams',
18
- '--webhookUrl=' + process.env.WEBHOOK_TESTING_TEAMS
18
+ '--webhookUrl=' + process.env.WEBHOOK_TESTING_TEAMS,
19
19
  ],
20
20
  {
21
21
  env: {
22
- ...process.env
23
- }
22
+ ...process.env,
23
+ },
24
24
  }
25
25
  );
26
26
  cli.stdout.on('data', (chunk) => {
@@ -34,79 +34,67 @@ test('Should post message to Teams', { timeout: 10000 }, (t) => {
34
34
  });
35
35
  });
36
36
 
37
- test(
38
- 'Should post message to Teams with additional message',
39
- { timeout: 10000 },
40
- (t) => {
41
- const cli = spawn(
42
- path.join(path.dirname('.'), 'node'),
43
- [
44
- 'dist/cli.js',
45
- 'listUsers',
46
- 'http://localhost:8080',
47
- 'keycloak-reporter',
48
- '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
49
- '--output=webhook',
50
- '--webhookType=teams',
51
- '--webhookUrl=' + process.env.WEBHOOK_TESTING_TEAMS,
52
- '--webhookMessage="' +
53
- (process.env.WEBHOOK_ADDITIONAL_MESSAGE || 'From Github Actions') +
54
- '"'
55
- ],
56
- {
57
- env: {
58
- ...process.env
59
- }
60
- }
61
- );
62
- cli.stdout.on('data', (chunk) => {
63
- console.log(chunk.toString());
64
- });
65
- cli.stderr.on('data', (msg) => {
66
- t.fail(msg);
67
- });
68
- cli.stdout.on('end', () => {
69
- t.end();
70
- });
71
- }
72
- );
37
+ test('Should post message to Teams with additional message', { timeout: 10000 }, (t) => {
38
+ const cli = spawn(
39
+ path.join(path.dirname('.'), 'node'),
40
+ [
41
+ 'dist/cli.js',
42
+ 'listUsers',
43
+ 'http://localhost:8080',
44
+ 'keycloak-reporter',
45
+ '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
46
+ '--output=webhook',
47
+ '--webhookType=teams',
48
+ '--webhookUrl=' + process.env.WEBHOOK_TESTING_TEAMS,
49
+ '--webhookMessage="' + (process.env.WEBHOOK_ADDITIONAL_MESSAGE || 'From Github Actions') + '"',
50
+ ],
51
+ {
52
+ env: {
53
+ ...process.env,
54
+ },
55
+ }
56
+ );
57
+ cli.stdout.on('data', (chunk) => {
58
+ console.log(chunk.toString());
59
+ });
60
+ cli.stderr.on('data', (msg) => {
61
+ t.fail(msg);
62
+ });
63
+ cli.stdout.on('end', () => {
64
+ t.end();
65
+ });
66
+ });
73
67
 
74
- test(
75
- 'Should post message to Slack with additional message',
76
- { timeout: 10000 },
77
- (t) => {
78
- const cli = spawn(
79
- path.join(path.dirname('.'), 'node'),
80
- [
81
- 'dist/cli.js',
82
- 'listUsers',
83
- 'http://localhost:8080',
84
- 'keycloak-reporter',
85
- '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
86
- '--output=webhook',
87
- '--webhookType=slack',
88
- '--webhookUrl=' + process.env.WEBHOOK_TESTING_SLACK,
89
- '--webhookMessage="' +
90
- (process.env.WEBHOOK_ADDITIONAL_MESSAGE || 'From Github Actions') +
91
- '"'
92
- ],
93
- {
94
- env: {
95
- ...process.env
96
- }
97
- }
98
- );
99
- cli.stdout.on('data', (chunk) => {
100
- console.log(chunk.toString());
101
- });
102
- cli.stderr.on('data', (msg) => {
103
- t.fail(msg);
104
- });
105
- cli.stdout.on('end', () => {
106
- t.end();
107
- });
108
- }
109
- );
68
+ test('Should post message to Slack with additional message', { timeout: 10000 }, (t) => {
69
+ const cli = spawn(
70
+ path.join(path.dirname('.'), 'node'),
71
+ [
72
+ 'dist/cli.js',
73
+ 'listUsers',
74
+ 'http://localhost:8080',
75
+ 'keycloak-reporter',
76
+ '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
77
+ '--output=webhook',
78
+ '--webhookType=slack',
79
+ '--webhookUrl=' + process.env.WEBHOOK_TESTING_SLACK,
80
+ '--webhookMessage="' + (process.env.WEBHOOK_ADDITIONAL_MESSAGE || 'From Github Actions') + '"',
81
+ ],
82
+ {
83
+ env: {
84
+ ...process.env,
85
+ },
86
+ }
87
+ );
88
+ cli.stdout.on('data', (chunk) => {
89
+ console.log(chunk.toString());
90
+ });
91
+ cli.stderr.on('data', (msg) => {
92
+ t.fail(msg);
93
+ });
94
+ cli.stdout.on('end', () => {
95
+ t.end();
96
+ });
97
+ });
110
98
 
111
99
  test('Should post message to Slack', { timeout: 10000 }, (t) => {
112
100
  const cli = spawn(
@@ -119,12 +107,73 @@ test('Should post message to Slack', { timeout: 10000 }, (t) => {
119
107
  '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
120
108
  '--output=webhook',
121
109
  '--webhookType=slack',
122
- '--webhookUrl=' + process.env.WEBHOOK_TESTING_SLACK
110
+ '--webhookUrl=' + process.env.WEBHOOK_TESTING_SLACK,
111
+ ],
112
+ {
113
+ env: {
114
+ ...process.env,
115
+ },
116
+ }
117
+ );
118
+ cli.stdout.on('data', (chunk) => {
119
+ console.log(chunk.toString());
120
+ });
121
+ cli.stderr.on('data', (msg) => {
122
+ t.fail(msg);
123
+ });
124
+ cli.stdout.on('end', () => {
125
+ t.end();
126
+ });
127
+ });
128
+
129
+ test.skip('Should post message to generic webhook', { timeout: 10000 }, (t) => {
130
+ const cli = spawn(
131
+ path.join(path.dirname('.'), 'node'),
132
+ [
133
+ 'dist/cli.js',
134
+ 'listUsers',
135
+ 'http://localhost:8080',
136
+ 'keycloak-reporter',
137
+ '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
138
+ '--output=webhook',
139
+ '--webhookType=generic',
140
+ '--webhookUrl=' + process.env.WEBHOOK_TESTING_GENERIC,
141
+ ],
142
+ {
143
+ env: {
144
+ ...process.env,
145
+ },
146
+ }
147
+ );
148
+ cli.stdout.on('data', (chunk) => {
149
+ console.log(chunk.toString());
150
+ });
151
+ cli.stderr.on('data', (msg) => {
152
+ t.fail(msg);
153
+ });
154
+ cli.stdout.on('end', () => {
155
+ t.end();
156
+ });
157
+ });
158
+
159
+ test.skip('Should post message to generic webhook with additional message', { timeout: 10000 }, (t) => {
160
+ const cli = spawn(
161
+ path.join(path.dirname('.'), 'node'),
162
+ [
163
+ 'dist/cli.js',
164
+ 'listUsers',
165
+ 'http://localhost:8080',
166
+ 'keycloak-reporter',
167
+ '3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
168
+ '--output=webhook',
169
+ '--webhookType=generic',
170
+ '--webhookUrl=' + process.env.WEBHOOK_TESTING_GENERIC,
171
+ '--webhookMessage="' + (process.env.WEBHOOK_ADDITIONAL_MESSAGE || 'From Github Actions') + '"',
123
172
  ],
124
173
  {
125
174
  env: {
126
- ...process.env
127
- }
175
+ ...process.env,
176
+ },
128
177
  }
129
178
  );
130
179
  cli.stdout.on('data', (chunk) => {
package/lib/output.ts CHANGED
@@ -4,7 +4,8 @@ import { IncomingWebhook as SlackWebhook } from '@slack/webhook';
4
4
 
5
5
  enum WebhookType {
6
6
  SLACK = 'slack',
7
- TEAMS = 'teams'
7
+ TEAMS = 'teams',
8
+ GENERIC = 'generic'
8
9
  }
9
10
 
10
11
  export interface WebhookMessage {
@@ -75,6 +76,27 @@ export async function post2Webhook(
75
76
  }
76
77
  ]
77
78
  });
79
+ case WebhookType.GENERIC.toString(): {
80
+ const payload: Record<string, unknown> = {
81
+ title,
82
+ date: `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`,
83
+ reportContent
84
+ };
85
+ if (text != null) {
86
+ payload.message = text;
87
+ }
88
+ const response = await fetch(url, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify(payload)
92
+ });
93
+ if (!response.ok) {
94
+ const error = new Error(`Generic webhook request failed with status ${response.status}`);
95
+ (error as NodeJS.ErrnoException).code = 'generic_webhook_http_error';
96
+ throw error;
97
+ }
98
+ return response;
99
+ }
78
100
  // defaulting to Slack
79
101
  default:
80
102
  // eslint-disable-next-line no-case-declarations
package/lib/report.ts CHANGED
@@ -43,6 +43,9 @@ export async function convert(cfg: ConvertConfig) {
43
43
  case 'slack_webhook_http_error':
44
44
  logger.error('Invalid Slack Webhook Payload. Check your params.');
45
45
  throw new Error('Invalid Slack Payload');
46
+ case 'generic_webhook_http_error':
47
+ logger.error('Generic Webhook request failed. Check your URL and params.');
48
+ throw new Error('Invalid Generic Webhook request');
46
49
  default:
47
50
  logger.error(`Error during sending webhook.(${e?.code})`, e?.original);
48
51
  throw e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuoussecuritytooling/keycloak-reporter",
3
- "version": "1.1.9",
3
+ "version": "1.2.0-3090",
4
4
  "description": "Reporting Tools for Keycloak",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,129 @@
1
+ /// <reference types="jest-extended" />
2
+ import { post2Webhook } from '../lib/output';
3
+
4
+ const mockSlackSend = jest.fn();
5
+ jest.mock('@slack/webhook', () => ({
6
+ IncomingWebhook: jest.fn().mockImplementation(() => ({
7
+ send: mockSlackSend
8
+ }))
9
+ }));
10
+
11
+ const mockTeamsSend = jest.fn();
12
+ jest.mock('ms-teams-webhook', () => ({
13
+ IncomingWebhook: jest.fn().mockImplementation(() => ({
14
+ send: mockTeamsSend
15
+ }))
16
+ }));
17
+
18
+ describe('post2Webhook - Slack', () => {
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ mockSlackSend.mockResolvedValue({});
22
+ });
23
+
24
+ test('posts to Slack webhook', async () => {
25
+ await post2Webhook('slack', 'https://hooks.slack.com/test', 'Users Report', 'report content');
26
+ expect(mockSlackSend).toHaveBeenCalledTimes(1);
27
+ expect(mockSlackSend).toHaveBeenCalledWith(
28
+ expect.objectContaining({ blocks: expect.any(Array) })
29
+ );
30
+ });
31
+
32
+ test('posts to Slack webhook with message', async () => {
33
+ await post2Webhook('slack', 'https://hooks.slack.com/test', 'Users Report', 'report content', 'Hello from CI');
34
+ expect(mockSlackSend).toHaveBeenCalledTimes(1);
35
+ const blocks = mockSlackSend.mock.calls[0][0].blocks;
36
+ const contextBlock = blocks.find((b) => b.type === 'context' && b.elements?.[0]?.text === 'Hello from CI');
37
+ expect(contextBlock).toBeDefined();
38
+ });
39
+
40
+ test('Slack payload includes title and date fields', async () => {
41
+ await post2Webhook('slack', 'https://hooks.slack.com/test', 'My Title', 'content');
42
+ const blocks = mockSlackSend.mock.calls[0][0].blocks;
43
+ const sectionBlock = blocks.find((b) => b.type === 'section');
44
+ expect(sectionBlock.fields[0].text).toContain('My Title');
45
+ });
46
+ });
47
+
48
+ describe('post2Webhook - Teams', () => {
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ mockTeamsSend.mockResolvedValue({});
52
+ });
53
+
54
+ test('posts to Teams webhook', async () => {
55
+ await post2Webhook('teams', 'https://outlook.office.com/webhook/test', 'Clients Report', 'report content');
56
+ expect(mockTeamsSend).toHaveBeenCalledTimes(1);
57
+ expect(mockTeamsSend).toHaveBeenCalledWith(
58
+ expect.objectContaining({ type: 'message' })
59
+ );
60
+ });
61
+
62
+ test('posts to Teams webhook with message', async () => {
63
+ await post2Webhook('teams', 'https://outlook.office.com/webhook/test', 'Clients Report', 'report content', 'Hello from CI');
64
+ expect(mockTeamsSend).toHaveBeenCalledTimes(1);
65
+ const attachment = mockTeamsSend.mock.calls[0][0].attachments[0];
66
+ const textBlock = attachment.content.body.find((b) => b.type === 'TextBlock' && b.text === 'Hello from CI');
67
+ expect(textBlock).toBeDefined();
68
+ });
69
+
70
+ test('Teams payload includes title in FactSet', async () => {
71
+ await post2Webhook('teams', 'https://outlook.office.com/webhook/test', 'My Title', 'content');
72
+ const attachment = mockTeamsSend.mock.calls[0][0].attachments[0];
73
+ const factSet = attachment.content.body.find((b) => b.type === 'FactSet');
74
+ const typeFact = factSet.facts.find((f) => f.title === 'Type');
75
+ expect(typeFact.value).toBe('My Title');
76
+ });
77
+ });
78
+
79
+ describe('post2Webhook - Generic', () => {
80
+ beforeEach(() => {
81
+ jest.clearAllMocks();
82
+ global.fetch = jest.fn();
83
+ });
84
+
85
+ test('posts to generic webhook', async () => {
86
+ (global.fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200 });
87
+ await post2Webhook('generic', 'https://example.com/webhook', 'Users Report', 'report content');
88
+ expect(global.fetch).toHaveBeenCalledTimes(1);
89
+ expect(global.fetch).toHaveBeenCalledWith(
90
+ 'https://example.com/webhook',
91
+ expect.objectContaining({
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' }
94
+ })
95
+ );
96
+ });
97
+
98
+ test('generic webhook payload contains title, date and reportContent', async () => {
99
+ (global.fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200 });
100
+ await post2Webhook('generic', 'https://example.com/webhook', 'My Title', 'my content');
101
+ const callArgs = (global.fetch as jest.Mock).mock.calls[0];
102
+ const body = JSON.parse(callArgs[1].body);
103
+ expect(body.title).toBe('My Title');
104
+ expect(body.reportContent).toBe('my content');
105
+ expect(body.date).toBeDefined();
106
+ expect(body.message).toBeUndefined();
107
+ });
108
+
109
+ test('generic webhook payload includes message when provided', async () => {
110
+ (global.fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200 });
111
+ await post2Webhook('generic', 'https://example.com/webhook', 'My Title', 'my content', 'Hello from CI');
112
+ const callArgs = (global.fetch as jest.Mock).mock.calls[0];
113
+ const body = JSON.parse(callArgs[1].body);
114
+ expect(body.message).toBe('Hello from CI');
115
+ });
116
+
117
+ test('throws on generic webhook HTTP error response', async () => {
118
+ (global.fetch as jest.Mock).mockResolvedValue({ ok: false, status: 500 });
119
+ await expect(
120
+ post2Webhook('generic', 'https://example.com/webhook', 'My Title', 'my content')
121
+ ).rejects.toThrow('Generic webhook request failed with status 500');
122
+ });
123
+
124
+ test('thrown error has generic_webhook_http_error code', async () => {
125
+ (global.fetch as jest.Mock).mockResolvedValue({ ok: false, status: 400 });
126
+ const err = await post2Webhook('generic', 'https://example.com/webhook', 'My Title', 'my content').catch((e) => e);
127
+ expect((err as NodeJS.ErrnoException).code).toBe('generic_webhook_http_error');
128
+ });
129
+ });
@@ -1 +0,0 @@
1
- sha256:c7df4f7c36b529d39cc93cf6ba77e49e196ff3a05177c8904183647974fe2f80
Binary file