Skip to content

Commit e31be9b

Browse files
authored
Merge branch 'main' into cs-241-framework-dropdown
2 parents bcb4ef7 + c84480e commit e31be9b

17 files changed

Lines changed: 1434 additions & 326 deletions

File tree

apps/api/src/cloud-security/cloud-security.controller.ts

Lines changed: 103 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import { CloudSecurityQueryService } from './cloud-security-query.service';
2525
import { CloudSecurityLegacyService } from './cloud-security-legacy.service';
2626
import { logCloudSecurityActivity } from './cloud-security-audit';
2727
import { CloudSecurityActivityService } from './cloud-security-activity.service';
28-
import { GCPSecurityService } from './providers/gcp-security.service';
28+
import {
29+
GCPSecurityService,
30+
type GcpSetupStep,
31+
type GcpSetupStepId,
32+
} from './providers/gcp-security.service';
2933
import { AzureSecurityService } from './providers/azure-security.service';
3034

3135
@Controller({ path: 'cloud-security', version: '1' })
@@ -226,55 +230,119 @@ export class CloudSecurityController {
226230
@OrganizationId() organizationId: string,
227231
) {
228232
try {
229-
const connection = await this.cloudSecurityService.getConnectionForDetect(
230-
connectionId,
231-
organizationId,
232-
);
233+
const context = await this.resolveGcpSetupContext(connectionId, organizationId);
233234

234-
const credentials = connection.credentials as Record<string, unknown>;
235-
const accessToken = credentials?.access_token as string;
236-
if (!accessToken) {
237-
throw new Error('No access token found. Reconnect the GCP integration.');
238-
}
235+
const result = await this.gcpSecurityService.autoSetup({
236+
accessToken: context.accessToken,
237+
organizationId: context.organizationId ?? '',
238+
projectId: context.projectId,
239+
});
239240

240-
const variables = (connection.variables ?? {}) as Record<string, unknown>;
241-
const gcpOrgId = variables.organization_id as string | undefined;
242-
243-
// Auto-detect org if not set
244-
let orgId = gcpOrgId;
245-
if (!orgId) {
246-
const orgs = await this.gcpSecurityService.detectOrganizations(accessToken);
247-
if (orgs.length > 0) {
248-
orgId = orgs[0].id;
249-
await this.cloudSecurityService.saveConnectionVariable(connectionId, 'organization_id', orgId, organizationId);
250-
}
251-
}
241+
return {
242+
...result,
243+
steps: this.withGcpResolveActions(result.steps, connectionId),
244+
organizationId: context.organizationId,
245+
projectId: context.projectId,
246+
};
247+
} catch (error) {
248+
const message =
249+
error instanceof Error ? error.message : 'GCP setup failed';
250+
throw new HttpException(message, HttpStatus.BAD_REQUEST);
251+
}
252+
}
252253

