fix(core): Use class-validator with XSS check for survey answers (#10490)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions

Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
Iván Ovejero 2024-08-21 16:18:16 +02:00 committed by GitHub
parent d5acde5ce4
commit 547a60642c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 274 additions and 102 deletions

View file

@ -1,4 +1,4 @@
import { ValidationError, validate } from 'class-validator'; import { validate } from 'class-validator';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
@ -9,7 +9,7 @@ import type {
UserUpdatePayload, UserUpdatePayload,
} from '@/requests'; } from '@/requests';
import { BadRequestError } from './errors/response-errors/bad-request.error'; import { BadRequestError } from './errors/response-errors/bad-request.error';
import { NoXss } from '@/validators/no-xss.validator'; import type { PersonalizationSurveyAnswersV4 } from './controllers/survey-answers.dto';
export async function validateEntity( export async function validateEntity(
entity: entity:
@ -19,7 +19,8 @@ export async function validateEntity(
| User | User
| UserUpdatePayload | UserUpdatePayload
| UserRoleChangePayload | UserRoleChangePayload
| UserSettingsUpdatePayload, | UserSettingsUpdatePayload
| PersonalizationSurveyAnswersV4,
): Promise<void> { ): Promise<void> {
const errors = await validate(entity); const errors = await validate(entity);
@ -37,37 +38,3 @@ export async function validateEntity(
} }
export const DEFAULT_EXECUTIONS_GET_ALL_LIMIT = 20; export const DEFAULT_EXECUTIONS_GET_ALL_LIMIT = 20;
class StringWithNoXss {
@NoXss()
value: string;
constructor(value: string) {
this.value = value;
}
}
// Temporary solution until we implement payload validation middleware
export async function validateRecordNoXss(record: Record<string, string>) {
const errors: ValidationError[] = [];
for (const [key, value] of Object.entries(record)) {
const stringWithNoXss = new StringWithNoXss(value);
const validationErrors = await validate(stringWithNoXss);
if (validationErrors.length > 0) {
const error = new ValidationError();
error.property = key;
error.constraints = validationErrors[0].constraints;
errors.push(error);
}
}
if (errors.length > 0) {
const errorMessages = errors
.map((error) => `${error.property}: ${Object.values(error.constraints ?? {}).join(', ')}`)
.join(' | ');
throw new BadRequestError(errorMessages);
}
}

View file

@ -349,10 +349,40 @@ describe('MeController', () => {
); );
}); });
it('should throw BadRequestError on XSS attempt', async () => { test.each([
const req = mock<MeRequest.SurveyAnswers>({ 'automationGoalDevops',
body: { 'test-answer': '<script>alert("XSS")</script>' }, 'companyIndustryExtended',
}); 'otherCompanyIndustryExtended',
'automationGoalSm',
'usageModes',
])('should throw BadRequestError on XSS attempt for an array field %s', async (fieldName) => {
const req = mock<MeRequest.SurveyAnswers>();
req.body = {
version: 'v4',
personalization_survey_n8n_version: '1.0.0',
personalization_survey_submitted_at: new Date().toISOString(),
[fieldName]: ['<script>alert("XSS")</script>'],
};
await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError);
});
test.each([
'automationGoalDevopsOther',
'companySize',
'companyType',
'automationGoalSmOther',
'roleOther',
'reportedSource',
'reportedSourceOther',
])('should throw BadRequestError on XSS attempt for a string field %s', async (fieldName) => {
const req = mock<MeRequest.SurveyAnswers>();
req.body = {
version: 'v4',
personalization_survey_n8n_version: '1.0.0',
personalization_survey_submitted_at: new Date().toISOString(),
[fieldName]: '<script>alert("XSS")</script>',
};
await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError); await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError);
}); });

View file

