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 @@ -251,6 +251,14 @@ export function CompanySubmissionWizard({

const validateRequiredMatrixCells = () => {
for (const field of matrixFields) {
// Skip matrix validation when a companion file has been uploaded
// (e.g. RBAC matrix allows uploading a spreadsheet instead of filling rows)
const companionFileKey = `${field.key.replace('Rows', '')}File`;
const companionFile = getValues(companionFileKey as never);
if (companionFile && typeof companionFile === 'object' && 'fileKey' in companionFile) {
continue;
}

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

Expand Down Expand Up @@ -610,6 +618,78 @@ export function CompanySubmissionWizard({
)}
/>
))}
{matrixFields.map((field) => {
const rows = normalizeMatrixRows(watch(field.key as never));
const rowValues = rows.length > 0 ? rows : [createEmptyMatrixRow(field.columns)];
const matrixError = errors[field.key as keyof typeof errors];
return (
<Field key={field.key}>
<FieldLabel>{field.label}</FieldLabel>
{field.description && (
<Text size="sm" variant="muted">
{field.description}
</Text>
)}
<div className="space-y-3">
{rowValues.map((row, rowIndex) => (
<div
key={`${field.key}-${rowIndex}`}
className="rounded-md border border-border p-3"
>
<div className="mb-3 flex items-center justify-between">
<Text size="sm" weight="medium">
Row {rowIndex + 1}
</Text>
<Button
type="button"
variant="ghost"
onClick={() => removeMatrixRow(field, rowIndex)}
disabled={rowValues.length <= 1}
>
Remove row
</Button>
</div>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{field.columns.map((column) => (
<Field key={`${field.key}-${rowIndex}-${column.key}`}>
<FieldLabel htmlFor={`${field.key}-${rowIndex}-${column.key}`}>
{column.label}
</FieldLabel>
{column.description && (
<Text size="sm" variant="muted">
{column.description}
</Text>
)}
<Input
id={`${field.key}-${rowIndex}-${column.key}`}
value={row[column.key] ?? ''}
onChange={(event) =>
updateMatrixCell(
field,
rowIndex,
column.key,
event.target.value,
)
}
placeholder={column.placeholder}
/>
</Field>
))}
</div>
</div>
))}
<Button
type="button"
variant="secondary"
onClick={() => addMatrixRow(field)}
>
{field.addRowLabel ?? 'Add row'}
</Button>
</div>
<FieldError errors={[matrixError as never]} />
</Field>
);
})}
{extendedFields.map((field) => (
<Controller
key={field.key}
Expand Down Expand Up @@ -672,6 +752,15 @@ export function CompanySubmissionWizard({
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(
Expand Down Expand Up @@ -709,78 +798,6 @@ export function CompanySubmissionWizard({
)}
/>
))}
{matrixFields.map((field) => {
const rows = normalizeMatrixRows(watch(field.key as never));
const rowValues = rows.length > 0 ? rows : [createEmptyMatrixRow(field.columns)];
const matrixError = errors[field.key as keyof typeof errors];
return (
<Field key={field.key}>
<FieldLabel>{field.label}</FieldLabel>
{field.description && (
<Text size="sm" variant="muted">
{field.description}
</Text>
)}
<div className="space-y-3">
{rowValues.map((row, rowIndex) => (
<div
key={`${field.key}-${rowIndex}`}
className="rounded-md border border-border p-3"
>
<div className="mb-3 flex items-center justify-between">
<Text size="sm" weight="medium">
Row {rowIndex + 1}
</Text>
<Button
type="button"
variant="ghost"
onClick={() => removeMatrixRow(field, rowIndex)}
disabled={rowValues.length <= 1}
>
Remove row
</Button>
</div>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{field.columns.map((column) => (
<Field key={`${field.key}-${rowIndex}-${column.key}`}>
<FieldLabel htmlFor={`${field.key}-${rowIndex}-${column.key}`}>
{column.label}
</FieldLabel>
{column.description && (
<Text size="sm" variant="muted">
{column.description}
</Text>
)}
<Input
id={`${field.key}-${rowIndex}-${column.key}`}
value={row[column.key] ?? ''}
onChange={(event) =>
updateMatrixCell(
field,
rowIndex,
column.key,
event.target.value,
)
}
placeholder={column.placeholder}
/>
</Field>
))}
</div>
</div>
))}
<Button
type="button"
variant="secondary"
onClick={() => addMatrixRow(field)}
>
{field.addRowLabel ?? 'Add row'}
</Button>
</div>
<FieldError errors={[matrixError as never]} />
</Field>
);
})}
</>
)}
</FieldGroup>
Expand Down Expand Up @@ -830,6 +847,15 @@ export function CompanySubmissionWizard({
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),
Expand Down
15 changes: 12 additions & 3 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 @@ -222,7 +222,7 @@ Remediation: Status of fixes (e.g. 3 of 5 critical findings remediated; 2 in pro
key: 'matrixRows',
label: 'RBAC entries',
type: 'matrix',
required: true,
required: false,
description: 'Audit-minimum role access evidence.',
addRowLabel: 'Add RBAC row',
columns: [
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
29 changes: 25 additions & 4 deletions packages/company/src/evidence-forms/submission-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,31 @@ 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 emptyMatrixRowsToUndefined = (value: unknown): unknown => {
if (!Array.isArray(value)) return value;

const nonEmptyRows = value.filter((row) => {
if (!row || typeof row !== 'object') return true;

return Object.values(row).some((cell) => {
if (typeof cell === 'string') return cell.trim().length > 0;
return Boolean(cell);
});
});

return nonEmptyRows.length > 0 ? nonEmptyRows : undefined;
};

const rbacMatrixDataSchema = z
.object({
submissionDate: required('Submission date'),
matrixRows: z.preprocess(emptyMatrixRowsToUndefined, z.array(rbacMatrixRowSchema).optional()),
matrixFile: evidenceFormFileSchema.optional(),
})
.refine((data) => (data.matrixRows && data.matrixRows.length > 0) || data.matrixFile, {
message: 'Enter at least one RBAC row or upload a spreadsheet',
path: ['matrixRows'],
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Partial matrix rows block submission despite companion file

Medium Severity

The validateRequiredMatrixCells function correctly skips cell-level validation when a companion file is uploaded, but the Zod schema in rbacMatrixDataSchema has no matching logic. The emptyMatrixRowsToUndefined preprocessor keeps any row with at least one non-empty cell, and z.array(rbacMatrixRowSchema).optional() then validates those partial rows, failing on empty required columns. A user who starts filling in matrix rows and then decides to upload a spreadsheet instead gets blocked by trigger() in goToStepThree — the two validation layers are inconsistent.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 59b3ddd. Configure here.


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