This repository was archived by the owner on Jun 25, 2024. It is now read-only.
generated from JupiterOne-Archives/integration-template
-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathiam.ts
304 lines (272 loc) · 9.3 KB
/
iam.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import * as url from 'url';
import * as querystring from 'querystring';
import { IntegrationError, JobState } from '@jupiterone/integration-sdk-core';
import { iam_v1 } from 'googleapis';
export const IAM_MANAGED_ROLES_DATA_JOB_STATE_KEY = 'iam_managed_roles';
/**
* Some IAM members resemble this format: `deleted:serviceAccount:{emailid}?uid={uniqueid}`
*
* We want to extract the user identifier, in this case {emailid} as well as
* the {uniqueid} from the query.
*
* @param partialMember - Partial member in the following format: {emailid}?uid={uniqueid}
*/
function parseMemberIdentifyingData(partialMember: string) {
const { pathname, query } = url.parse(partialMember);
if (!pathname || !query) {
// The format of the member data is one that we do not know how to handle.
// This should never happen.
throw new IntegrationError({
message: 'parseMemberIdentifyingData cannot process this member format.',
code: 'UNPROCESSABLE_PARTIAL_IAM_MEMBER_FORMAT',
});
}
const { uid } = querystring.parse(query);
return {
identifier: pathname,
uniqueid: uid as string | undefined,
};
}
export type ParsedIamMemberType =
| 'allUsers'
| 'allAuthenticatedUsers'
| 'user'
| 'serviceAccount'
| 'group'
| 'domain';
export interface ParsedIamMember {
type: ParsedIamMemberType;
identifier: string | undefined;
uniqueid: string | undefined;
deleted: boolean;
}
/**
* Parses the IAM member and returns relevant metadata
*
* Ex.
*
* Input: serviceAccount:[email protected]
* Output:
*
* {
* type: 'serviceAccount',
* identifier: '[email protected]'
* }
*
* IAM member type notes:
*
* Specifies the identities requesting access for a Cloud Platform resource.
* `members` can have the following values:
*
* - `allUsers`: A special identifier that represents anyone who is on the
* internet; with or without a Google account.
*
* - `allAuthenticatedUsers`: A special identifier that represents anyone who is
* authenticated with a Google account or a service account.
*
* - `user:{emailid}`: An email address that represents a specific Google
* account. For example, `[email protected]`.
*
* - `serviceAccount:{emailid}`: An email address that represents a service
* account. For example, `[email protected]`.
*
* - `group:{emailid}`: An email address that represents a Google group.
* For example, `[email protected]`.
*
* - `deleted:user:{emailid}?uid={uniqueid}`: An email address (plus unique identifier)
* representing a user that has been recently deleted.
* For example, `[email protected]?uid=123456789012345678901`. If the user is
* recovered, this value reverts to `user:{emailid}` and the recovered user
* retains the role in the binding.
*
* - `deleted:serviceAccount:{emailid}?uid={uniqueid}`: An email address (plus unique identifier)
* representing a service account that has been recently deleted. For example,
* `[email protected]?uid=123456789012345678901`.
* If the service account is undeleted, this value reverts to `serviceAccount:{emailid}`
* and the undeleted service account retains the
* role in the binding.
*
* - `deleted:group:{emailid}?uid={uniqueid}`: An email address (plus unique identifier)
* representing a Google group that has been recently deleted.
*
* For example, `[email protected]?uid=123456789012345678901`. If the group
* is recovered, this value reverts to `group:{emailid}` and the recovered
* group retains the role in the binding.
*
* - `domain:{domain}`: The G Suite domain (primary) that represents all the
* users of that domain. For example, `google.com` or `example.com`.
*
* @param member - A member listed in a IAM role policy (See: https://cloud.google.com/iam/docs/overview#cloud-iam-policy)
*/
export function parseIamMember(member: string): ParsedIamMember {
if (member === 'allUsers' || member === 'allAuthenticatedUsers') {
return {
type: member,
identifier: undefined,
uniqueid: undefined,
deleted: false,
};
}
const data = member.split(':');
if (data.length === 1) {
// The format of the member data is one that we do not know how to handle.
// This should never happen.
throw new IntegrationError({
message:
'getIamMemberDataFromIamMember cannot process this member format.',
code: 'UNKNOWN_IAM_MEMBER_FORMAT',
});
}
const deleted = data[0] === 'deleted';
let type: ParsedIamMemberType;
let identifier: string;
let uniqueid: string | undefined;
if (deleted) {
// Example: `deleted:serviceAccount:{emailid}?uid={uniqueid}`
type = data[1] as ParsedIamMemberType;
({ identifier, uniqueid } = parseMemberIdentifyingData(data[2]));
} else {
// Example: `serviceAccount:{emailid}`
type = data[0] as ParsedIamMemberType;
identifier = data[1];
}
return {
type,
identifier,
uniqueid,
deleted,
};
}
/**
* Returns true if a member is public to all authenticated users or all users
* on the internet
*
* allAuthenticatedUsers
* The value allAuthenticatedUsers is a special identifier that represents all
* service accounts and all users on the internet who have authenticated with a
* Google Account. This identifier includes accounts that aren't connected to a
* Google Workspace or Cloud Identity domain, such as personal Gmail accounts.
* Users who aren't authenticated, such as anonymous visitors, aren't included.
*
* allUsers
* The value allUsers is a special identifier that represents anyone who is on
* the internet, including authenticated and unauthenticated users.
*
* See:
* - https://cloud.google.com/iam/docs/overview#allauthenticatedusers
* - https://cloud.google.com/iam/docs/overview#allusers
*/
export function isMemberPublic(member: string) {
return member === 'allUsers' || member === 'allAuthenticatedUsers';
}
export async function getIamManagedRoleData(
jobState: JobState,
): Promise<iam_v1.Schema$Role[]> {
const managedRoles = await jobState.getData<iam_v1.Schema$Role[]>(
IAM_MANAGED_ROLES_DATA_JOB_STATE_KEY,
);
if (!managedRoles) {
throw new IntegrationError({
message: 'Could not find managed roles in job state',
code: 'MANAGED_ROLES_NOT_FOUND',
fatal: true,
});
}
return managedRoles;
}
export function buildPermissionsByApiServiceMap(roles: iam_v1.Schema$Role[]) {
const allPermissions: Set<string> = new Set();
const permissionsByServiceMap: Map<string, string[]> = new Map();
for (const role of roles) {
for (const permission of role.includedPermissions || []) {
if (allPermissions.has(permission)) {
continue;
}
// apigateway.apiconfigs.update -> apigateway
const service = permission.split('.')[0];
const permissionsByService = permissionsByServiceMap.get(service);
if (!permissionsByService) {
permissionsByServiceMap.set(service, [permission]);
} else {
permissionsByService.push(permission);
}
}
}
return permissionsByServiceMap;
}
/**
* Determines whether a Google Cloud permission is read-only or not
*
* See: https://cloud.google.com/iam/docs/permissions-reference
*
* Examples:
*
* Input: binaryauthorization.attestors.update
* Output: false
*
* Input: binaryauthorization.continuousValidationConfig.get
* Output: true
*/
export function isReadOnlyPermission(permission: string): boolean {
const splitPermission = permission.split('.');
const action = splitPermission[splitPermission.length - 1];
return (
action === 'group' ||
action.startsWith('get') ||
action.startsWith('list') ||
action.startsWith('export') ||
action.startsWith('view') ||
action.startsWith('check') ||
action.startsWith('read')
);
}
export function isReadOnlyRole(role: iam_v1.Schema$Role): boolean {
for (const permission of role.includedPermissions || []) {
if (!isReadOnlyPermission(permission)) {
return false;
}
}
return true;
}
/**
* Some permissions are not 1:1 to the actual service API short name.
*
* For example:
*
* Permission: resourcemanager.projects.get
* Actual API service: cloudresourcemanager.googleapis.com
*/
const SHORT_SERVICE_TO_SERVICE_API_MAP: Map<string, string> = new Map([
['resourcemanager', 'cloudresourcemanager'],
]);
/**
* Computes a Google Cloud a Google Cloud API service name from a permission
*
* See: https://cloud.google.com/iam/docs/permissions-reference
*
* Examples:
*
* Input: binaryauthorization.attestors.update
* Output: binaryauthorization.googleapis.com
*/
export function getFullServiceApiNameFromPermission(
permission: string,
): string {
const splitPermission = permission.split('.');
const shortService = splitPermission[0];
const actualShortService =
SHORT_SERVICE_TO_SERVICE_API_MAP.get(shortService) || shortService;
return `${actualShortService}.googleapis.com`;
}
export function getUniqueFullServiceApiNamesFromRole(
role: iam_v1.Schema$Role,
): string[] {
const serviceApiNameSet: Set<string> = new Set();
for (const permission of role.includedPermissions || []) {
serviceApiNameSet.add(getFullServiceApiNameFromPermission(permission));
}
return Array.from(serviceApiNameSet);
}
export function isServiceAccountEmail(email: string): boolean {
return email.endsWith('.gserviceaccount.com');
}