Skip to content

Commit b61b0db

Browse files
authored
Merge pull request #2547 from trycompai/feat/gcp-multi-project-scoping
feat(gcp): multi-project scoping for scans, services, and remediation
2 parents 8854953 + e96e850 commit b61b0db

File tree

297 files changed

+9000
-3669
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

297 files changed

+9000
-3669
lines changed

apps/api/src/admin-organizations/admin-audit-log.interceptor.spec.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,36 @@ jest.mock('@db', () => ({
1818
},
1919
Prisma: {},
2020
db: {
21-
auditLog: { get create() { return mockCreate; } },
22-
policy: { get findFirst() { return mockPolicyFind; } },
23-
taskItem: { get findFirst() { return mockTaskFind; } },
24-
vendor: { get findFirst() { return mockVendorFind; } },
25-
finding: { get findFirst() { return mockFindingFind; } },
26-
context: { get findFirst() { return mockContextFind; } },
21+
auditLog: {
22+
get create() {
23+
return mockCreate;
24+
},
25+
},
26+
policy: {
27+
get findFirst() {
28+
return mockPolicyFind;
29+
},
30+
},
31+
taskItem: {
32+
get findFirst() {
33+
return mockTaskFind;
34+
},
35+
},
36+
vendor: {
37+
get findFirst() {
38+
return mockVendorFind;
39+
},
40+
},
41+
finding: {
42+
get findFirst() {
43+
return mockFindingFind;
44+
},
45+
},
46+
context: {
47+
get findFirst() {
48+
return mockContextFind;
49+
},
50+
},
2751
},
2852
}));
2953

apps/api/src/admin-organizations/admin-context.controller.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export class AdminContextController {
4343
}
4444

