Skip to content

Commit 7268e9d

Browse files
committed
chore: merge main into release for new releases
2 parents 76185ab + 0cfbce6 commit 7268e9d

133 files changed

Lines changed: 8520 additions & 1827 deletions

File tree

Some content is hidden

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

‎.cursor/rules/ui.mdc‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
2-
description: Use when building or modifying React components, styling, or working with the design system
3-
alwaysApply: false
2+
alwaysApply: true
43
---
54
# UI Components
65

‎apps/api/src/attachments/attachments.service.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
CopyObjectCommand,
23
DeleteObjectCommand,
34
GetObjectCommand,
45
PutObjectCommand,
@@ -334,6 +335,44 @@ export class AttachmentsService {
334335
}
335336
}
336337

338+
/**
339+
* Copy a policy PDF to a new S3 key for versioning
340+
*/
341+
async copyPolicyVersionPdf(
342+
sourceKey: string,
343+
destinationKey: string,
344+
): Promise<string | null> {
345+
try {
346+
await this.s3Client.send(
347+
new CopyObjectCommand({
348+
Bucket: this.bucketName,
349+
CopySource: `${this.bucketName}/${sourceKey}`,
350+
Key: destinationKey,
351+
}),
352+
);
353+
return destinationKey;
354+
} catch (error) {
355+
console.error('Error copying policy PDF:', error);
356+
return null;
357+
}
358+
}
359+
360+
/**
361+
* Delete a policy version PDF from S3
362+
*/
363+
async deletePolicyVersionPdf(s3Key: string): Promise<void> {
364+
try {
365+
await this.s3Client.send(
366+
new DeleteObjectCommand({
367+
Bucket: this.bucketName,
368+
Key: s3Key,
369+
}),
370+
);
371+
} catch (error) {
372+
console.error('Error deleting policy PDF:', error);
373+
}
374+
}
375+
337376
/**
338377
* Generate signed URL for file download
339378
*/

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

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -228,37 +228,40 @@ export class CloudSecurityService {
228228
const passedCount = findings.filter((f) => f.passed).length;
229229
const failedCount = findings.filter((f) => !f.passed).length;
230230

231-
// Create a scan run record
232-
const scanRun = await db.integrationCheckRun.create({
233-
data: {
234-
connectionId,
235-
checkId: `${provider}-security-scan`,
236-
checkName: `${provider.toUpperCase()} Security Scan`,
237-
status: 'success',
238-
startedAt: new Date(),
239-
completedAt: new Date(),
240-
totalChecked: findings.length,
241-
passedCount,
242-
failedCount,
243-
},
244-
});
245-
246-
// Store each finding as a check result
247-
if (findings.length > 0) {
248-
await db.integrationCheckResult.createMany({
249-
data: findings.map((finding) => ({
250-
checkRunId: scanRun.id,
251-
passed: finding.passed ?? false,
252-
resourceType: finding.resourceType,
253-
resourceId: finding.resourceId,
254-
title: finding.title,
255-
description: finding.description,
256-
severity: finding.passed ? 'info' : finding.severity, // Passed checks are info level
257-
remediation: finding.remediation,
258-
evidence: (finding.evidence || {}) as object,
259-
collectedAt: new Date(finding.createdAt),
260-
})),
231+
// Use a transaction to ensure atomicity - both run and results are created together
232+
await db.$transaction(async (tx) => {
233+
// Create a scan run record
234+
const scanRun = await tx.integrationCheckRun.create({
235+
data: {
236+
connectionId,
237+
checkId: `${provider}-security-scan`,
238+
checkName: `${provider.toUpperCase()} Security Scan`,
239+
status: 'success',
240+
startedAt: new Date(),
241+
completedAt: new Date(),
242+
totalChecked: findings.length,
243+
passedCount,
244+
failedCount,
245+
},
261246
});
262-
}
247+
248+
// Store each finding as a check result
249+
if (findings.length > 0) {
250+
await tx.integrationCheckResult.createMany({
251+
data: findings.map((finding) => ({
252+
checkRunId: scanRun.id,
253+
passed: finding.passed ?? false,
254+
resourceType: finding.resourceType,
255+
resourceId: finding.resourceId,
256+
title: finding.title,
257+
description: finding.description ?? '',
258+
severity: finding.passed ? 'info' : finding.severity,
259+
remediation: finding.remediation ?? null,
260+
evidence: (finding.evidence || {}) as object,
261+
collectedAt: new Date(finding.createdAt),
262+
})),
263+
});
264+
}
265+
});
263266
}
264267
}

‎apps/api/src/findings/dto/update-finding.dto.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,16 @@ export class UpdateFindingDto {
3939
@IsNotEmpty({ message: 'Content cannot be empty if provided' })
4040
@MaxLength(5000)
4141
content?: string;
42+
43+
@ApiProperty({
44+
description: 'Auditor note when requesting revision (only for needs_revision status)',
45+
example: 'Please provide clearer screenshots showing the timestamp.',
46+
maxLength: 2000,
47+
required: false,
48+
nullable: true,
49+
})
50+
@IsString()
51+
@IsOptional()
52+
@MaxLength(2000)
53+
revisionNote?: string | null;
4254
}

‎apps/api/src/findings/findings.service.ts‎

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ export class FindingsService {
7171
],
7272
});
7373