253-
// Auto-detect project if not known
254-
const projects = await this.gcpSecurityService.detectProjects(accessToken);
255-
const projectId = projects[0]?.id;
256-
if (!projectId) {
257-
throw new Error('No GCP projects found. Ensure your account has access to at least one project.');
254+
@Post('setup-gcp/:connectionId/resolve-step')
255+
@UseGuards(HybridAuthGuard, PermissionGuard)
256+
@RequirePermission('integration', 'update')
257+
async resolveGcpSetupStep(
258+
@Param('connectionId') connectionId: string,
259+
@Body() body: { stepId: GcpSetupStepId },
260+
@OrganizationId() organizationId: string,
261+
) {
262+
try {
263+
if (!body?.stepId) {
264+
throw new Error('stepId is required');
258265
}
259266

260-
const result = await this.gcpSecurityService.autoSetup({
261-
accessToken,
262-
organizationId: orgId ?? '',
263-
projectId,
267+
const context = await this.resolveGcpSetupContext(connectionId, organizationId);
268+
const result = await this.gcpSecurityService.resolveSetupStep({
269+
stepId: body.stepId,
270+
accessToken: context.accessToken,
271+
organizationId: context.organizationId ?? '',
272+
projectId: context.projectId,
264273
});
265274

266275
return {
267-
...result,
268-
organizationId: orgId,
269-
projectId,
276+
email: result.email,
277+
step: this.withGcpResolveActions([result.step], connectionId)[0],
278+
organizationId: context.organizationId,
279+
projectId: context.projectId,
270280
};
271281
} catch (error) {
272282
const message =
273-
error instanceof Error ? error.message : 'GCP setup failed';
283+
error instanceof Error ? error.message : 'Failed to resolve setup step';
274284
throw new HttpException(message, HttpStatus.BAD_REQUEST);
275285
}
276286
}
277287

288+
private withGcpResolveActions(steps: GcpSetupStep[], connectionId: string): GcpSetupStep[] {
289+
return steps.map((step) => {
290+
if (step.success) return step;
291+
return {
292+
...step,
293+
resolveAction: {
294+
label: 'Resolve this',
295+
method: 'POST',
296+
endpoint: `/v1/cloud-security/setup-gcp/${connectionId}/resolve-step`,
297+
body: { stepId: step.id },
298+
},
299+
};
300+
});
301+
}
302+
303+
private async resolveGcpSetupContext(connectionId: string, organizationId: string) {
304+
const connection = await this.cloudSecurityService.getConnectionForDetect(
305+
connectionId,
306+
organizationId,
307+
);
308+
309+
const credentials = connection.credentials as Record<string, unknown>;
310+
const accessToken = credentials?.access_token as string;
311+
if (!accessToken) {
312+
throw new Error('No access token found. Reconnect the GCP integration.');
313+
}
314+
315+
const variables = (connection.variables ?? {}) as Record<string, unknown>;
316+
let gcpOrgId = variables.organization_id as string | undefined;
317+
318+
if (!gcpOrgId) {
319+
const orgs = await this.gcpSecurityService.detectOrganizations(accessToken);
320+
if (orgs.length > 0) {
321+
gcpOrgId = orgs[0].id;
322+
await this.cloudSecurityService.saveConnectionVariable(
323+
connectionId,
324+
'organization_id',
325+
gcpOrgId,
326+
organizationId,
327+
);
328+
}
329+
}
330+
331+
const projects = await this.gcpSecurityService.detectProjects(accessToken);
332+
const projectId = projects[0]?.id;
333+
if (!projectId) {
334+
throw new Error(
335+
'No GCP projects found. Ensure your account has access to at least one project.',
336+
);
337+
}
338+
339+
return {
340+
accessToken,
341+
organizationId: gcpOrgId,
342+
projectId,
343+
};
344+
}
345+
278346
@Post('setup-azure/:connectionId')
279347
@UseGuards(HybridAuthGuard, PermissionGuard)
280348
@RequirePermission('integration', 'update')

apps/api/src/cloud-security/cloud-security.service.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,13 @@ export class CloudSecurityService {
162162
// Get variables for the scan
163163
const variables = (connection.variables as Record<string, unknown>) || {};
164164

165-
// Security baseline: always scanned regardless of toggles.
166-
// Every AWS account should have these configured.
167-
const BASELINE_SERVICES = [
168-
'cloudtrail', 'config', 'guardduty', 'iam-analyzer', 'cloudwatch', 'kms',
169-
];
165+
// Provider baselines are always scanned regardless of toggles.
166+
const BASELINE_SERVICES_BY_PROVIDER: Record<string, string[]> = {
167+
aws: ['cloudtrail', 'config', 'guardduty', 'iam-analyzer', 'cloudwatch', 'kms'],
168+
gcp: ['security-command-center'],
169+
azure: [],
170+
};
171+
const baselineServices = BASELINE_SERVICES_BY_PROVIDER[providerSlug] ?? [];
170172

171173
// Smart service filtering: auto-detect is additive, user can only exclude.
172174
// Scan = (detectedServices MINUS disabledServices) UNION baselineServices.
@@ -178,11 +180,11 @@ export class CloudSecurityService {
178180
if (Array.isArray(variables.enabledServices) && (variables.enabledServices as string[]).length > 0) {
179181
// Legacy format: explicit enabled list (backward compat) + baseline
180182
const userEnabled = (variables.enabledServices as string[]).filter((s) => !disabledServices.has(s));
181-
enabledServices = [...new Set([...userEnabled, ...BASELINE_SERVICES])];
183+
enabledServices = [...new Set([...userEnabled, ...baselineServices])];
182184
} else if (Array.isArray(variables.detectedServices) && (variables.detectedServices as string[]).length > 0) {
183185
// New smart format: detected minus disabled + baseline always included
184186
const filtered = (variables.detectedServices as string[]).filter((s) => !disabledServices.has(s));
185-
enabledServices = [...new Set([...filtered, ...BASELINE_SERVICES])];
187+
enabledServices = [...new Set([...filtered, ...baselineServices])];
186188
}
187189
// else: undefined = scan all adapters (no detection data at all)
188190

@@ -219,6 +221,7 @@ export class CloudSecurityService {
219221
findings = await this.gcpService.scanSecurityFindings(
220222
credentials,
221223
variables,
224+
enabledServices,
222225
);
223226
break;
224227
case 'aws':
@@ -358,19 +361,32 @@ export class CloudSecurityService {
358361
return [];
359362
}
360363

361-
// Save detected services and auto-enable them (remove from disabledServices)
362-
const currentDisabled = Array.isArray(variables.disabledServices)
363-
? (variables.disabledServices as string[])
364-
: [];
365-
const updatedDisabled = currentDisabled.filter((s) => !detected.includes(s));
364+
// Merge with existing detected services and only auto-enable genuinely NEW detections.
365+
// This preserves explicit user toggles (both enabled and disabled).
366+
const existingDetected = new Set<string>(
367+
Array.isArray(variables.detectedServices)
368+
? (variables.detectedServices as string[])
369+
: [],
370+
);
371+
const updatedDisabled = new Set<string>(
372+
Array.isArray(variables.disabledServices)
373+
? (variables.disabledServices as string[])
374+
: [],
375+
);
376+
for (const id of detected) {
377+
if (!existingDetected.has(id)) {
378+
updatedDisabled.delete(id);
379+
}
380+
existingDetected.add(id);
381+
}
366382

367383
await db.integrationConnection.update({
368384
where: { id: connectionId },
369385
data: {
370386
variables: {
371387
...variables,
372-
detectedServices: detected,
373-
disabledServices: updatedDisabled,
388+
detectedServices: [...existingDetected],
389+
disabledServices: [...updatedDisabled],
374390
},
375391
},
376392
});

0 commit comments

Comments
 (0)