4545
@Post(':orgId/context')
46-
@ApiOperation({ summary: 'Create a context entry for an organization (admin)' })
46+
@ApiOperation({
47+
summary: 'Create a context entry for an organization (admin)',
48+
})
4749
@UsePipes(
4850
new ValidationPipe({
4951
whitelist: true,
@@ -59,7 +61,9 @@ export class AdminContextController {
5961
}
6062

6163
@Patch(':orgId/context/:contextId')
62-
@ApiOperation({ summary: 'Update a context entry for an organization (admin)' })
64+
@ApiOperation({
65+
summary: 'Update a context entry for an organization (admin)',
66+
})
6367
@UsePipes(
6468
new ValidationPipe({
6569
whitelist: true,

apps/api/src/admin-organizations/admin-evidence.controller.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ describe('AdminEvidenceController', () => {
2828
beforeEach(async () => {
2929
const module: TestingModule = await Test.createTestingModule({
3030
controllers: [AdminEvidenceController],
31-
providers: [
32-
{ provide: EvidenceFormsService, useValue: mockService },
33-
],
31+
providers: [{ provide: EvidenceFormsService, useValue: mockService }],
3432
}).compile();
3533

3634
controller = module.get<AdminEvidenceController>(AdminEvidenceController);

apps/api/src/admin-organizations/admin-evidence.controller.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ import {
2424
@UseInterceptors(AdminAuditLogInterceptor)
2525
@Throttle({ default: { ttl: 60000, limit: 30 } })
2626
export class AdminEvidenceController {
27-
constructor(
28-
private readonly evidenceFormsService: EvidenceFormsService,
29-
) {}
27+
constructor(private readonly evidenceFormsService: EvidenceFormsService) {}
3028

3129
@Get(':orgId/evidence-forms')
32-
@ApiOperation({ summary: 'List evidence form statuses for an organization (admin)' })
30+
@ApiOperation({
31+
summary: 'List evidence form statuses for an organization (admin)',
32+
})
3333
async listFormStatuses(@Param('orgId') orgId: string) {
3434
return this.evidenceFormsService.getFormStatuses(orgId);
3535
}
@@ -53,8 +53,12 @@ export class AdminEvidenceController {
5353
authContext: buildPlatformAdminAuthContext(req.userId, orgId),
5454
formType,
5555
search,
56-
limit: limit ? String(Math.min(200, Math.max(1, parseInt(limit, 10) || 1))) : undefined,
57-
offset: offset ? String(Math.max(0, parseInt(offset, 10) || 0)) : undefined,
56+
limit: limit
57+
? String(Math.min(200, Math.max(1, parseInt(limit, 10) || 1)))
58+
: undefined,
59+
offset: offset
60+
? String(Math.max(0, parseInt(offset, 10) || 0))
61+
: undefined,
5862
});
5963
}
6064
}

apps/api/src/admin-organizations/admin-findings.controller.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@ export class AdminFindingsController {
3333

3434
@Get(':orgId/findings')
3535
@ApiOperation({ summary: 'List all findings for an organization (admin)' })
36-
async list(
37-
@Param('orgId') orgId: string,
38-
@Query('status') status?: string,
39-
) {
36+
async list(@Param('orgId') orgId: string, @Query('status') status?: string) {
4037
let validatedStatus: FindingStatus | undefined;
4138
if (status) {
4239
if (!Object.values(FindingStatus).includes(status as FindingStatus)) {

apps/api/src/admin-organizations/admin-guard-integration.spec.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ describe('PlatformAdminGuard — runtime rejection scenarios', () => {
9999
});
100100
const ctx = buildContext({ cookie: 'session=valid' });
101101

102-
await expect(guard.canActivate(ctx)).rejects.toThrow(
103-
ForbiddenException,
104-
);
102+
await expect(guard.canActivate(ctx)).rejects.toThrow(ForbiddenException);
105103
await expect(guard.canActivate(ctx)).rejects.toThrow(
106104
'Access denied: Platform admin privileges required',
107105
);
@@ -116,9 +114,7 @@ describe('PlatformAdminGuard — runtime rejection scenarios', () => {
116114
});
117115
const ctx = buildContext({ cookie: 'session=valid' });
118116

119-
await expect(guard.canActivate(ctx)).rejects.toThrow(
120-
ForbiddenException,
121-
);
117+
await expect(guard.canActivate(ctx)).rejects.toThrow(ForbiddenException);
122118
});
123119

124120
it('rejects a user with role "owner" (org role, not platform admin)', async () => {
@@ -130,9 +126,7 @@ describe('PlatformAdminGuard — runtime rejection scenarios', () => {
130126
});
131127
const ctx = buildContext({ cookie: 'session=valid' });
132128

133-
await expect(guard.canActivate(ctx)).rejects.toThrow(
134-
ForbiddenException,
135-
);
129+
await expect(guard.canActivate(ctx)).rejects.toThrow(ForbiddenException);
136130
});
137131

138132
it('rejects when session claims admin but DB says user', async () => {
@@ -146,9 +140,7 @@ describe('PlatformAdminGuard — runtime rejection scenarios', () => {
146140
});
147141
const ctx = buildContext({ authorization: 'Bearer valid' });
148142

149-
await expect(guard.canActivate(ctx)).rejects.toThrow(
150-
ForbiddenException,
151-
);
143+
await expect(guard.canActivate(ctx)).rejects.toThrow(ForbiddenException);
152144
expect(mockFindUnique).toHaveBeenCalledWith({
153145
where: { id: 'usr_sneaky' },
154146
select: { id: true, email: true, role: true },

apps/api/src/admin-organizations/admin-organizations.controller.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,25 @@ export class AdminOrganizationsController {
4343
}
4444

4545
@Get('activity')
46-
@ApiOperation({ summary: 'Organization activity report - shows last session per org (platform admin)' })
47-
@ApiQuery({ name: 'inactiveDays', required: false, description: 'Filter orgs with no session in N days (default: 90)' })
48-
@ApiQuery({ name: 'hasAccess', required: false, description: 'Filter by hasAccess (true/false)' })
49-
@ApiQuery({ name: 'onboarded', required: false, description: 'Filter by onboardingCompleted (true/false)' })
46+
@ApiOperation({
47+
summary:
48+
'Organization activity report - shows last session per org (platform admin)',
49+
})
50+
@ApiQuery({
51+
name: 'inactiveDays',
52+
required: false,
53+
description: 'Filter orgs with no session in N days (default: 90)',
54+
})
55+
@ApiQuery({
56+
name: 'hasAccess',
57+
required: false,
58+
description: 'Filter by hasAccess (true/false)',
59+
})
60+
@ApiQuery({
61+
name: 'onboarded',
62+
required: false,
63+
description: 'Filter by onboardingCompleted (true/false)',
64+
})
5065
@ApiQuery({ name: 'page', required: false })
5166
@ApiQuery({ name: 'limit', required: false })
5267
async activity(
@@ -57,9 +72,16 @@ export class AdminOrganizationsController {
5772
@Query('limit') limit?: string,
5873
) {
5974
return this.service.getOrgActivity({
60-
inactiveDays: Math.max(0, Number.isFinite(parseInt(inactiveDays ?? '90', 10)) ? parseInt(inactiveDays ?? '90', 10) : 90),
61-
hasAccess: hasAccess === 'true' ? true : hasAccess === 'false' ? false : undefined,
62-
onboarded: onboarded === 'true' ? true : onboarded === 'false' ? false : undefined,
75+
inactiveDays: Math.max(
76+
0,
77+
Number.isFinite(parseInt(inactiveDays ?? '90', 10))
78+
? parseInt(inactiveDays ?? '90', 10)
79+
: 90,
80+
),
81+
hasAccess:
82+
hasAccess === 'true' ? true : hasAccess === 'false' ? false : undefined,
83+
onboarded:
84+
onboarded === 'true' ? true : onboarded === 'false' ? false : undefined,
6385
page: Math.max(1, parseInt(page || '1', 10) || 1),
6486
limit: Math.min(100, Math.max(1, parseInt(limit || '50', 10) || 50)),
6587
});
@@ -109,9 +131,19 @@ export class AdminOrganizationsController {
109131
}
110132

111133
@Get(':id/audit-logs')
112-
@ApiOperation({ summary: 'Get audit logs for an organization (platform admin)' })
113-
@ApiQuery({ name: 'entityType', required: false, description: 'Filter by entity type (e.g. policy, task)' })
114-
@ApiQuery({ name: 'take', required: false, description: 'Number of logs to return (max 100, default 100)' })
134+
@ApiOperation({
135+
summary: 'Get audit logs for an organization (platform admin)',
136+
})
137+
@ApiQuery({
138+
name: 'entityType',
139+
required: false,
140+
description: 'Filter by entity type (e.g. policy, task)',
141+
})
142+
@ApiQuery({
143+
name: 'take',
144+
required: false,
145+
description: 'Number of logs to return (max 100, default 100)',
146+
})
115147
async getAuditLogs(
116148
@Param('id') id: string,
117149
@Query('entityType') entityType?: string,

apps/api/src/admin-organizations/admin-organizations.service.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,14 @@ export class AdminOrganizationsService {
125125
createdAt: true,
126126
hasAccess: true,
127127
onboardingCompleted: true,
128-
_count: { select: { members: true, tasks: true, policy: true, auditLog: true } },
128+
_count: {
129+
select: {
130+
members: true,
131+
tasks: true,
132+
policy: true,
133+
auditLog: true,
134+
},
135+
},
129136
members: {
130137
where: { deactivated: false },
131138
select: {
@@ -168,14 +175,20 @@ export class AdminOrganizationsService {
168175
lastSession = sess;
169176
}
170177
if (member.role?.includes('owner') && !owner) {
171-
owner = { id: member.user.id, name: member.user.name, email: member.user.email };
178+
owner = {
179+
id: member.user.id,
180+
name: member.user.name,
181+
email: member.user.email,
182+
};
172183
}
173184
}
174185

175186
const lastAuditLog = org.auditLog?.[0]?.timestamp ?? null;
176187
const lastActivity = [lastSession, lastAuditLog]
177188
.filter(Boolean)
178-
.sort((a, b) => (b as Date).getTime() - (a as Date).getTime())[0] as Date | undefined;
189+
.sort((a, b) => (b as Date).getTime() - (a as Date).getTime())[0] as
190+
| Date
191+
| undefined;
179192

180193
const isActive = lastActivity ? lastActivity >= cutoff : false;
181194

@@ -191,7 +204,7 @@ export class AdminOrganizationsService {
191204
auditLogCount: org._count.auditLog,
192205
owner,
193206
lastSession: lastSession?.toISOString() ?? null,
194-
lastAuditLog: lastAuditLog ? (lastAuditLog as Date).toISOString() : null,
207+
lastAuditLog: lastAuditLog ? lastAuditLog.toISOString() : null,
195208
lastActivity: lastActivity?.toISOString() ?? null,
196209
isActive,
197210
};

apps/api/src/admin-organizations/admin-policies.controller.spec.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ jest.mock('@db', () => ({
2929

3030
jest.mock('@trigger.dev/sdk', () => ({
3131
auth: {
32-
createPublicToken: jest
33-
.fn()
34-
.mockResolvedValue('mock-public-access-token'),
32+
createPublicToken: jest.fn().mockResolvedValue('mock-public-access-token'),
3533
},
3634
tasks: {
3735
trigger: jest.fn().mockResolvedValue({ id: 'run_123' }),
@@ -85,9 +83,9 @@ describe('AdminPoliciesController', () => {
8583
});
8684

8785
it('should reject missing status', async () => {
88-
await expect(
89-
controller.update('org_1', 'pol_1', {}),
90-
).rejects.toThrow(BadRequestException);
86+
await expect(controller.update('org_1', 'pol_1', {})).rejects.toThrow(
87+
BadRequestException,
88+
);
9189
});
9290

9391
it('should reject invalid status', async () => {

apps/api/src/admin-organizations/admin-policies.controller.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ export class AdminPoliciesController {
7979
const updateData: Record<string, unknown> = {};
8080

8181
if (body.status !== undefined) {
82-
if (
83-
!Object.values(PolicyStatus).includes(body.status as PolicyStatus)
84-
) {
82+
if (!Object.values(PolicyStatus).includes(body.status as PolicyStatus)) {
8583
throw new BadRequestException(
8684
`Invalid status. Must be one of: ${Object.values(PolicyStatus).join(', ')}`,
8785
);
@@ -135,9 +133,7 @@ export class AdminPoliciesController {
135133
});
136134

137135
const uniqueFrameworks = Array.from(
138-
new Map(
139-
instances.map((fi) => [fi.framework.id, fi.framework]),
140-
).values(),
136+
new Map(instances.map((fi) => [fi.framework.id, fi.framework])).values(),
141137
).map((f) => ({
142138
id: f.id,
143139
name: f.name,

0 commit comments

Comments
 (0)