Skip to content

Commit 582714d

Browse files
committed
refactor: add AWS credentials validation and integration test actions
1 parent 7dc966d commit 582714d

29 files changed

Lines changed: 2845 additions & 728 deletions

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface CloudProvider {
2222
requiredVariables: string[];
2323
accountId?: string;
2424
regions?: string[];
25+
tenantId?: string;
26+
subscriptionId?: string;
2527
supportsMultipleConnections?: boolean;
2628
}
2729

@@ -116,6 +118,14 @@ export class CloudSecurityQueryService {
116118
(r): r is string => typeof r === 'string',
117119
)
118120
: undefined,
121+
tenantId:
122+
typeof metadata.tenantId === 'string'
123+
? metadata.tenantId
124+
: undefined,
125+
subscriptionId:
126+
typeof metadata.subscriptionId === 'string'
127+
? metadata.subscriptionId
128+
: undefined,
119129
supportsMultipleConnections:
120130
manifest?.supportsMultipleConnections ?? false,
121131
};
@@ -150,6 +160,14 @@ export class CloudSecurityQueryService {
150160
(r): r is string => typeof r === 'string',
151161
)
152162
: undefined,
163+
tenantId:
164+
typeof settings.tenantId === 'string'
165+
? settings.tenantId
166+
: undefined,
167+
subscriptionId:
168+
typeof settings.subscriptionId === 'string'
169+
? settings.subscriptionId
170+
: undefined,
153171
supportsMultipleConnections:
154172
manifest?.supportsMultipleConnections ?? false,
155173
};

