Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5e771bc
chore: merge release v3.23.6 back to main [skip ci]
github-actions[bot] Apr 18, 2026
3b5418a
feat(findings): unify findings into a single overview surface
carhartlewis Apr 19, 2026
b3908b5
refactor(findings): update deep link structure and enhance FindingsTa…
carhartlewis Apr 19, 2026
0d49475
feat(findings): add confirmation dialog for finding deletion and link…
carhartlewis Apr 19, 2026
2db2f7b
fix(findings): scope task-finding notifications to stakeholders
carhartlewis Apr 19, 2026
cf4f782
Merge pull request #2596 from trycompai/lewis/comp-people-fix
carhartlewis Apr 19, 2026
e689c95
fix(findings): reclassify legacy-backfilled rows to area='other'
carhartlewis Apr 19, 2026
ba9e970
fix(findings): use AuditLog findingScope to drive legacy reclass + pr…
carhartlewis Apr 19, 2026
30c312f
feat(findings): surface legacy scope in UI, drop destructive reclass …
carhartlewis Apr 19, 2026
4ebcc10
fix(findings): support general risk/vendor/policy findings + dedupe a…
carhartlewis Apr 19, 2026
f41a096
fix(findings): address review feedback (routes, DTO, notifier, admin …
carhartlewis Apr 19, 2026
6b8e4c6
perf(people): move compliance queries out of page shell
carhartlewis Apr 19, 2026
320d053
fix(findings): expose evidence submission target + complete test mock
carhartlewis Apr 19, 2026
9e81604
Merge pull request #2598 from trycompai/lewis/comp-findings-fixes
carhartlewis Apr 19, 2026
fa234fb
fix(findings): accept new FindingArea values regardless of client regen
carhartlewis Apr 19, 2026
96976a9
fix(findings): truncate long labels in Create Finding select triggers
carhartlewis Apr 19, 2026
32a98b7
fix(findings): drop className on SelectTrigger (type error)
carhartlewis Apr 19, 2026
d9a03af
Merge pull request #2599 from trycompai/lewis/comp-findings-fixes-v2
carhartlewis Apr 19, 2026
00a3350
fix(findings): role-gated status options, severity filter, admin picker
carhartlewis Apr 19, 2026
4db03d5
Merge branch 'main' into lewis/comp-findings-fixes-v3
carhartlewis Apr 19, 2026
9f2ccdd
fix(findings): match API's literal auditor-role check in status gating
carhartlewis Apr 19, 2026
6f2474b
Merge pull request #2600 from trycompai/lewis/comp-findings-fixes-v3
carhartlewis Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ export class AdminFindingsController {
validatedStatus = status as FindingStatus;
}

return this.findingsService.findByOrganizationId(orgId, validatedStatus);
return this.findingsService.listForOrganization(orgId, {
status: validatedStatus,
});
}

@Post(':orgId/findings')
Expand Down
19 changes: 8 additions & 11 deletions apps/api/src/audit/audit-log.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,29 +281,26 @@ export function extractPolicyActionDescription(
}

