@continuoussecuritytooling/keycloak-reporter 0.1.0
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.
- package/.bin/start-server.mjs +87 -0
- package/.bin/wait-for-server.sh +13 -0
- package/.docs/webhook-slack-sample.png +0 -0
- package/.docs/webhook-teams-sample.png +0 -0
- package/.editorconfig +23 -0
- package/.eslintignore +1 -0
- package/.eslintrc.cjs +14 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/workflows/pipeline.yml +101 -0
- package/Dockerfile +9 -0
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/cli.ts +125 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/client.js +41 -0
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/convert.js +9 -0
- package/dist/lib/convert.js.map +1 -0
- package/dist/lib/output.js +98 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/user.js +75 -0
- package/dist/lib/user.js.map +1 -0
- package/dist/package.json +53 -0
- package/dist/src/cli.js +19 -0
- package/dist/src/cli.js.map +1 -0
- package/docker_entrypoint.sh +3 -0
- package/e2e/fixtures/auth-utils/test-realm.json +5095 -0
- package/e2e/run-tests.sh +45 -0
- package/e2e/spec/clients.js +29 -0
- package/e2e/spec/users.js +29 -0
- package/e2e/spec/webhooks.js +60 -0
- package/index.ts +125 -0
- package/jest.config.js +25 -0
- package/lib/client.ts +55 -0
- package/lib/convert.ts +9 -0
- package/lib/output.ts +115 -0
- package/lib/user.ts +99 -0
- package/package.json +53 -0
- package/renovate.json +14 -0
- package/src/cli.ts +26 -0
- package/test/convert.spec.ts +15 -0
- package/tsconfig.json +11 -0
package/e2e/run-tests.sh
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash -e
|
|
2
|
+
|
|
3
|
+
TEST_DIR=reports
|
|
4
|
+
|
|
5
|
+
run_tests() {
|
|
6
|
+
FILE_PATH=$1
|
|
7
|
+
mkdir -p ${TEST_DIR}
|
|
8
|
+
|
|
9
|
+
echo "------------------------------------"
|
|
10
|
+
echo "Running tests for file: ${FILE_PATH}"
|
|
11
|
+
echo -e "------------------------------------\n"
|
|
12
|
+
|
|
13
|
+
if [[ -f ${FILE_PATH} ]]; then
|
|
14
|
+
TEST_NAME="${FILE_PATH##*/}"
|
|
15
|
+
|
|
16
|
+
node ${FILE_PATH} | tee ${TEST_DIR}/${TEST_NAME}.txt
|
|
17
|
+
cat ${TEST_DIR}/${TEST_NAME}.txt | node ./node_modules/.bin/tap-xunit --package="${TEST_NAME}" > ${TEST_DIR}/${TEST_NAME}.xml
|
|
18
|
+
RET_VAL=$?
|
|
19
|
+
|
|
20
|
+
echo "------------------------------------"
|
|
21
|
+
echo "Tests for file ${FILE_PATH} finished with return value ${RET_VAL}"
|
|
22
|
+
echo -e "------------------------------------\n"
|
|
23
|
+
|
|
24
|
+
if [[ ${RET_VAL} -ne 0 ]]; then
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
else
|
|
28
|
+
echo "------------------------------------"
|
|
29
|
+
echo "Test file ${FILE_PATH} does not exists! Aborting tests execution."
|
|
30
|
+
echo "------------------------------------"
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if [[ "$1" == "group1" ]]; then
|
|
36
|
+
for i in `ls e2e/spec/*.js | grep -v "enforcer-spec"`; do run_tests ${i}; done
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [[ "$1" == "group2" ]]; then
|
|
40
|
+
for i in `ls e2e/spec/*.js | grep "enforcer-spec"`; do run_tests ${i}; done
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [[ -z "$1" ]]; then
|
|
44
|
+
for i in `ls e2e/spec/*.js`; do run_tests ${i}; done
|
|
45
|
+
fi
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { test } from 'tape';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
test('Should list clients as JSON', (t) => {
|
|
8
|
+
const cli = spawn(
|
|
9
|
+
path.join(path.dirname('.'), 'node'),
|
|
10
|
+
[
|
|
11
|
+
'dist/cli.js',
|
|
12
|
+
'listClients',
|
|
13
|
+
'http://localhost:8080',
|
|
14
|
+
'keycloak-reporter',
|
|
15
|
+
'3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
|
|
16
|
+
],
|
|
17
|
+
{
|
|
18
|
+
env: {
|
|
19
|
+
...process.env,
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
cli.stdout.on('data', (chunk) => {
|
|
24
|
+
t.equal(JSON.parse(chunk.toString()).length, 24);
|
|
25
|
+
});
|
|
26
|
+
cli.stdout.on('end', () => {
|
|
27
|
+
t.end();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { test } from 'tape';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
test('Should list users as JSON', (t) => {
|
|
8
|
+
const cli = spawn(
|
|
9
|
+
path.join(path.dirname('.'), 'node'),
|
|
10
|
+
[
|
|
11
|
+
'dist/cli.js',
|
|
12
|
+
'listUsers',
|
|
13
|
+
'http://localhost:8080',
|
|
14
|
+
'keycloak-reporter',
|
|
15
|
+
'3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
|
|
16
|
+
],
|
|
17
|
+
{
|
|
18
|
+
env: {
|
|
19
|
+
...process.env,
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
cli.stdout.on('data', (chunk) => {
|
|
24
|
+
t.equal(JSON.parse(chunk.toString()).length, 3);
|
|
25
|
+
});
|
|
26
|
+
cli.stdout.on('end', () => {
|
|
27
|
+
t.end();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { test } from 'tape';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
test('Should post message to Teams', (t) => {
|
|
8
|
+
const cli = spawn(
|
|
9
|
+
path.join(path.dirname('.'), 'node'),
|
|
10
|
+
[
|
|
11
|
+
'dist/cli.js',
|
|
12
|
+
'listUsers',
|
|
13
|
+
'http://localhost:8080',
|
|
14
|
+
'keycloak-reporter',
|
|
15
|
+
'3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
|
|
16
|
+
'--output=webhook',
|
|
17
|
+
'--webhookType=teams',
|
|
18
|
+
'--webhookUrl=' + process.env.WEBHOOK_TESTING_TEAMS,
|
|
19
|
+
],
|
|
20
|
+
{
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
cli.stdout.on('data', (chunk) => {
|
|
27
|
+
console.log(chunk.toString())
|
|
28
|
+
});
|
|
29
|
+
cli.stdout.on('end', () => {
|
|
30
|
+
t.end();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('Should post message to Slack', (t) => {
|
|
35
|
+
const cli = spawn(
|
|
36
|
+
path.join(path.dirname('.'), 'node'),
|
|
37
|
+
[
|
|
38
|
+
'dist/cli.js',
|
|
39
|
+
'listUsers',
|
|
40
|
+
'http://localhost:8080',
|
|
41
|
+
'keycloak-reporter',
|
|
42
|
+
'3UYhI2hryFwoVtcd7ljlaDuD9HXrGV5r',
|
|
43
|
+
'--output=webhook',
|
|
44
|
+
'--webhookType=slack',
|
|
45
|
+
'--webhookUrl=' + process.env.WEBHOOK_TESTING_SLACK,
|
|
46
|
+
],
|
|
47
|
+
{
|
|
48
|
+
env: {
|
|
49
|
+
...process.env,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
cli.stdout.on('data', (chunk) => {
|
|
54
|
+
console.log(chunk.toString())
|
|
55
|
+
});
|
|
56
|
+
cli.stdout.on('end', () => {
|
|
57
|
+
t.end();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import yargs from 'yargs/yargs';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import { listUsers, listClients } from './src/cli.js';
|
|
6
|
+
import { Options } from './lib/client.js';
|
|
7
|
+
import { convertJSON2CSV } from './lib/convert.js';
|
|
8
|
+
import { post2Webhook } from './lib/output.js';
|
|
9
|
+
|
|
10
|
+
class WebhookConfig {
|
|
11
|
+
type: string;
|
|
12
|
+
url: string;
|
|
13
|
+
title: string;
|
|
14
|
+
constructor(type: string, url: string, title: string) {
|
|
15
|
+
this.type = type;
|
|
16
|
+
this.url = url;
|
|
17
|
+
this.title = title;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function convert(
|
|
22
|
+
format: string,
|
|
23
|
+
output: string,
|
|
24
|
+
config: WebhookConfig,
|
|
25
|
+
json: object
|
|
26
|
+
) {
|
|
27
|
+
let outputContent: string;
|
|
28
|
+
switch (format) {
|
|
29
|
+
case 'csv':
|
|
30
|
+
outputContent = (await convertJSON2CSV(json)).toString();
|
|
31
|
+
break;
|
|
32
|
+
// defaulting to JSON
|
|
33
|
+
default:
|
|
34
|
+
outputContent = JSON.stringify(json);
|
|
35
|
+
}
|
|
36
|
+
switch (output) {
|
|
37
|
+
case 'webhook':
|
|
38
|
+
try {
|
|
39
|
+
await post2Webhook(
|
|
40
|
+
config.type,
|
|
41
|
+
config.url,
|
|
42
|
+
config.title,
|
|
43
|
+
outputContent
|
|
44
|
+
);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error('Error during sending webhook: ', e);
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
// defaulting to standard out
|
|
50
|
+
default:
|
|
51
|
+
console.log(outputContent);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
yargs(hideBin(process.argv))
|
|
56
|
+
.command(
|
|
57
|
+
'listUsers [url] [clientId] [clientSecret]',
|
|
58
|
+
'fetches all users in the realms.',
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
60
|
+
() => {},
|
|
61
|
+
async (argv) => {
|
|
62
|
+
const users = await listUsers(<Options>{
|
|
63
|
+
clientId: argv.clientId as string,
|
|
64
|
+
clientSecret: argv.clientSecret as string,
|
|
65
|
+
rootUrl: argv.url as string,
|
|
66
|
+
});
|
|
67
|
+
await convert(
|
|
68
|
+
argv.format as string,
|
|
69
|
+
argv.output as string,
|
|
70
|
+
new WebhookConfig(
|
|
71
|
+
argv.webhookType as string,
|
|
72
|
+
argv.webhookUrl as string,
|
|
73
|
+
'User Listing'
|
|
74
|
+
),
|
|
75
|
+
users
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
.command(
|
|
80
|
+
'listClients [url] [clientId] [clientSecret]',
|
|
81
|
+
'fetches all clients in the realms.',
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
83
|
+
() => {},
|
|
84
|
+
async (argv) => {
|
|
85
|
+
const clients = await listClients(<Options>{
|
|
86
|
+
clientId: argv.clientId as string,
|
|
87
|
+
clientSecret: argv.clientSecret as string,
|
|
88
|
+
rootUrl: argv.url as string,
|
|
89
|
+
});
|
|
90
|
+
await convert(
|
|
91
|
+
argv.format as string,
|
|
92
|
+
argv.output as string,
|
|
93
|
+
new WebhookConfig(
|
|
94
|
+
argv.webhookType as string,
|
|
95
|
+
argv.webhookUrl as string,
|
|
96
|
+
'Client Listing'
|
|
97
|
+
),
|
|
98
|
+
clients
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
.option('format', {
|
|
103
|
+
alias: 'f',
|
|
104
|
+
type: 'string',
|
|
105
|
+
default: 'json',
|
|
106
|
+
description: 'output format, e.g. JSON|CSV',
|
|
107
|
+
})
|
|
108
|
+
.option('output', {
|
|
109
|
+
alias: 'o',
|
|
110
|
+
type: 'string',
|
|
111
|
+
default: 'stdout',
|
|
112
|
+
description: 'output channel',
|
|
113
|
+
})
|
|
114
|
+
.option('webhookType', {
|
|
115
|
+
alias: 'w',
|
|
116
|
+
type: 'string',
|
|
117
|
+
default: 'slack',
|
|
118
|
+
description: 'Webhook Type',
|
|
119
|
+
})
|
|
120
|
+
.option('webhookUrl', {
|
|
121
|
+
alias: 't',
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Webhook URL',
|
|
124
|
+
})
|
|
125
|
+
.parse();
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
2
|
+
export default {
|
|
3
|
+
roots: [
|
|
4
|
+
'src',
|
|
5
|
+
'lib',
|
|
6
|
+
'test',
|
|
7
|
+
],
|
|
8
|
+
preset: 'ts-jest',
|
|
9
|
+
testEnvironment: 'node',
|
|
10
|
+
collectCoverage: true,
|
|
11
|
+
collectCoverageFrom: [
|
|
12
|
+
'index.ts',
|
|
13
|
+
'src/**',
|
|
14
|
+
'lib/**',
|
|
15
|
+
],
|
|
16
|
+
setupFilesAfterEnv: [
|
|
17
|
+
'jest-extended/all',
|
|
18
|
+
],
|
|
19
|
+
transformIgnorePatterns: [
|
|
20
|
+
'node_modules/(?!(string-width|strip-ansi|ansi-regex|test-json-import)/)',
|
|
21
|
+
],
|
|
22
|
+
'transform': {
|
|
23
|
+
'^.+\\.(ts|tsx)$': 'ts-jest',
|
|
24
|
+
},
|
|
25
|
+
};
|
package/lib/client.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Issuer } from 'openid-client';
|
|
2
|
+
import KcAdminClient from '@keycloak/keycloak-admin-client';
|
|
3
|
+
|
|
4
|
+
// Token refresh interval 60 seconds
|
|
5
|
+
const TOKEN_REFRESH = 60;
|
|
6
|
+
|
|
7
|
+
export interface Options {
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret: string;
|
|
10
|
+
rootUrl: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function createClient(options: Options): Promise<KcAdminClient> {
|
|
14
|
+
const kcAdminClient = new KcAdminClient({
|
|
15
|
+
baseUrl: options.rootUrl,
|
|
16
|
+
realmName: 'master',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// client login
|
|
21
|
+
await kcAdminClient.auth({
|
|
22
|
+
clientId: options.clientId,
|
|
23
|
+
clientSecret: options.clientSecret,
|
|
24
|
+
grantType: 'client_credentials',
|
|
25
|
+
});
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('Check Client Config:',e.response.data.error_description);
|
|
28
|
+
return Promise.reject();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const keycloakIssuer = await Issuer.discover(
|
|
32
|
+
`${options.rootUrl}/realms/master`
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const client = new keycloakIssuer.Client({
|
|
36
|
+
client_id: options.clientId,
|
|
37
|
+
token_endpoint_auth_method: 'none', // to send only client_id in the header
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Use the grant type 'password'
|
|
41
|
+
const tokenSet = await client.grant({
|
|
42
|
+
client_id: options.clientId,
|
|
43
|
+
client_secret: options.clientSecret,
|
|
44
|
+
grant_type: 'client_credentials',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
// TODO: FIXME - Periodically using refresh_token grant flow to get new access token here
|
|
49
|
+
setInterval(async () => {
|
|
50
|
+
const refreshToken = tokenSet.refresh_token;
|
|
51
|
+
kcAdminClient.setAccessToken((await client.refresh(refreshToken)).access_token);
|
|
52
|
+
}, TOKEN_REFRESH * 1000); */
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve) => resolve(kcAdminClient));
|
|
55
|
+
}
|
package/lib/convert.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AsyncParser } from '@json2csv/node';
|
|
2
|
+
|
|
3
|
+
export async function convertJSON2CSV(json: object) {
|
|
4
|
+
const opts = {};
|
|
5
|
+
const transformOpts = {};
|
|
6
|
+
const asyncOpts = {};
|
|
7
|
+
const parser = new AsyncParser(opts, transformOpts, asyncOpts);
|
|
8
|
+
return await parser.parse(json).promise();
|
|
9
|
+
}
|
package/lib/output.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { IncomingWebhook as TeamsWebhook } from 'ms-teams-webhook';
|
|
2
|
+
import { Block, SectionBlock } from '@slack/types';
|
|
3
|
+
import { IncomingWebhook as SlackWebhook } from '@slack/webhook';
|
|
4
|
+
|
|
5
|
+
enum WebhookType {
|
|
6
|
+
SLACK = 'slack',
|
|
7
|
+
TEAMS = 'teams',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WebhookMessage {
|
|
11
|
+
url: string;
|
|
12
|
+
type: WebhookType;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function post2Webhook(
|
|
16
|
+
type: string,
|
|
17
|
+
url: string,
|
|
18
|
+
title: string,
|
|
19
|
+
reportContent: string
|
|
20
|
+
): Promise<unknown> {
|
|
21
|
+
//const title= 'Keycloak Reporting';
|
|
22
|
+
const date = new Date();
|
|
23
|
+
switch (type) {
|
|
24
|
+
case WebhookType.TEAMS.toString():
|
|
25
|
+
return new TeamsWebhook(url).send({
|
|
26
|
+
type: 'message',
|
|
27
|
+
attachments: [
|
|
28
|
+
{
|
|
29
|
+
contentType: 'application/vnd.microsoft.card.adaptive',
|
|
30
|
+
content: {
|
|
31
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
32
|
+
type: 'AdaptiveCard',
|
|
33
|
+
version: '1.2',
|
|
34
|
+
body: [
|
|
35
|
+
{
|
|
36
|
+
type: 'FactSet',
|
|
37
|
+
facts: [
|
|
38
|
+
{
|
|
39
|
+
title: 'Type',
|
|
40
|
+
value: title,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: 'Date',
|
|
44
|
+
value: `${date.getDate()}-${
|
|
45
|
+
date.getMonth() + 1
|
|
46
|
+
}-${date.getFullYear()}`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
actions: [
|
|
52
|
+
{
|
|
53
|
+
type: 'Action.ShowCard',
|
|
54
|
+
title: 'Show raw report data',
|
|
55
|
+
card: {
|
|
56
|
+
type: 'AdaptiveCard',
|
|
57
|
+
body: [
|
|
58
|
+
{
|
|
59
|
+
type: 'TextBlock',
|
|
60
|
+
text: reportContent,
|
|
61
|
+
wrap: true,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
$schema:
|
|
65
|
+
'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
// defaulting to Slack
|
|
74
|
+
default:
|
|
75
|
+
return new SlackWebhook(url).send({
|
|
76
|
+
blocks: [
|
|
77
|
+
{
|
|
78
|
+
type: 'section',
|
|
79
|
+
fields: [
|
|
80
|
+
{ type: 'mrkdwn', text: `*Type*: ${title}` },
|
|
81
|
+
{
|
|
82
|
+
type: 'mrkdwn',
|
|
83
|
+
text: `*Date*: ${date.getDate()}-${
|
|
84
|
+
date.getMonth() + 1
|
|
85
|
+
}-${date.getFullYear()}`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'divider',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'context',
|
|
94
|
+
elements: [
|
|
95
|
+
{
|
|
96
|
+
type: 'mrkdwn',
|
|
97
|
+
text: `
|
|
98
|
+
\`\`\`
|
|
99
|
+
${reportContent}
|
|
100
|
+
\`\`\`
|
|
101
|
+
`
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: 'context',
|
|
107
|
+
elements: [{ type: 'plain_text', text: 'Raw report data' }],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
*/
|
package/lib/user.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import KcAdminClient from '@keycloak/keycloak-admin-client';
|
|
2
|
+
|
|
3
|
+
export interface User {
|
|
4
|
+
username: string;
|
|
5
|
+
realm: string;
|
|
6
|
+
id: string;
|
|
7
|
+
firstName: string;
|
|
8
|
+
lastName: string;
|
|
9
|
+
email: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Client {
|
|
14
|
+
client: string;
|
|
15
|
+
realm: string;
|
|
16
|
+
id: string;
|
|
17
|
+
description: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
public: boolean;
|
|
20
|
+
allowedOrigins: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function clientListing(
|
|
24
|
+
client: KcAdminClient
|
|
25
|
+
): Promise<Array<Client>> {
|
|
26
|
+
const currentRealm = client.realmName;
|
|
27
|
+
let realms;
|
|
28
|
+
try {
|
|
29
|
+
// iterate over realms
|
|
30
|
+
realms = await client.realms.find();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error('Check Client role:', e.response.statusText);
|
|
33
|
+
return Promise.reject();
|
|
34
|
+
}
|
|
35
|
+
let allClients = new Array<Client>();
|
|
36
|
+
for (const realm of realms) {
|
|
37
|
+
// switch realm
|
|
38
|
+
client.setConfig({
|
|
39
|
+
realmName: realm.realm,
|
|
40
|
+
});
|
|
41
|
+
const realmClients = new Array<Client>();
|
|
42
|
+
for (const user of await client.clients.find()) {
|
|
43
|
+
realmClients.push({
|
|
44
|
+
client: user.clientId,
|
|
45
|
+
id: user.id,
|
|
46
|
+
description: user.description,
|
|
47
|
+
realm: realm.realm,
|
|
48
|
+
enabled: user.enabled,
|
|
49
|
+
public: user.publicClient,
|
|
50
|
+
allowedOrigins: JSON.stringify(user.webOrigins),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
allClients = [...allClients, ...realmClients];
|
|
54
|
+
}
|
|
55
|
+
// switch back to realm
|
|
56
|
+
client.setConfig({
|
|
57
|
+
realmName: currentRealm,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => resolve(allClients));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function userListing(client: KcAdminClient): Promise<Array<User>> {
|
|
64
|
+
const currentRealm = client.realmName;
|
|
65
|
+
let realms;
|
|
66
|
+
// iterate over realms
|
|
67
|
+
try {
|
|
68
|
+
realms = await client.realms.find();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error('Check Client role:', e.response.statusText);
|
|
71
|
+
return Promise.reject();
|
|
72
|
+
}
|
|
73
|
+
let allUsers = new Array<User>();
|
|
74
|
+
for (const realm of realms) {
|
|
75
|
+
// switch realm
|
|
76
|
+
client.setConfig({
|
|
77
|
+
realmName: realm.realm,
|
|
78
|
+
});
|
|
79
|
+
const realmUsers = new Array<User>();
|
|
80
|
+
for (const user of await client.users.find()) {
|
|
81
|
+
realmUsers.push({
|
|
82
|
+
username: user.username,
|
|
83
|
+
id: user.id,
|
|
84
|
+
firstName: user.firstName,
|
|
85
|
+
lastName: user.lastName,
|
|
86
|
+
email: user.email,
|
|
87
|
+
realm: realm.realm,
|
|
88
|
+
enabled: user.enabled,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
allUsers = [...allUsers, ...realmUsers];
|
|
92
|
+
}
|
|
93
|
+
// switch back to realm
|
|
94
|
+
client.setConfig({
|
|
95
|
+
realmName: currentRealm,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return new Promise((resolve) => resolve(allUsers));
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@continuoussecuritytooling/keycloak-reporter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reporting Tools for Keycloak",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": "dist/cli.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"clean": "rm -rf dist/* && npm i",
|
|
10
|
+
"build": "tsc && chmod +x dist/cli.js && cp package.json dist/",
|
|
11
|
+
"test": "eslint . && jest",
|
|
12
|
+
"end2end:start-server": ".bin/start-server.mjs -Dkeycloak.profile.feature.account_api=disabled -Dkeycloak.profile.feature.account2=disabled -Dkeycloak.migration.action=import -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=e2e/fixtures/auth-utils/test-realm.json -Dkeycloak.migration.strategy=OVERWRITE_EXISTING",
|
|
13
|
+
"end2end:test": "./e2e/run-tests.sh"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ContinuousSecurityTooling/keycloak-reporter.git"
|
|
18
|
+
},
|
|
19
|
+
"author": "Martin Reinhardt",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/ContinuousSecurityTooling/keycloak-reporter/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/ContinuousSecurityTooling/keycloak-reporter#readme",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@json2csv/node": "^7.0.0",
|
|
27
|
+
"@keycloak/keycloak-admin-client": "^20.0.5",
|
|
28
|
+
"@slack/webhook": "^6.1.0",
|
|
29
|
+
"install": "^0.13.0",
|
|
30
|
+
"ms-teams-webhook": "^2.0.2",
|
|
31
|
+
"npm": "^9.6.7",
|
|
32
|
+
"openid-client": "^5.4.2",
|
|
33
|
+
"yargs": "^17.7.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@octokit/rest": "^19.0.11",
|
|
37
|
+
"@types/jest": "^29.5.1",
|
|
38
|
+
"@types/node": "^20.1.5",
|
|
39
|
+
"@types/yargs": "^17.0.24",
|
|
40
|
+
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
|
41
|
+
"@typescript-eslint/parser": "^5.59.6",
|
|
42
|
+
"eslint": "^8.40.0",
|
|
43
|
+
"gunzip-maybe": "^1.4.2",
|
|
44
|
+
"jest": "^29.5.0",
|
|
45
|
+
"jest-extended": "^3.2.4",
|
|
46
|
+
"node-fetch": "^3.3.1",
|
|
47
|
+
"tap-xunit": "^2.4.1",
|
|
48
|
+
"tape": "^5.6.3",
|
|
49
|
+
"tar-fs": "^2.1.1",
|
|
50
|
+
"ts-jest": "^29.1.0",
|
|
51
|
+
"typescript": "^5.0.4"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/renovate.json
ADDED
package/src/cli.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Options, createClient } from '../lib/client.js';
|
|
2
|
+
import { User, userListing, clientListing, Client } from '../lib/user.js';
|
|
3
|
+
|
|
4
|
+
export async function listUsers(options: Options): Promise<Array<User>> {
|
|
5
|
+
const users = await userListing(
|
|
6
|
+
await createClient({
|
|
7
|
+
clientId: options.clientId,
|
|
8
|
+
clientSecret: options.clientSecret,
|
|
9
|
+
rootUrl: options.rootUrl,
|
|
10
|
+
})
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve) => resolve(users));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function listClients(options: Options): Promise<Array<Client>> {
|
|
17
|
+
const clients = await clientListing(
|
|
18
|
+
await createClient({
|
|
19
|
+
clientId: options.clientId,
|
|
20
|
+
clientSecret: options.clientSecret,
|
|
21
|
+
rootUrl: options.rootUrl,
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve) => resolve(clients));
|
|
26
|
+
}
|