Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -63,6 +63,37 @@ function createEmptyMatrixRow(columns: ReadonlyArray<MatrixColumnDefinition>): M
return Object.fromEntries(columns.map((column) => [column.key, '']));
}

function buildFileAcceptMap(accept?: string): Record<string, string[]> {
if (!accept) {
return { 'application/pdf': [], 'image/*': [], 'text/*': [] };
}

return Object.fromEntries(
accept
.split(',')
.map((ext): [string, string[]] | null => {
const trimmed = ext.trim().toLowerCase();
if (trimmed === '.pdf') return ['application/pdf', []];
if (trimmed === '.doc') return ['application/msword', []];
if (trimmed === '.docx') {
return ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', []];
}
if (trimmed === '.png') return ['image/png', []];
if (trimmed === '.jpg' || trimmed === '.jpeg') return ['image/jpeg', []];
if (trimmed === '.txt') return ['text/plain', []];
if (trimmed === '.svg') return ['image/svg+xml', []];
if (trimmed === '.vsdx') return ['application/vnd.visio', []];
if (trimmed === '.csv') return ['text/csv', []];
if (trimmed === '.xlsx') {
return ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', []];
}
if (trimmed === '.xls') return ['application/vnd.ms-excel', []];
return null;
})
.filter((entry): entry is [string, string[]] => entry !== null),
);
}