@ -6,7 +6,7 @@ import { randomBytes } from 'crypto';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import { Delete, Get, Patch, Post, RestController } from '@/decorators'; import { Delete, Get, Patch, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility'; import { PasswordUtility } from '@/services/password.utility';
import { validateEntity, validateRecordNoXss } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { import {
AuthenticatedRequest, AuthenticatedRequest,
@ -25,6 +25,7 @@ import { isApiEnabled } from '@/PublicApi';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { MfaService } from '@/Mfa/mfa.service'; import { MfaService } from '@/Mfa/mfa.service';
import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error'; import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error';
import { PersonalizationSurveyAnswersV4 } from './survey-answers.dto';
export const API_KEY_PREFIX = 'n8n_api_'; export const API_KEY_PREFIX = 'n8n_api_';
@ -195,7 +196,7 @@ export class MeController {
if (!personalizationAnswers) { if (!personalizationAnswers) {
this.logger.debug( this.logger.debug(
'Request to store user personalization survey failed because of empty payload', 'Request to store user personalization survey failed because of undefined payload',
{ {
userId: req.user.id, userId: req.user.id,
}, },
@ -203,12 +204,18 @@ export class MeController {
throw new BadRequestError('Personalization answers are mandatory'); throw new BadRequestError('Personalization answers are mandatory');
} }
await validateRecordNoXss(personalizationAnswers); const validatedAnswers = plainToInstance(
PersonalizationSurveyAnswersV4,
personalizationAnswers,
{ excludeExtraneousValues: true },
);
await validateEntity(validatedAnswers);
await this.userRepository.save( await this.userRepository.save(
{ {
id: req.user.id, id: req.user.id,
personalizationAnswers, personalizationAnswers: validatedAnswers,
}, },
{ transaction: false }, { transaction: false },
); );
@ -217,7 +224,7 @@ export class MeController {
this.eventService.emit('user-submitted-personalization-survey', { this.eventService.emit('user-submitted-personalization-survey', {
userId: req.user.id, userId: req.user.id,
answers: personalizationAnswers, answers: validatedAnswers,
}); });
return { success: true }; return { success: true };

View file

@ -0,0 +1,109 @@
import { NoXss } from '@/validators/no-xss.validator';
import { Expose } from 'class-transformer';
import { IsString, IsArray, IsOptional, IsEmail, IsEnum } from 'class-validator';
import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow';
export class PersonalizationSurveyAnswersV4 implements IPersonalizationSurveyAnswersV4 {
@NoXss()
@Expose()
@IsEnum(['v4'])
version: 'v4';
@NoXss()
@Expose()
@IsString()
personalization_survey_submitted_at: string;
@NoXss()
@Expose()
@IsString()
personalization_survey_n8n_version: string;
@Expose()
@IsOptional()
@IsArray()
@NoXss({ each: true })
@IsString({ each: true })
automationGoalDevops?: string[] | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
automationGoalDevopsOther?: string | null;
@NoXss({ each: true })
@Expose()
@IsOptional()
@IsArray()
@IsString({ each: true })
companyIndustryExtended?: string[] | null;
@NoXss({ each: true })
@Expose()
@IsOptional()
@IsString({ each: true })
otherCompanyIndustryExtended?: string[] | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
companySize?: string | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
companyType?: string | null;
@NoXss({ each: true })
@Expose()
@IsOptional()
@IsArray()
@IsString({ each: true })
automationGoalSm?: string[] | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
automationGoalSmOther?: string | null;
@NoXss({ each: true })
@Expose()
@IsOptional()
@IsArray()
@IsString({ each: true })
usageModes?: string[] | null;
@NoXss()
@Expose()
@IsOptional()
@IsEmail()
email?: string | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
role?: string | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
roleOther?: string | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
reportedSource?: string | null;
@NoXss()
@Expose()
@IsOptional()
@IsString()
reportedSourceOther?: string | null;
}

View file

@ -863,10 +863,10 @@ describe('TelemetryEventRelay', () => {
const event: RelayEventMap['user-submitted-personalization-survey'] = { const event: RelayEventMap['user-submitted-personalization-survey'] = {
userId: 'user123', userId: 'user123',
answers: { answers: {
version: 'v4',
personalization_survey_n8n_version: '1.0.0',
personalization_survey_submitted_at: '2021-10-01T00:00:00.000Z',
companySize: '1-10', companySize: '1-10',
workArea: 'IT',
automationGoal: 'Improve efficiency',
valueExpectation: 'Time savings',
}, },
}; };
@ -874,10 +874,10 @@ describe('TelemetryEventRelay', () => {
expect(telemetry.track).toHaveBeenCalledWith('User responded to personalization questions', { expect(telemetry.track).toHaveBeenCalledWith('User responded to personalization questions', {
user_id: 'user123', user_id: 'user123',
version: 'v4',
personalization_survey_n8n_version: '1.0.0',
personalization_survey_submitted_at: '2021-10-01T00:00:00.000Z',
company_size: '1-10', company_size: '1-10',
work_area: 'IT',
automation_goal: 'Improve efficiency',
value_expectation: 'Time savings',
}); });
}); });

View file

@ -1,4 +1,9 @@
import type { AuthenticationMethod, IRun, IWorkflowBase } from 'n8n-workflow'; import type {
AuthenticationMethod,
IPersonalizationSurveyAnswersV4,
IRun,
IWorkflowBase,
} from 'n8n-workflow';
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces'; import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import type { ProjectRole } from '@/databases/entities/ProjectRelation'; import type { ProjectRole } from '@/databases/entities/ProjectRelation';
import type { GlobalRole } from '@/databases/entities/User'; import type { GlobalRole } from '@/databases/entities/User';
@ -106,7 +111,7 @@ export type RelayEventMap = {
'user-submitted-personalization-survey': { 'user-submitted-personalization-survey': {
userId: string; userId: string;
answers: Record<string, string>; answers: IPersonalizationSurveyAnswersV4;
}; };
'user-deleted': { 'user-deleted': {

View file

@ -945,11 +945,15 @@ export class TelemetryEventRelay extends EventRelay {
userId, userId,
answers, answers,
}: RelayEventMap['user-submitted-personalization-survey']) { }: RelayEventMap['user-submitted-personalization-survey']) {
const camelCaseKeys = Object.keys(answers);
const personalizationSurveyData = { user_id: userId } as Record<string, string | string[]>; const personalizationSurveyData = { user_id: userId } as Record<string, string | string[]>;
camelCaseKeys.forEach((camelCaseKey) => {
personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey]; // ESlint is wrong here
}); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
for (const [camelCaseKey, value] of Object.entries(answers)) {
if (value) {
personalizationSurveyData[snakeCase(camelCaseKey)] = value;
}
}
this.telemetry.track('User responded to personalization questions', personalizationSurveyData); this.telemetry.track('User responded to personalization questions', personalizationSurveyData);
} }

View file

@ -8,6 +8,7 @@ import type {
INodeCredentials, INodeCredentials,
INodeParameters, INodeParameters,
INodeTypeNameVersion, INodeTypeNameVersion,
IPersonalizationSurveyAnswersV4,
IUser, IUser,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -235,7 +236,7 @@ export declare namespace MeRequest {
{}, {},
{ currentPassword: string; newPassword: string; mfaCode?: string } { currentPassword: string; newPassword: string; mfaCode?: string }
>; >;
export type SurveyAnswers = AuthenticatedRequest<{}, {}, Record<string, string> | {}>; export type SurveyAnswers = AuthenticatedRequest<{}, {}, IPersonalizationSurveyAnswersV4>;
} }
export interface UserSetupPayload { export interface UserSetupPayload {

View file

@ -11,6 +11,9 @@ describe('NoXss', () => {
@NoXss() @NoXss()
version = ''; version = '';
@NoXss({ each: true })
categories: string[] = [];
} }
const entity = new Entity(); const entity = new Entity();
@ -71,7 +74,7 @@ describe('NoXss', () => {
} }
}); });
describe('Miscellanous strings', () => { describe('Miscellaneous strings', () => {
const VALID_MISCELLANEOUS_STRINGS = ['CI/CD']; const VALID_MISCELLANEOUS_STRINGS = ['CI/CD'];
for (const str of VALID_MISCELLANEOUS_STRINGS) { for (const str of VALID_MISCELLANEOUS_STRINGS) {
@ -81,4 +84,34 @@ describe('NoXss', () => {
}); });
} }
}); });
describe('Array of strings', () => {
const VALID_STRING_ARRAYS = [
['cloud-infrastructure-orchestration', 'ci-cd', 'reporting'],
['automationGoalDevops', 'cloudComputing', 'containerization'],
];
for (const arr of VALID_STRING_ARRAYS) {
test(`should allow array: ${JSON.stringify(arr)}`, async () => {
entity.categories = arr;
await expect(validate(entity)).resolves.toBeEmptyArray();
});
}
const INVALID_STRING_ARRAYS = [
['valid-string', '<script>alert("xss")</script>', 'another-valid-string'],
['<img src="x" onerror="alert(\'XSS\')">', 'valid-string'],
];
for (const arr of INVALID_STRING_ARRAYS) {
test(`should reject array containing invalid string: ${JSON.stringify(arr)}`, async () => {
entity.categories = arr;
const errors = await validate(entity);
expect(errors).toHaveLength(1);
const [error] = errors;
expect(error.property).toEqual('categories');
expect(error.constraints).toEqual({ NoXss: 'Potentially malicious string' });
});
}
});
}); });