74-
this.logger.log(
75-
`Retrieved ${findings.length} findings for task ${taskId}`,
76-
);
74+
this.logger.log(`Retrieved ${findings.length} findings for task ${taskId}`);
7775
return findings;
7876
}
7977

@@ -311,19 +309,38 @@ export class FindingsService {
311309
}
312310

313311
// ready_for_review can only be set by non-auditor admins/owners (client signals to auditor)
314-
if (updateDto.status === FindingStatus.ready_for_review && isAuditor && !isPlatformAdmin) {
312+
if (
313+
updateDto.status === FindingStatus.ready_for_review &&
314+
isAuditor &&
315+
!isPlatformAdmin
316+
) {
315317
throw new ForbiddenException(
316318
`Auditors cannot set status to 'ready_for_review'. This status is for clients to signal readiness.`,
317319
);
318320
}
319321
}
320322

323+
// Handle revisionNote logic:
324+
// - Set revisionNote when status is needs_revision and a note is provided
325+
// - Clear revisionNote when status changes to anything other than needs_revision
326+
let revisionNoteUpdate: { revisionNote?: string | null } = {};
327+
if (updateDto.status === FindingStatus.needs_revision) {
328+
// Set revision note if provided (can be null to clear)
329+
if (updateDto.revisionNote !== undefined) {
330+
revisionNoteUpdate = { revisionNote: updateDto.revisionNote || null };
331+
}
332+
} else if (updateDto.status !== undefined) {
333+
// Clear revision note when moving to a different status
334+
revisionNoteUpdate = { revisionNote: null };
335+
}
336+
321337
const updatedFinding = await db.finding.update({
322338
where: { id: findingId },
323339
data: {
324340
...(updateDto.status !== undefined && { status: updateDto.status }),
325341
...(updateDto.type !== undefined && { type: updateDto.type }),
326342
...(updateDto.content !== undefined && { content: updateDto.content }),
343+
...revisionNoteUpdate,
327344
},
328345
include: {
329346
createdBy: {
@@ -411,22 +428,34 @@ export class FindingsService {
411428

412429
switch (updateDto.status) {
413430
case FindingStatus.ready_for_review:
414-
this.logger.log(`Triggering 'ready_for_review' notification for finding ${findingId}`);
431+
this.logger.log(
432+
`Triggering 'ready_for_review' notification for finding ${findingId}`,
433+
);
415434
void this.findingNotifierService.notifyReadyForReview({
416435
...notificationParams,
417436
findingCreatorMemberId: finding.createdById,
418437
});
419438
break;
420439
case FindingStatus.needs_revision:
421-
this.logger.log(`Triggering 'needs_revision' notification for finding ${findingId}`);
422-
void this.findingNotifierService.notifyNeedsRevision(notificationParams);
440+
this.logger.log(
441+
`Triggering 'needs_revision' notification for finding ${findingId}`,
442+
);
443+
void this.findingNotifierService.notifyNeedsRevision(
444+
notificationParams,
445+
);
423446
break;
424447
case FindingStatus.closed:
425-
this.logger.log(`Triggering 'closed' notification for finding ${findingId}`);
426-
void this.findingNotifierService.notifyFindingClosed(notificationParams);
448+
this.logger.log(
449+
`Triggering 'closed' notification for finding ${findingId}`,
450+
);
451+
void this.findingNotifierService.notifyFindingClosed(
452+
notificationParams,
453+
);
427454
break;
428455
case FindingStatus.open:
429-
this.logger.log(`Status changed to 'open' for finding ${findingId}. No notification sent.`);
456+
this.logger.log(
457+
`Status changed to 'open' for finding ${findingId}. No notification sent.`,
458+
);
430459
break;
431460
default:
432461
this.logger.warn(
@@ -489,6 +518,9 @@ export class FindingsService {
489518
// Verify finding exists
490519
await this.findById(organizationId, findingId);
491520

492-
return this.findingAuditService.getFindingActivity(findingId, organizationId);
521+
return this.findingAuditService.getFindingActivity(
522+
findingId,
523+
organizationId,
524+
);
493525
}
494526
}

‎apps/api/src/lib/fleet.service.ts‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ export class FleetService {
6767
}
6868
}
6969

70+
/**
71+
* Remove a single host from FleetDM by ID
72+
* @param hostId - The FleetDM host ID
73+
*/
74+
async removeHostById(hostId: number): Promise<void> {
75+
try {
76+
await this.fleetInstance.delete(`/hosts/${hostId}`);
77+
this.logger.debug(`Deleted host ${hostId} from FleetDM`);
78+
} catch (error) {
79+
this.logger.error(`Failed to delete host ${hostId}:`, error);
80+
throw new Error(`Failed to remove host ${hostId}`);
81+
}
82+
}
83+
7084
async getMultipleHosts(hostIds: number[]) {
7185
try {
7286
const requests = hostIds.map((id) => this.getHostById(id));
@@ -104,14 +118,12 @@ export class FleetService {
104118
// Extract host IDs
105119
const hostIds = labelHosts.hosts.map((host: { id: number }) => host.id);
106120

107-
// Delete each host
121+
// Delete each host using removeHostById for consistent behavior
108122
const deletePromises = hostIds.map(async (hostId: number) => {
109123
try {
110-
await this.fleetInstance.delete(`/hosts/${hostId}`);
111-
this.logger.debug(`Deleted host ${hostId} from FleetDM`);
124+
await this.removeHostById(hostId);
112125
return { success: true, hostId };
113-
} catch (error) {
114-
this.logger.error(`Failed to delete host ${hostId}:`, error);
126+
} catch {
115127
return { success: false, hostId };
116128
}
117129
});

‎apps/api/src/people/people.controller.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Delete,
77
Body,
88
Param,
9+
ParseIntPipe,
910
UseGuards,
1011
HttpCode,
1112
HttpStatus,
@@ -22,6 +23,7 @@ import {
2223
} from '@nestjs/swagger';
2324
import { AuthContext, OrganizationId } from '../auth/auth-context.decorator';
2425
import { HybridAuthGuard } from '../auth/hybrid-auth.guard';
26+
import { RequireRoles } from '../auth/role-validator.guard';
2527
import type { AuthContext as AuthContextType } from '../auth/types';
2628
import { CreatePeopleDto } from './dto/create-people.dto';
2729
import { UpdatePeopleDto } from './dto/update-people.dto';
@@ -34,6 +36,7 @@ import { BULK_CREATE_MEMBERS_RESPONSES } from './schemas/bulk-create-members.res
3436
import { GET_PERSON_BY_ID_RESPONSES } from './schemas/get-person-by-id.responses';
3537
import { UPDATE_MEMBER_RESPONSES } from './schemas/update-member.responses';
3638
import { DELETE_MEMBER_RESPONSES } from './schemas/delete-member.responses';
39+
import { REMOVE_HOST_RESPONSES } from './schemas/remove-host.responses';
3740
import { PEOPLE_OPERATIONS } from './schemas/people-operations';
3841
import { PEOPLE_PARAMS } from './schemas/people-params';
3942
import { PEOPLE_BODIES } from './schemas/people-bodies';
@@ -199,6 +202,41 @@ export class PeopleController {
199202
};
200203
}
201204

205+
@Delete(':id/host/:hostId')
206+
@HttpCode(HttpStatus.OK)
207+
@UseGuards(RequireRoles('owner'))
208+
@ApiOperation(PEOPLE_OPERATIONS.removeHost)
209+
@ApiParam(PEOPLE_PARAMS.memberId)
210+
@ApiParam(PEOPLE_PARAMS.hostId)
211+
@ApiResponse(REMOVE_HOST_RESPONSES[200])
212+
@ApiResponse(REMOVE_HOST_RESPONSES[401])
213+
@ApiResponse(REMOVE_HOST_RESPONSES[404])
214+
@ApiResponse(REMOVE_HOST_RESPONSES[500])
215+
async removeHost(
216+
@Param('id') memberId: string,
217+
@Param('hostId', ParseIntPipe) hostId: number,
218+
@OrganizationId() organizationId: string,
219+
@AuthContext() authContext: AuthContextType,
220+
) {
221+
const result = await this.peopleService.removeHostById(
222+
memberId,
223+
organizationId,
224+
hostId,
225+
);
226+
227+
return {
228+
...result,
229+
authType: authContext.authType,
230+
...(authContext.userId &&
231+
authContext.userEmail && {
232+
authenticatedUser: {
233+
id: authContext.userId,
234+
email: authContext.userEmail,
235+
},
236+
}),
237+
};
238+
}
239+
202240
@Delete(':id')
203241
@ApiOperation(PEOPLE_OPERATIONS.deleteMember)
204242
@ApiParam(PEOPLE_PARAMS.memberId)

0 commit comments

Comments
 (0)