/**
* Detects finding-specific actions and builds a description
* that includes the actor's role (auditor vs platform admin).
* Detects finding-specific actions and builds a human-readable description.
* (Role prefix intentionally omitted — the activity entry already shows the
* actor's name, so "Admin" / "Auditor" labels looked redundant and noisy.)
*/
export function extractFindingDescription(
path: string,
method: string,
resource: string,
userRoles?: string[],
_userRoles?: string[],
): string | null {
if (resource !== 'finding') return null;

const isAuditor = userRoles?.includes('auditor');
const actor = isAuditor ? 'Auditor' : 'Admin';

switch (method) {
case 'POST':
return `${actor} created a finding`;
return 'created a finding';
case 'PATCH':
case 'PUT': {
return `${actor} updated a finding`;
}
case 'PUT':
return 'updated a finding';
case 'DELETE':
return `${actor} deleted a finding`;
return 'deleted a finding';
default:
return null;
}
Expand Down
82 changes: 55 additions & 27 deletions apps/api/src/findings/dto/create-finding.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,77 @@ import {
IsOptional,
MaxLength,
} from 'class-validator';
import { FindingScope, FindingType } from '@db';
import { FindingArea, FindingSeverity, FindingType } from '@db';
import {
evidenceFormTypeSchema,
type EvidenceFormType,
} from '@/evidence-forms/evidence-forms.definitions';

export class CreateFindingDto {
@ApiProperty({
description: 'Task ID this finding is associated with',
example: 'tsk_abc123',
required: false,
})
@ApiProperty({ description: 'Task ID', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
taskId?: string;

@ApiProperty({
description: 'Evidence submission ID this finding is associated with',
example: 'evs_abc123',
required: false,
})
@ApiProperty({ description: 'Evidence submission ID', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
evidenceSubmissionId?: string;

@ApiProperty({
description:
'Evidence form type this finding is associated with (e.g., access-request, whistleblower-report)',
example: 'access-request',
description: 'Evidence form type',
enum: evidenceFormTypeSchema.options,
required: false,
})
@IsIn(evidenceFormTypeSchema.options)
@IsOptional()
evidenceFormType?: EvidenceFormType;

@ApiProperty({ description: 'Policy ID', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
policyId?: string;
Comment thread
carhartlewis marked this conversation as resolved.

@ApiProperty({ description: 'Vendor ID', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
vendorId?: string;

@ApiProperty({ description: 'Risk ID', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
riskId?: string;

@ApiProperty({ description: 'Member ID (person this finding targets)', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
memberId?: string;

@ApiProperty({ description: 'Device ID', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
deviceId?: string;

@ApiProperty({
description:
'People area scope (e.g. people directory) when not tied to a task or evidence',
enum: FindingScope,
description: 'Broad area when the finding is not tied to a specific item',
enum: FindingArea,
required: false,
})
@IsEnum(FindingScope)
// Use an explicit string list instead of @IsEnum(FindingArea). The Prisma-
// generated enum is captured at decorator-eval time, so a dev server started
// before `prisma generate` picked up new enum values will keep rejecting
// them (e.g. "area must be one of the following values: people, documents,
// compliance" even though `risks`/`vendors`/`policies` are now valid).
@IsIn(['people', 'documents', 'compliance', 'risks', 'vendors', 'policies', 'other'])
@IsOptional()
scope?: FindingScope;
area?: FindingArea;

@ApiProperty({
description: 'Type of finding (SOC 2 or ISO 27001)',
Expand All @@ -63,20 +89,22 @@ export class CreateFindingDto {
type?: FindingType;

@ApiProperty({
description: 'Finding template ID (optional)',
example: 'fnd_t_abc123',
description: 'Severity',
enum: FindingSeverity,
default: FindingSeverity.medium,
required: false,
})
@IsEnum(FindingSeverity)
@IsOptional()
severity?: FindingSeverity;

@ApiProperty({ description: 'Finding template ID', required: false })
@IsString()
@IsNotEmpty()
@IsOptional()
templateId?: string;

@ApiProperty({
description: 'Finding content/message',
example:
'The uploaded evidence does not clearly show the Organization Name or URL.',
maxLength: 5000,
})
@ApiProperty({ description: 'Finding content/message', maxLength: 5000 })
@IsString()
@IsNotEmpty()
@MaxLength(5000)
Expand Down
31 changes: 10 additions & 21 deletions apps/api/src/findings/dto/update-finding.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,33 @@ import {
IsNotEmpty,
MaxLength,
} from 'class-validator';
import { FindingStatus, FindingType } from '@db';
import { FindingSeverity, FindingStatus, FindingType } from '@db';

export class UpdateFindingDto {
@ApiProperty({
description: 'Finding status',
enum: FindingStatus,
required: false,
})
@ApiProperty({ description: 'Finding status', enum: FindingStatus, required: false })
@IsEnum(FindingStatus)
@IsOptional()
status?: FindingStatus;

@ApiProperty({
description: 'Type of finding (SOC 2 or ISO 27001)',
enum: FindingType,
required: false,
})
@ApiProperty({ description: 'Finding type', enum: FindingType, required: false })
@IsEnum(FindingType)
@IsOptional()
type?: FindingType;

@ApiProperty({
description: 'Finding content/message',
example:
'The uploaded evidence does not clearly show the Organization Name or URL.',
maxLength: 5000,
required: false,
})
@ApiProperty({ description: 'Severity', enum: FindingSeverity, required: false })
@IsEnum(FindingSeverity)
@IsOptional()
severity?: FindingSeverity;

@ApiProperty({ description: 'Finding content/message', maxLength: 5000, required: false })
@IsString()
@IsOptional()
@IsNotEmpty({ message: 'Content cannot be empty if provided' })
@MaxLength(5000)
content?: string;

@ApiProperty({
description:
'Auditor note when requesting revision (only for needs_revision status)',
example: 'Please provide clearer screenshots showing the timestamp.',
description: 'Auditor note when requesting revision',
maxLength: 2000,
required: false,
nullable: true,
Expand Down
83 changes: 3 additions & 80 deletions apps/api/src/findings/finding-audit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,6 @@ export interface FindingAuditParams {
export class FindingAuditService {
private readonly logger = new Logger(FindingAuditService.name);

/**
* Log finding creation
*/
async logFindingCreated(
params: FindingAuditParams & {
taskId?: string;
taskTitle?: string;
evidenceSubmissionId?: string;
evidenceSubmissionFormType?: string;
findingScope?: string;
content: string;
type: FindingType;
},
): Promise<void> {
try {
await db.auditLog.create({
data: {
organizationId: params.organizationId,
userId: params.userId,
memberId: params.memberId,
entityType: 'finding',
entityId: params.findingId,
description: 'created this finding',
data: {
action: 'created',
findingId: params.findingId,
taskId: params.taskId,
taskTitle: params.taskTitle,
evidenceSubmissionId: params.evidenceSubmissionId,
evidenceSubmissionFormType: params.evidenceSubmissionFormType,
findingScope: params.findingScope,
content: params.content,
type: params.type,
status: FindingStatus.open,
},
},
});
} catch (error) {
this.logger.error('Failed to log finding creation:', error);
// Don't throw - audit log failures should not block operations
}
}

/**
* Log finding status change
*/
async logFindingStatusChanged(
params: FindingAuditParams & {
previousStatus: FindingStatus;
Expand Down Expand Up @@ -86,9 +40,6 @@ export class FindingAuditService {
}
}

/**
* Log finding content update
*/
async logFindingContentUpdated(
params: FindingAuditParams & {
previousContent: string;
Expand Down Expand Up @@ -117,9 +68,6 @@ export class FindingAuditService {
}
}

/**
* Log finding type change
*/
async logFindingTypeChanged(
params: FindingAuditParams & {
previousType: FindingType;
Expand Down Expand Up @@ -148,18 +96,8 @@ export class FindingAuditService {
}
}

/**
* Log finding deletion
*/
async logFindingDeleted(
params: FindingAuditParams & {
taskId?: string;
taskTitle?: string;
evidenceSubmissionId?: string;
evidenceSubmissionFormType?: string;
findingScope?: string;
content: string;
},
params: FindingAuditParams & { content: string },
): Promise<void> {
try {
await db.auditLog.create({
Expand All @@ -173,11 +111,6 @@ export class FindingAuditService {
data: {
action: 'deleted',
findingId: params.findingId,
taskId: params.taskId,
taskTitle: params.taskTitle,
evidenceSubmissionId: params.evidenceSubmissionId,
evidenceSubmissionFormType: params.evidenceSubmissionFormType,
findingScope: params.findingScope,
content: params.content,
},
},
Expand All @@ -187,9 +120,6 @@ export class FindingAuditService {
}
}

/**
* Get activity logs for a finding
*/
async getFindingActivity(findingId: string, organizationId: string) {
try {
return await db.auditLog.findMany({
Expand All @@ -200,17 +130,10 @@ export class FindingAuditService {
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
image: true,
},
select: { id: true, name: true, email: true, image: true },
},
},
orderBy: {
timestamp: 'desc', // Newest first
},
orderBy: { timestamp: 'desc' },
});
} catch (error) {
this.logger.error('Failed to fetch finding activity:', error);
Expand Down
Loading
Loading