View file

@ -4,7 +4,9 @@ import { registerDecorator, ValidatorConstraint } from 'class-validator';
@ValidatorConstraint({ name: 'NoXss', async: false }) @ValidatorConstraint({ name: 'NoXss', async: false })
class NoXssConstraint implements ValidatorConstraintInterface { class NoXssConstraint implements ValidatorConstraintInterface {
validate(value: string) { validate(value: unknown) {
if (typeof value !== 'string') return false;
return ( return (
value === value ===
xss(value, { xss(value, {

View file

@ -1,7 +1,6 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { IsNull } from '@n8n/typeorm'; import { IsNull } from '@n8n/typeorm';
import validator from 'validator'; import validator from 'validator';
import { randomString } from 'n8n-workflow';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
@ -15,6 +14,7 @@ import { addApiKey, createOwner, createUser, createUserShell } from './shared/db
import type { SuperAgentTest } from './shared/types'; import type { SuperAgentTest } from './shared/types';
import { mockInstance } from '@test/mocking'; import { mockInstance } from '@test/mocking';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow';
const testServer = utils.setupTestServer({ endpointGroups: ['me'] }); const testServer = utils.setupTestServer({ endpointGroups: ['me'] });
@ -145,16 +145,16 @@ describe('Owner shell', () => {
}); });
test('POST /me/survey should succeed with valid inputs', async () => { test('POST /me/survey should succeed with valid inputs', async () => {
const validPayloads = [SURVEY, {}]; const validPayloads = [SURVEY, EMPTY_SURVEY];
for (const validPayload of validPayloads) { for (const validPayload of validPayloads) {
const response = await authOwnerShellAgent.post('/me/survey').send(validPayload); const response = await authOwnerShellAgent.post('/me/survey').send(validPayload);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
expect(response.statusCode).toBe(200);
const storedShellOwner = await Container.get(UserRepository).findOneOrFail({ const storedShellOwner = await Container.get(UserRepository).findOneOrFail({
where: { email: IsNull() }, where: { id: ownerShell.id },
}); });
expect(storedShellOwner.personalizationAnswers).toEqual(validPayload); expect(storedShellOwner.personalizationAnswers).toEqual(validPayload);
@ -300,7 +300,7 @@ describe('Member', () => {
}); });
test('POST /me/survey should succeed with valid inputs', async () => { test('POST /me/survey should succeed with valid inputs', async () => {
const validPayloads = [SURVEY, {}]; const validPayloads = [SURVEY, EMPTY_SURVEY];
for (const validPayload of validPayloads) { for (const validPayload of validPayloads) {
const response = await authMemberAgent.post('/me/survey').send(validPayload); const response = await authMemberAgent.post('/me/survey').send(validPayload);
@ -392,16 +392,31 @@ describe('Owner', () => {
}); });
}); });
const SURVEY = [ const SURVEY: IPersonalizationSurveyAnswersV4 = {
'codingSkill', version: 'v4',
'companyIndustry', personalization_survey_submitted_at: '2024-08-21T13:05:51.709Z',
'companySize', personalization_survey_n8n_version: '1.0.0',
'otherCompanyIndustry', automationGoalDevops: ['test'],
'otherWorkArea', automationGoalDevopsOther: 'test',
'workArea', companyIndustryExtended: ['test'],
].reduce<Record<string, string>>((acc, cur) => { otherCompanyIndustryExtended: ['test'],
return (acc[cur] = randomString(2, 10)), acc; companySize: 'test',
}, {}); companyType: 'test',
automationGoalSm: ['test'],
automationGoalSmOther: 'test',
usageModes: ['test'],
email: 'test@email.com',
role: 'test',
roleOther: 'test',
reportedSource: 'test',
reportedSourceOther: 'test',
};
const EMPTY_SURVEY: IPersonalizationSurveyAnswersV4 = {
version: 'v4',
personalization_survey_submitted_at: '2024-08-21T13:05:51.709Z',
personalization_survey_n8n_version: '1.0.0',
};
const VALID_PATCH_ME_PAYLOADS = [ const VALID_PATCH_ME_PAYLOADS = [
{ {

View file

@ -48,6 +48,7 @@ import type {
NodeConnectionType, NodeConnectionType,
INodeCredentialsDetails, INodeCredentialsDetails,
StartNodeData, StartNodeData,
IPersonalizationSurveyAnswersV4,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { BulkCommand, Undoable } from '@/models/history'; import type { BulkCommand, Undoable } from '@/models/history';
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers'; import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
@ -648,24 +649,6 @@ export type IPersonalizationSurveyAnswersV3 = {
email?: string | null; email?: string | null;
}; };
export type IPersonalizationSurveyAnswersV4 = {
version: 'v4';
automationGoalDevops?: string[] | null;
automationGoalDevopsOther?: string | null;
companyIndustryExtended?: string[] | null;
otherCompanyIndustryExtended?: string[] | null;
companySize?: string | null;
companyType?: string | null;
automationGoalSm?: string[] | null;
automationGoalSmOther?: string | null;
usageModes?: string[] | null;
email?: string | null;
role?: string | null;
roleOther?: string | null;
reportedSource?: string | null;
reportedSourceOther?: string | null;
};
export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV4; export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV4;
export type IPersonalizationSurveyVersions = export type IPersonalizationSurveyVersions =

View file

@ -140,7 +140,6 @@ import {
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
import type { IFormInputs, IPersonalizationLatestVersion } from '@/Interface'; import type { IFormInputs, IPersonalizationLatestVersion } from '@/Interface';
import type { GenericValue } from 'n8n-workflow';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
@ -696,19 +695,16 @@ export default defineComponent({
this.isSaving = true; this.isSaving = true;
try { try {
const survey: Record<string, GenericValue> = { const survey: IPersonalizationLatestVersion = {
...values, ...values,
version: SURVEY_VERSION, version: SURVEY_VERSION,
personalization_survey_submitted_at: new Date().toISOString(), personalization_survey_submitted_at: new Date().toISOString(),
personalization_survey_n8n_version: this.rootStore.versionCli, personalization_survey_n8n_version: this.rootStore.versionCli,
}; };
await this.externalHooks.run( await this.externalHooks.run('personalizationModal.onSubmit', survey);
'personalizationModal.onSubmit',
survey as IPersonalizationLatestVersion,
);
await this.usersStore.submitPersonalizationSurvey(survey as IPersonalizationLatestVersion); await this.usersStore.submitPersonalizationSurvey(survey);
this.posthogStore.setMetadata(survey, 'user'); this.posthogStore.setMetadata(survey, 'user');

View file

@ -65,11 +65,11 @@ import type {
IPersonalizationSurveyAnswersV1, IPersonalizationSurveyAnswersV1,
IPersonalizationSurveyAnswersV2, IPersonalizationSurveyAnswersV2,
IPersonalizationSurveyAnswersV3, IPersonalizationSurveyAnswersV3,
IPersonalizationSurveyAnswersV4,
IPersonalizationSurveyVersions, IPersonalizationSurveyVersions,
IUser, IUser,
ILogInStatus, ILogInStatus,
} from '@/Interface'; } from '@/Interface';
import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow';
/* /*
Utility functions used to handle users in n8n Utility functions used to handle users in n8n

View file

@ -2789,3 +2789,23 @@ export type Functionality = 'regular' | 'configuration-node' | 'pairedItem';
export type Result<T, E> = { ok: true; result: T } | { ok: false; error: E }; export type Result<T, E> = { ok: true; result: T } | { ok: false; error: E };
export type CallbackManager = CallbackManagerLC; export type CallbackManager = CallbackManagerLC;
export type IPersonalizationSurveyAnswersV4 = {
version: 'v4';
personalization_survey_submitted_at: string;
personalization_survey_n8n_version: string;
automationGoalDevops?: string[] | null;
automationGoalDevopsOther?: string | null;
companyIndustryExtended?: string[] | null;
otherCompanyIndustryExtended?: string[] | null;
companySize?: string | null;
companyType?: string | null;
automationGoalSm?: string[] | null;
automationGoalSmOther?: string | null;
usageModes?: string[] | null;
email?: string | null;
role?: string | null;
roleOther?: string | null;
reportedSource?: string | null;
reportedSourceOther?: string | null;
};