Skip to content

Commit 2de8e21

Browse files
committed
feat(gcp): add GCP integration with IAM access and monitoring checks
1 parent be2f0df commit 2de8e21

File tree

15 files changed

+1881
-1766
lines changed

15 files changed

+1881
-1766
lines changed

packages/docs/openapi.json

Lines changed: 85 additions & 1766 deletions
Large diffs are not rendered by default.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { TASK_TEMPLATES } from '../../../task-mappings';
2+
import type { CheckContext, IntegrationCheck } from '../../../types';
3+
import { createGCPClients, getProjectIAMPolicy, listServiceAccounts } from '../helpers';
4+
import type { GCPCredentials } from '../types';
5+
6+
/**
7+
* GCP IAM Access Review Check
8+
*
9+
* Fetches IAM bindings and service accounts for access review.
10+
* Maps to: Access Review Log task
11+
*/
12+
export const iamAccessCheck: IntegrationCheck = {
13+
id: 'iam-access',
14+
name: 'IAM Access Review',
15+
description: 'Fetch IAM policies and service accounts from GCP for access review',
16+
taskMapping: TASK_TEMPLATES.accessReviewLog,
17+
variables: [],
18+
19+
run: async (ctx: CheckContext) => {
20+
ctx.log('Starting GCP IAM Access Review check');
21+
22+
const credentials = ctx.credentials as unknown as GCPCredentials;
23+
24+
let gcp;
25+
try {
26+
gcp = await createGCPClients(credentials, ctx.log);
27+
} catch (error) {
28+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
29+
ctx.fail({
30+
title: 'Failed to Connect to GCP',
31+
resourceType: 'connection',
32+
resourceId: 'gcp-auth',
33+
severity: 'critical',
34+
description: `Could not authenticate with GCP: ${errorMessage}`,
35+
remediation: 'Verify the service account key is valid and has the required permissions',
36+
evidence: { error: String(error) },
37+
});
38+
return;
39+
}
40+
41+
const projectId = gcp.projectId;
42+
ctx.log(`Fetching IAM policy for project: ${projectId}`);
43+
44+
// Fetch IAM policy
45+
let iamPolicy;
46+
try {
47+
iamPolicy = await getProjectIAMPolicy(gcp.client, projectId);
48+
} catch (error) {
49+
const errorMessage = error instanceof Error ? error.message : String(error);
50+
ctx.fail({
51+
title: 'Failed to Fetch IAM Policy',
52+
resourceType: 'iam-policy',
53+
resourceId: projectId,
54+
severity: 'high',
55+
description: `Could not fetch IAM policy: ${errorMessage}`,
56+
remediation: 'Ensure the service account has the "Security Reviewer" role',
57+
evidence: { error: errorMessage, projectId },
58+
});
59+
return;
60+
}
61+
62+
ctx.log(`Found ${iamPolicy.bindings?.length || 0} IAM bindings`);
63+
64+
// Fetch service accounts
65+
ctx.log('Fetching service accounts...');
66+
const serviceAccounts: Array<{
67+
name: string;
68+
email: string;
69+
displayName?: string;
70+
disabled: boolean;
71+
}> = [];
72+
73+
try {
74+
let pageToken: string | undefined;
75+
do {
76+
const response = await listServiceAccounts(gcp.client, projectId, pageToken);
77+
if (response.accounts) {
78+
serviceAccounts.push(...response.accounts);
79+
}
80+
pageToken = response.nextPageToken;
81+
} while (pageToken);
82+
} catch (error) {
83+
ctx.log(`Could not fetch service accounts: ${error}`);
84+
}
85+
86+
ctx.log(`Found ${serviceAccounts.length} service accounts`);
87+
88+
// Process IAM bindings to extract users and their roles
89+
const userRoles = new Map<string, string[]>();
90+
const roleMembers = new Map<string, string[]>();
91+
92+
for (const binding of iamPolicy.bindings || []) {
93+
roleMembers.set(binding.role, binding.members);
94+
95+
for (const member of binding.members) {
96+
const existing = userRoles.get(member) || [];
97+
existing.push(binding.role);
98+
userRoles.set(member, existing);
99+
}
100+
}
101+
102+
// Build the access list
103+
const accessList = Array.from(userRoles.entries()).map(([member, roles]) => {
104+
// Parse member type (user:, serviceAccount:, group:, etc.)
105+
const [type, identifier] = member.includes(':') ? member.split(':', 2) : ['unknown', member];
106+
107+
return {
108+
member,
109+
type,
110+
identifier,
111+
roles,
112+
roleCount: roles.length,
113+
};
114+
});
115+
116+
// Categorize members
117+
const users = accessList.filter((a) => a.type === 'user');
118+
const serviceAccountMembers = accessList.filter((a) => a.type === 'serviceAccount');
119+
const groups = accessList.filter((a) => a.type === 'group');
120+
const domains = accessList.filter((a) => a.type === 'domain');
121+
122+
// Pass with the full access list as evidence
123+
ctx.pass({
124+
title: 'IAM Access Review',
125+
resourceType: 'project',
126+
resourceId: projectId,
127+
description: `Retrieved IAM access for project ${projectId}: ${users.length} users, ${serviceAccountMembers.length} service accounts, ${groups.length} groups`,
128+
evidence: {
129+
projectId,
130+
totalBindings: iamPolicy.bindings?.length || 0,
131+
totalMembers: accessList.length,
132+
userCount: users.length,
133+
serviceAccountCount: serviceAccountMembers.length,
134+
groupCount: groups.length,
135+
domainCount: domains.length,
136+
reviewedAt: new Date().toISOString(),
137+
accessList,
138+
serviceAccounts: serviceAccounts.map((sa) => ({
139+
email: sa.email,
140+
displayName: sa.displayName,
141+
disabled: sa.disabled,
142+
})),
143+
rolesSummary: Array.from(roleMembers.entries()).map(([role, members]) => ({
144+
role,
145+
memberCount: members.length,
146+
})),
147+
},
148+
});
149+
150+
ctx.log('GCP IAM Access Review check complete');
151+
},
152+
};
153+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { securityFindingsCheck } from './security-findings';
2+
export { iamAccessCheck } from './iam-access';
3+
export { monitoringAlertingCheck } from './monitoring-alerting';
4+

0 commit comments

Comments
 (0)