export function CompanySubmissionWizard({
organizationId,
formType,
Expand Down Expand Up @@ -251,6 +282,15 @@ export function CompanySubmissionWizard({

const validateRequiredMatrixCells = () => {
for (const field of matrixFields) {
if (field.key === 'matrixRows') {
const matrixFileValue = getValues('matrixFile' as never);
const hasMatrixFile =
typeof matrixFileValue === 'object' &&
matrixFileValue !== null &&
'fileName' in matrixFileValue;
if (hasMatrixFile) continue;
}

const rows = normalizeMatrixRows(getValues(field.key as never));
const rowValues = rows.length > 0 ? rows : [createEmptyMatrixRow(field.columns)];

Expand Down Expand Up @@ -649,37 +689,7 @@ export function CompanySubmissionWizard({
<FileUploader
maxFileCount={1}
maxSize={100 * 1024 * 1024}
accept={
field.accept
? Object.fromEntries(
field.accept
.split(',')
.map((ext): [string, string[]] | null => {
const trimmed = ext.trim().toLowerCase();
if (trimmed === '.pdf') return ['application/pdf', []];
if (trimmed === '.doc') return ['application/msword', []];
if (trimmed === '.docx') {
return [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
[],
];
}
if (trimmed === '.png') return ['image/png', []];
if (trimmed === '.jpg' || trimmed === '.jpeg') {
return ['image/jpeg', []];
}
if (trimmed === '.txt') return ['text/plain', []];
if (trimmed === '.svg') return ['image/svg+xml', []];
if (trimmed === '.vsdx')
return ['application/vnd.visio', []];
return null;
})
.filter(
(entry): entry is [string, string[]] => entry !== null,
),
)
: { 'application/pdf': [], 'image/*': [], 'text/*': [] }
}
accept={buildFileAcceptMap(field.accept)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File upload renders above matrix rows, inverting intended order

Medium Severity

For the rbac-matrix form, the matrixFile field (type file) is categorized into extendedFields and the matrixRows field (type matrix) into matrixFields. Since step 2 renders extendedFields before matrixFields, the file uploader appears above the matrix rows. This contradicts the definition order and the matrixFile description text which reads "instead of entering rows above" — there are no rows above it. The primary input (matrix) ends up below the secondary alternative (file upload), creating a confusing UX.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c6b30e9. Configure here.

disabled={uploadingField === field.key}
onUpload={async (files) => {
const file = files[0];
Expand Down Expand Up @@ -808,34 +818,7 @@ export function CompanySubmissionWizard({
<FileUploader
maxFileCount={1}
maxSize={100 * 1024 * 1024}
accept={
field.accept
? Object.fromEntries(
field.accept
.split(',')
.map((ext): [string, string[]] | null => {
const trimmed = ext.trim().toLowerCase();
if (trimmed === '.pdf') return ['application/pdf', []];
if (trimmed === '.doc') return ['application/msword', []];
if (trimmed === '.docx') {
return [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
[],
];
}
if (trimmed === '.png') return ['image/png', []];
if (trimmed === '.jpg' || trimmed === '.jpeg') {
return ['image/jpeg', []];
}
if (trimmed === '.txt') return ['text/plain', []];
if (trimmed === '.svg') return ['image/svg+xml', []];
if (trimmed === '.vsdx') return ['application/vnd.visio', []];
return null;
})
.filter((entry): entry is [string, string[]] => entry !== null),
)
: { 'application/pdf': [], 'image/*': [], 'text/*': [] }
}
accept={buildFileAcceptMap(field.accept)}
disabled={uploadingField === field.key}
onUpload={async (files) => {
const file = files[0];
Expand Down
13 changes: 11 additions & 2 deletions packages/company/src/evidence-forms/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export const evidenceFormDefinitions: Record<EvidenceFormType, EvidenceFormDefin
label: 'Supporting file',
type: 'file',
required: false,
accept: '.pdf,.doc,.docx,.txt,.png,.jpg,.jpeg',
accept: '.pdf,.doc,.docx,.txt,.png,.jpg,.jpeg,.csv,.xlsx,.xls',
description: 'Optional file attachment to support your report',
},
],
Expand Down Expand Up @@ -258,6 +258,15 @@ Remediation: Status of fixes (e.g. 3 of 5 critical findings remediated; 2 in pro
},
],
},
{
key: 'matrixFile',
label: 'Or upload a spreadsheet',
type: 'file',
required: false,
accept: '.csv,.xlsx,.xls,.pdf',
description:
'Optionally upload a CSV or Excel file with your RBAC matrix instead of entering rows above',
},
],
},
'infrastructure-inventory': {
Expand Down Expand Up @@ -568,7 +577,7 @@ Injects: At 10:30, media contacts the company about the incident.`,
label: 'Supporting evidence',
type: 'file',
required: false,
accept: '.pdf,.doc,.docx,.png,.jpg,.jpeg',
accept: '.pdf,.doc,.docx,.png,.jpg,.jpeg,.csv,.xlsx,.xls',
description:
'Optionally upload additional evidence such as slides, agendas, or sign-in sheets',
},
Expand Down
50 changes: 47 additions & 3 deletions packages/company/src/evidence-forms/submission-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,55 @@ const rbacMatrixRowSchema = z.object({
lastReviewed: requiredTrimmed('Last reviewed'),
});

const rbacMatrixDataSchema = z.object({
submissionDate: required('Submission date'),
matrixRows: z.array(rbacMatrixRowSchema).min(1, 'At least one RBAC entry is required'),
const optionalRbacMatrixRowSchema = z.object({
system: z.string().trim().optional(),
roleName: z.string().trim().optional(),
permissionsScope: z.string().trim().optional(),
approvedBy: z.string().trim().optional(),
lastReviewed: z.string().trim().optional(),
});

const rbacMatrixDataSchema = z
.object({
submissionDate: required('Submission date'),
matrixRows: z.array(optionalRbacMatrixRowSchema).optional(),
matrixFile: evidenceFormFileSchema.optional(),
})
.superRefine((data, ctx) => {
if (data.matrixFile) return;

const rows = data.matrixRows ?? [];
const nonEmptyRows = rows.filter((row) =>
Object.values(row).some((value) => typeof value === 'string' && value.trim().length > 0),
);

if (nonEmptyRows.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'At least one RBAC entry is required',
path: ['matrixRows'],
});
return;
}

rows.forEach((row, rowIndex) => {
const hasAnyValue = Object.values(row).some(
(value) => typeof value === 'string' && value.trim().length > 0,
);
if (!hasAnyValue) return;

const parsedRow = rbacMatrixRowSchema.safeParse(row);
if (parsedRow.success) return;

parsedRow.error.issues.forEach((issue) => {
ctx.addIssue({
...issue,
path: ['matrixRows', rowIndex, ...issue.path],
});
});
});
});

const infrastructureInventoryRowSchema = z.object({
assetId: requiredTrimmed('Asset ID'),
systemType: requiredTrimmed('System type'),
Expand Down
Loading