apps/api/src/evidence-forms/evidence-forms.controller.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { AuthContext, OrganizationId } from '@/auth/auth-context.decorator';
22
import { HybridAuthGuard } from '@/auth/hybrid-auth.guard';
3+
import { PermissionGuard } from '@/auth/permission.guard';
4+
import { RequirePermission } from '@/auth/require-permission.decorator';
35
import type { AuthContext as AuthContextType } from '@/auth/types';
6+
import { AuditRead } from '@/audit/skip-audit-log.decorator';
47
import {
58
Body,
69
Controller,
@@ -19,7 +22,7 @@ import { EvidenceFormsService } from './evidence-forms.service';
1922

2023
@ApiTags('Evidence Forms')
2124
@Controller({ path: 'evidence-forms', version: '1' })
22-
@UseGuards(HybridAuthGuard)
25+
@UseGuards(HybridAuthGuard, PermissionGuard)
2326
@ApiSecurity('apikey')
2427
@ApiHeader({
2528
name: 'X-Organization-Id',
@@ -31,6 +34,7 @@ export class EvidenceFormsController {
3134
constructor(private readonly evidenceFormsService: EvidenceFormsService) {}
3235

3336
@Get()
37+
@RequirePermission('evidence', 'read')
3438
@ApiOperation({
3539
summary: 'List evidence forms',
3640
description: 'List all available pre-built evidence forms',
@@ -40,6 +44,7 @@ export class EvidenceFormsController {
4044
}
4145

4246
@Get('statuses')
47+
@RequirePermission('evidence', 'read')
4348
@ApiOperation({
4449
summary: 'Get submission statuses for all forms',
4550
description:
@@ -50,6 +55,7 @@ export class EvidenceFormsController {
5055
}
5156

5257
@Get('my-submissions')
58+
@RequirePermission('evidence', 'read')
5359
@ApiOperation({
5460
summary: 'Get current user submissions',
5561
description:
@@ -68,6 +74,7 @@ export class EvidenceFormsController {
6874
}
6975

7076
@Get('my-submissions/pending-count')
77+
@RequirePermission('evidence', 'read')
7178
@ApiOperation({
7279
summary: 'Get pending submission count for current user',
7380
description:
@@ -84,6 +91,7 @@ export class EvidenceFormsController {
8491
}
8592

8693
@Get(':formType')
94+
@RequirePermission('evidence', 'read')
8795
@ApiOperation({
8896
summary: 'Get form definition and submissions',
8997
description:
@@ -108,6 +116,7 @@ export class EvidenceFormsController {
108116
}
109117

110118
@Get(':formType/submissions/:submissionId')
119+
@RequirePermission('evidence', 'read')
111120
@ApiOperation({
112121
summary: 'Get a single submission',
113122
description:
@@ -128,6 +137,7 @@ export class EvidenceFormsController {
128137
}
129138

130139
@Post(':formType/submissions')
140+
@RequirePermission('evidence', 'create')
131141
@ApiOperation({
132142
summary: 'Submit evidence form entry',
133143
description:
@@ -148,6 +158,7 @@ export class EvidenceFormsController {
148158
}
149159

150160
@Patch(':formType/submissions/:submissionId/review')
161+
@RequirePermission('evidence', 'update')
151162
@ApiOperation({
152163
summary: 'Review a submission',
153164
description:
@@ -170,6 +181,7 @@ export class EvidenceFormsController {
170181
}
171182

172183
@Post('uploads')
184+
@RequirePermission('evidence', 'create')
173185
@ApiOperation({
174186
summary: 'Upload evidence form file',
175187
description:
@@ -188,6 +200,8 @@ export class EvidenceFormsController {
188200
}
189201

190202
@Get(':formType/export.csv')
203+
@RequirePermission('evidence', 'read')
204+
@AuditRead()
191205
@ApiOperation({
192206
summary: 'Export form submissions to CSV',
193207
description: 'Export all form submissions for an organization as CSV',
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use server';
2+
3+
import { runIntegrationTests } from '@/trigger/tasks/integration/run-integration-tests';
4+
import { auth } from '@/utils/auth';
5+
import { runs, tasks } from '@trigger.dev/sdk';
6+
import { revalidatePath } from 'next/cache';
7+
import { headers } from 'next/headers';
8+
9+
const MAX_POLL_ATTEMPTS = 60; // Max 2 minutes (60 * 2 seconds)
10+
const POLL_INTERVAL_MS = 2000;
11+
12+
/**
13+
* Run integration tests and wait for completion.
14+
* @param integrationId - Optional. If provided, only run tests for this specific connection.
15+
* If not provided, run tests for all connections in the organization.
16+
*/
17+
export const runTests = async (integrationId?: string) => {
18+
const session = await auth.api.getSession({
19+
headers: await headers(),
20+
});
21+
22+
if (!session) {
23+
return {
24+
success: false,
25+
errors: ['Unauthorized'],
26+
};
27+
}
28+
29+
const orgId = session.session?.activeOrganizationId;
30+
if (!orgId) {
31+
return {
32+
success: false,
33+
errors: ['No active organization'],
34+
};
35+
}
36+
37+
try {
38+
// Trigger the task
39+
const handle = await tasks.trigger<typeof runIntegrationTests>('run-integration-tests', {
40+
organizationId: orgId,
41+
...(integrationId ? { integrationId } : {}),
42+
});
43+
44+
// Poll for completion
45+
let attempts = 0;
46+
while (attempts < MAX_POLL_ATTEMPTS) {
47+
const run = await runs.retrieve(handle.id);
48+
49+
// Check if the run is in a terminal state
50+
if (run.isCompleted) {
51+
const headersList = await headers();
52+
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
53+
path = path.replace(/\/[a-z]{2}\//, '/');
54+
revalidatePath(path);
55+
56+
if (run.isSuccess) {
57+
const output = run.output as {
58+
success?: boolean;
59+
errors?: string[];
60+
failedIntegrations?: Array<{ name: string; error: string }>;
61+
} | null;
62+
63+
if (output?.success === false) {
64+
return {
65+
success: false,
66+
errors: output.errors || ['Scan completed with errors'],
67+
taskId: run.id,
68+
};
69+
}
70+
71+
return {
72+
success: true,
73+
errors: null,
74+
taskId: run.id,
75+
};
76+
}
77+
78+
// Handle all other terminal states (failed, cancelled, or unexpected)
79+
// This ensures we don't continue polling after the run has completed
80+
return {
81+
success: false,
82+
errors: run.isFailed || run.isCancelled
83+
? ['Task failed or was canceled']
84+
: ['Task completed with unexpected status'],
85+
taskId: run.id,
86+
};
87+
}
88+
89+
// Wait before polling again
90+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
91+
attempts++;
92+
}
93+
94+
// Timeout - task is taking too long
95+
return {
96+
success: false,
97+
errors: ['Scan is taking longer than expected. Check the status in Trigger.dev dashboard.'],
98+
taskId: handle.id,
99+
};
100+
} catch (error) {
101+
console.error('Error running integration tests:', error);
102+
103+
return {
104+
success: false,
105+
errors: [error instanceof Error ? error.message : 'Failed to run integration tests'],
106+
};
107+
}
108+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use server';
2+
3+
import { DescribeRegionsCommand, EC2Client } from '@aws-sdk/client-ec2';
4+
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
5+
import { z } from 'zod';
6+
import { authActionClient } from '../../../../../actions/safe-action';
7+
8+
const validateAwsCredentialsSchema = z.object({
9+
accessKeyId: z.string(),
10+
secretAccessKey: z.string(),
11+
});
12+
13+
export const validateAwsCredentialsAction = authActionClient
14+
.inputSchema(validateAwsCredentialsSchema)
15+
.metadata({
16+
name: 'validate-aws-credentials',
17+
track: {
18+
event: 'validate-aws-credentials',
19+
channel: 'cloud-tests',
20+
},
21+
})
22+
.action(async ({ parsedInput: { accessKeyId, secretAccessKey } }) => {
23+
try {
24+
// First, validate credentials using STS
25+
const stsClient = new STSClient({
26+
region: 'us-east-1', // Default region for validation
27+
credentials: {
28+
accessKeyId,
29+
secretAccessKey,
30+
},
31+
});
32+
33+
const identity = await stsClient.send(new GetCallerIdentityCommand({}));
34+
35+
// Get available regions
36+
const ec2Client = new EC2Client({
37+
region: 'us-east-1',
38+
credentials: {
39+
accessKeyId,
40+
secretAccessKey,
41+
},
42+
});
43+
44+
const regionsResponse = await ec2Client.send(new DescribeRegionsCommand({}));
45+
46+
// Map of common region codes to friendly names
47+
const regionNames: Record<string, string> = {
48+
'us-east-1': 'US East (N. Virginia)',
49+
'us-east-2': 'US East (Ohio)',
50+
'us-west-1': 'US West (N. California)',
51+
'us-west-2': 'US West (Oregon)',
52+
'eu-west-1': 'Europe (Ireland)',
53+
'eu-west-2': 'Europe (London)',
54+
'eu-west-3': 'Europe (Paris)',
55+
'eu-central-1': 'Europe (Frankfurt)',
56+
'eu-north-1': 'Europe (Stockholm)',
57+
'eu-south-1': 'Europe (Milan)',
58+
'ap-southeast-1': 'Asia Pacific (Singapore)',
59+
'ap-southeast-2': 'Asia Pacific (Sydney)',
60+
'ap-northeast-1': 'Asia Pacific (Tokyo)',
61+
'ap-northeast-2': 'Asia Pacific (Seoul)',
62+
'ap-northeast-3': 'Asia Pacific (Osaka)',
63+
'ap-south-1': 'Asia Pacific (Mumbai)',
64+
'ap-east-1': 'Asia Pacific (Hong Kong)',
65+
'ca-central-1': 'Canada (Central)',
66+
'sa-east-1': 'South America (São Paulo)',
67+
'me-south-1': 'Middle East (Bahrain)',
68+
'af-south-1': 'Africa (Cape Town)',
69+
};
70+
71+
const regions = (regionsResponse.Regions || [])
72+
.filter((region) => region.RegionName)
73+
.map((region) => {
74+
const code = region.RegionName!;
75+
const friendlyName = regionNames[code] || code;
76+
return {
77+
value: code,
78+
label: `${friendlyName} (${code})`,
79+
};
80+
})
81+
.sort((a, b) => a.value.localeCompare(b.value));
82+
83+
return {
84+
success: true,
85+
accountId: identity.Account,
86+
regions,
87+
};
88+
} catch (error) {
89+
console.error('AWS credential validation failed:', error);
90+
return {
91+
success: false,
92+
error:
93+
error instanceof Error
94+
? error.message
95+
: 'Failed to validate AWS credentials. Please check your access key and secret.',
96+
};
97+
}
98+
});

0 commit comments

Comments
 (0)