From 1dfa3c3d5d3025807fb03a4c9f9bd91843b5a0c0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 9 Jan 2025 22:50:20 +0000 Subject: [PATCH 01/79] create flattenOneTrustAssessment helper --- README.md | 6 +- src/oneTrust/flattenOneTrustAssessment.ts | 130 ++++++++++++++++++++++ src/oneTrust/parseCliPullOtArguments.ts | 2 +- src/oneTrust/types.ts | 31 ++++-- src/oneTrust/writeOneTrustAssessment.ts | 59 +++++++--- 5 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 src/oneTrust/flattenOneTrustAssessment.ts diff --git a/README.md b/README.md index 1b8deb87..b411546b 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,10 @@ - [Authentication](#authentication-34) - [Arguments](#arguments-33) - [Usage](#usage-34) + - [tr-pull-ot](#tr-pull-ot) + - [Authentication](#authentication-35) + - [Arguments](#arguments-34) + - [Usage](#usage-35) - [Prompt Manager](#prompt-manager) - [Proxy usage](#proxy-usage) @@ -599,7 +603,7 @@ To learn how to generate the token, see the [OAuth 2.0 Scopes](https://developer | auth | The OAuth access token with the scopes necessary to access the OneTrust Public APIs. | string | N/A | true | | hostname | The domain of the OneTrust environment from which to pull the resource (e.g. trial.onetrust.com). | string | N/A | true | | file | Path to the file to pull the resource into. Its format must match the fileFormat argument. | string | N/A | true | -| fileFormat | The format of the output file. For now, only json is supported. | string | json | false | +| fileFormat | The format of the output file. | string | csv | false | | resource | The resource to pull from OneTrust. For now, only assessments is supported. | string | assessments | false | | debug | Whether to print detailed logs in case of error. | boolean | false | false | diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts new file mode 100644 index 00000000..ef4f43a0 --- /dev/null +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -0,0 +1,130 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { + // OneTrustApprover, + OneTrustAssessment, + OneTrustAssessmentQuestion, + OneTrustAssessmentSection, + OneTrustGetAssessmentResponse, +} from './types'; + +const flattenObject = (obj: any, prefix = ''): any => + Object.keys(obj).reduce((acc, key) => { + const newKey = prefix ? `${prefix}_${key}` : key; + + const entry = obj[key]; + if (typeof entry === 'object' && entry !== null && !Array.isArray(entry)) { + Object.assign(acc, flattenObject(entry, newKey)); + } else { + acc[newKey] = Array.isArray(entry) + ? entry + .map((e) => { + if (typeof e === 'string') { + return e.replaceAll(',', ''); + } + return e; + }) + .join(', ') + : typeof entry === 'string' + ? entry.replaceAll(',', '') + : entry; + } + return acc; + }, {} as Record); + +const flattenList = (list: any[], prefix: string): any => { + if (list.length === 0) { + return {}; + } + const flattenedList = list.map((obj) => flattenObject(obj, prefix)); + + // get all possible keys from the flattenedList + const allKeys = Array.from( + new Set(flattenedList.flatMap((a) => Object.keys(a))), + ); + + // build a single object where all the keys contain the respective values of flattenedList + return allKeys.reduce((acc, key) => { + const values = flattenedList.map((a) => a[key] ?? '').join(', '); + acc[key] = values; + return acc; + }, {} as Record); +}; + +const flattenOneTrustQuestion = ( + oneTrustQuestion: OneTrustAssessmentQuestion, + prefix: string, +): any => { + const { + question: { options: questionOptions, ...restQuestion }, + // TODO: continue from here + // questionResponses, + // risks, + ...rest + } = oneTrustQuestion; + const newPrefix = `${prefix}_questions_${restQuestion.sequence}`; + + return { + ...flattenObject({ ...restQuestion, ...rest }, newPrefix), + ...flattenList(questionOptions ?? [], `${newPrefix}_options`), + }; +}; + +const flattenOneTrustQuestions = ( + questions: OneTrustAssessmentQuestion[], + prefix: string, +): any => + questions + .map((question) => flattenOneTrustQuestion(question, prefix)) + .reduce((acc, flattenedQuestion) => ({ ...acc, ...flattenedQuestion }), {}); + +const flattenOneTrustSection = (section: OneTrustAssessmentSection): any => { + // TODO: flatten header + const { questions, header, ...rest } = section; + // append the section sequence as a prefix (e.g. sections_${sequence}_header_sectionId) + const prefix = `sections_${section.sequence}`; + + return { + ...flattenObject({ ...header, ...rest }, prefix), + ...flattenOneTrustQuestions(questions, prefix), + }; +}; + +const flattenOneTrustSections = (sections: OneTrustAssessmentSection[]): any => + sections + .map((s) => flattenOneTrustSection(s)) + .reduce((acc, flattenedSection) => ({ ...acc, ...flattenedSection }), {}); + +export const flattenOneTrustAssessment = ( + assessment: OneTrustAssessment & OneTrustGetAssessmentResponse, +): any => { + const { + approvers, + primaryEntityDetails, + respondents, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + respondent, + sections, + ...rest + } = assessment; + + // console.log({ approvers: flattenApprovers(approvers) }); + return { + ...flattenObject(rest), + ...flattenList(approvers, 'approvers'), + ...flattenList(primaryEntityDetails, 'primaryEntityDetails'), + ...flattenList(respondents, 'respondents'), + ...flattenOneTrustSections(sections), + }; +}; + +/** + * + * + * TODO: convert to camelCase -> Title Case + * TODO: section -> header is spread + * TODO: section -> questions -> question is spread + * TODO: section -> questions -> question -> questionOptions are aggregated + * TODO: section -> questions -> question -> questionResponses -> responses are spread + */ diff --git a/src/oneTrust/parseCliPullOtArguments.ts b/src/oneTrust/parseCliPullOtArguments.ts index d1b42032..4d31cbc3 100644 --- a/src/oneTrust/parseCliPullOtArguments.ts +++ b/src/oneTrust/parseCliPullOtArguments.ts @@ -34,7 +34,7 @@ export const parseCliPullOtArguments = (): OneTrustCliArguments => { boolean: ['debug'], default: { resource: OneTrustPullResource.Assessments, - fileFormat: OneTrustFileFormat.Json, + fileFormat: OneTrustFileFormat.Csv, debug: false, }, }, diff --git a/src/oneTrust/types.ts b/src/oneTrust/types.ts index fa0d3c8a..b3f59b5a 100644 --- a/src/oneTrust/types.ts +++ b/src/oneTrust/types.ts @@ -32,7 +32,12 @@ export interface OneTrustAssessment { /** ID of the result. */ resultId: string; /** Name of the result. */ - resultName: string; + resultName: + | 'Approved - Remediation required' + | 'Approved' + | 'Rejected' + | 'Assessment suspended - On Hold' + | null; /** State of the assessment. */ state: 'ARCHIVE' | 'ACTIVE'; /** Status of the assessment. */ @@ -149,7 +154,7 @@ interface OneTrustAssessmentQuestionResponses { justification: string | null; } -interface OneTrustAssessmentQuestion { +export interface OneTrustAssessmentQuestion { /** The question */ question: { /** ID of the question. */ @@ -225,7 +230,7 @@ interface OneTrustAssessmentQuestion { attachmentIds: string[]; } -interface OneTrustAssessmentSection { +export interface OneTrustAssessmentSection { /** The Assessment section header */ header: { /** ID of the section in the assessment. */ @@ -270,7 +275,7 @@ interface OneTrustAssessmentSection { submittedBy: null | { /** The ID of the user who submitted the section */ id: string; - /** THe name of the user who submitted the section */ + /** THe name or email of the user who submitted the section */ name: string; }; /** Date of the submission */ @@ -291,7 +296,7 @@ interface OneTrustAssessmentSection { description: string | null; } -interface OneTrustApprover { +export interface OneTrustApprover { /** ID of the user assigned as an approver. */ id: string; /** ID of the workflow stage */ @@ -316,7 +321,12 @@ interface OneTrustApprover { /** ID of the assessment result. */ resultId: string; /** Name of the assessment result. */ - resultName: string; + resultName: + | 'Approved - Remediation required' + | 'Approved' + | 'Rejected' + | 'Assessment suspended - On Hold' + | null; /** Name key of the assessment result. */ resultNameKey: string; } @@ -343,7 +353,7 @@ export interface OneTrustGetAssessmentResponse { /** Date and time by which the assessment must be completed. */ deadline: string | null; /** Description of the assessment. */ - description: string; + description: string | null; /** Overall inherent risk score without considering the existing controls. */ inherentRiskScore: number | null; /** Date and time at which the assessment was last updated. */ @@ -406,7 +416,12 @@ export interface OneTrustGetAssessmentResponse { /** ID of the result. */ resultId: string | null; /** Name of the result. */ - resultName: string | null; + resultName: + | 'Approved - Remediation required' + | 'Approved' + | 'Rejected' + | 'Assessment suspended - On Hold' + | null; /** Risk level of the assessment. */ riskLevel: 'None' | 'Low' | 'Medium' | 'High' | 'Very High'; /** List of sections in the assessment. */ diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index 3e816317..fee14a29 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -3,6 +3,7 @@ import colors from 'colors'; import { OneTrustFileFormat } from '../enums'; import { OneTrustAssessment, OneTrustGetAssessmentResponse } from './types'; import fs from 'fs'; +import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; /** * Write the assessment to disk at the specified file path. @@ -12,8 +13,7 @@ import fs from 'fs'; */ export const writeOneTrustAssessment = ({ file, - // TODO: https://transcend.height.app/T-41372 - support converting to CSV - // fileFormat, + fileFormat, assessment, assessmentDetails, index, @@ -40,26 +40,53 @@ export const writeOneTrustAssessment = ({ ), ); - // Start with an opening bracket - if (index === 0) { - fs.writeFileSync(file, '[\n'); - } - - // combine the two assessments into a single stringified result + // combine the two assessments into a single enriched result const enrichedAssessment = { ...assessmentDetails, ...assessment, }; - const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); - // Add comma for all items except the last one - const comma = index < total - 1 ? ',' : ''; + // For json format + if (fileFormat === OneTrustFileFormat.Json) { + // start with an opening bracket + if (index === 0) { + fs.writeFileSync(file, '[\n'); + } + + // const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); + + // // Add comma for all items except the last one + // const comma = index < total - 1 ? ',' : ''; + + // // write to file + // fs.appendFileSync(file, stringifiedAssessment + comma); + + // // end with closing bracket + // if (index === total - 1) { + // fs.appendFileSync(file, ']'); + // } + } else if (fileFormat === OneTrustFileFormat.Csv) { + // flatten the json object + // start with an opening bracket + if (index === 0) { + fs.writeFileSync('./oneTrust.json', '[\n'); + } + + const flattened = flattenOneTrustAssessment(enrichedAssessment); + const stringifiedFlattened = JSON.stringify(flattened, null, 2); + + // const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); + + // Add comma for all items except the last one + const comma = index < total - 1 ? ',' : ''; - // write to file - fs.appendFileSync(file, stringifiedAssessment + comma); + // write to file + // fs.appendFileSync(file, stringifiedAssessment + comma); + fs.appendFileSync('./oneTrust.json', stringifiedFlattened + comma); - // End with closing bracket - if (index === total - 1) { - fs.appendFileSync(file, ']'); + // end with closing bracket + if (index === total - 1) { + fs.appendFileSync('./oneTrust.json', ']'); + } } }; From de282db10550c7c87085ab990981350f478b5121 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 16:40:14 +0000 Subject: [PATCH 02/79] flatten questionReponses --- src/oneTrust/flattenOneTrustAssessment.ts | 59 ++++++++++++++++------- src/oneTrust/types.ts | 2 +- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index ef4f43a0..0540b00c 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -5,10 +5,12 @@ import { // OneTrustApprover, OneTrustAssessment, OneTrustAssessmentQuestion, + OneTrustAssessmentQuestionResponses, OneTrustAssessmentSection, OneTrustGetAssessmentResponse, } from './types'; +// TODO: test what happens when a value is null -> it should convert to '' const flattenObject = (obj: any, prefix = ''): any => Object.keys(obj).reduce((acc, key) => { const newKey = prefix ? `${prefix}_${key}` : key; @@ -23,12 +25,12 @@ const flattenObject = (obj: any, prefix = ''): any => if (typeof e === 'string') { return e.replaceAll(',', ''); } - return e; + return e ?? ''; }) - .join(', ') + .join(',') : typeof entry === 'string' ? entry.replaceAll(',', '') - : entry; + : entry ?? ''; } return acc; }, {} as Record); @@ -46,28 +48,47 @@ const flattenList = (list: any[], prefix: string): any => { // build a single object where all the keys contain the respective values of flattenedList return allKeys.reduce((acc, key) => { - const values = flattenedList.map((a) => a[key] ?? '').join(', '); + const values = flattenedList.map((a) => a[key] ?? '').join(','); acc[key] = values; return acc; }, {} as Record); }; +const flattenOneTrustQuestionResponses = ( + questionResponses: OneTrustAssessmentQuestionResponses[], + prefix: string, +): any => { + if (questionResponses.length === 0) { + return {}; + } + + // despite being an array, questionResponses only returns one element + const { responses, ...rest } = questionResponses[0]; + return { + ...flattenList(responses, prefix), + ...flattenObject(rest, prefix), + }; +}; + const flattenOneTrustQuestion = ( oneTrustQuestion: OneTrustAssessmentQuestion, prefix: string, ): any => { const { question: { options: questionOptions, ...restQuestion }, - // TODO: continue from here - // questionResponses, + questionResponses, // risks, ...rest } = oneTrustQuestion; - const newPrefix = `${prefix}_questions_${restQuestion.sequence}`; + const newPrefix = `${prefix}_${restQuestion.sequence}`; return { ...flattenObject({ ...restQuestion, ...rest }, newPrefix), ...flattenList(questionOptions ?? [], `${newPrefix}_options`), + ...flattenOneTrustQuestionResponses( + questionResponses ?? [], + `${newPrefix}_responses`, + ), }; }; @@ -75,26 +96,30 @@ const flattenOneTrustQuestions = ( questions: OneTrustAssessmentQuestion[], prefix: string, ): any => - questions - .map((question) => flattenOneTrustQuestion(question, prefix)) - .reduce((acc, flattenedQuestion) => ({ ...acc, ...flattenedQuestion }), {}); + questions.reduce( + (acc, question) => ({ + ...acc, + ...flattenOneTrustQuestion(question, prefix), + }), + {}, + ); const flattenOneTrustSection = (section: OneTrustAssessmentSection): any => { - // TODO: flatten header const { questions, header, ...rest } = section; - // append the section sequence as a prefix (e.g. sections_${sequence}_header_sectionId) - const prefix = `sections_${section.sequence}`; + // the flattened section key has format like sections_${sequence}_sectionId + const prefix = `sections_${section.sequence}`; return { ...flattenObject({ ...header, ...rest }, prefix), - ...flattenOneTrustQuestions(questions, prefix), + ...flattenOneTrustQuestions(questions, `${prefix}_questions`), }; }; const flattenOneTrustSections = (sections: OneTrustAssessmentSection[]): any => - sections - .map((s) => flattenOneTrustSection(s)) - .reduce((acc, flattenedSection) => ({ ...acc, ...flattenedSection }), {}); + sections.reduce( + (acc, section) => ({ ...acc, ...flattenOneTrustSection(section) }), + {}, + ); export const flattenOneTrustAssessment = ( assessment: OneTrustAssessment & OneTrustGetAssessmentResponse, diff --git a/src/oneTrust/types.ts b/src/oneTrust/types.ts index b3f59b5a..017bcd24 100644 --- a/src/oneTrust/types.ts +++ b/src/oneTrust/types.ts @@ -99,7 +99,7 @@ interface OneTrustAssessmentQuestionRisks { impactLevel?: number; } -interface OneTrustAssessmentQuestionResponses { +export interface OneTrustAssessmentQuestionResponses { /** The responses */ responses: { /** ID of the response. */ From 10c19988327aa525fe97604314ce59090f63d631 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 17:31:21 +0000 Subject: [PATCH 03/79] create helper getOneTrustRisk --- src/cli-pull-ot.ts | 8 +- ...nts.ts => getListOfOneTrustAssessments.ts} | 3 +- ...Assessment.ts => getOneTrustAssessment.ts} | 3 +- src/oneTrust/getOneTrustRisk.ts | 23 ++ src/oneTrust/index.ts | 5 +- src/oneTrust/types.ts | 318 ++++++++++++++++++ 6 files changed, 352 insertions(+), 8 deletions(-) rename src/oneTrust/{getListOfAssessments.ts => getListOfOneTrustAssessments.ts} (89%) rename src/oneTrust/{getAssessment.ts => getOneTrustAssessment.ts} (84%) create mode 100644 src/oneTrust/getOneTrustRisk.ts diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index 1ea67f36..e2e98e2e 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -2,8 +2,8 @@ import { logger } from './logger'; import colors from 'colors'; import { - getListOfAssessments, - getAssessment, + getListOfOneTrustAssessments, + getOneTrustAssessment, writeOneTrustAssessment, parseCliPullOtArguments, createOneTrustGotInstance, @@ -30,7 +30,7 @@ async function main(): Promise { const oneTrust = createOneTrustGotInstance({ hostname, auth }); // fetch the list of all assessments in the OneTrust organization - const assessments = await getListOfAssessments({ oneTrust }); + const assessments = await getListOfOneTrustAssessments({ oneTrust }); // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory await mapSeries(assessments, async (assessment, index) => { @@ -39,7 +39,7 @@ async function main(): Promise { assessments.length }...`, ); - const assessmentDetails = await getAssessment({ + const assessmentDetails = await getOneTrustAssessment({ oneTrust, assessmentId: assessment.assessmentId, }); diff --git a/src/oneTrust/getListOfAssessments.ts b/src/oneTrust/getListOfOneTrustAssessments.ts similarity index 89% rename from src/oneTrust/getListOfAssessments.ts rename to src/oneTrust/getListOfOneTrustAssessments.ts index 926d786c..e840690f 100644 --- a/src/oneTrust/getListOfAssessments.ts +++ b/src/oneTrust/getListOfOneTrustAssessments.ts @@ -7,11 +7,12 @@ import { /** * Fetch a list of all assessments from the OneTrust client. + * ref: https://developer.onetrust.com/onetrust/reference/getallassessmentbasicdetailsusingget * * @param param - the information about the OneTrust client * @returns a list of OneTrustAssessment */ -export const getListOfAssessments = async ({ +export const getListOfOneTrustAssessments = async ({ oneTrust, }: { /** The OneTrust client instance */ diff --git a/src/oneTrust/getAssessment.ts b/src/oneTrust/getOneTrustAssessment.ts similarity index 84% rename from src/oneTrust/getAssessment.ts rename to src/oneTrust/getOneTrustAssessment.ts index 2b81b028..5b7fd090 100644 --- a/src/oneTrust/getAssessment.ts +++ b/src/oneTrust/getOneTrustAssessment.ts @@ -3,11 +3,12 @@ import { OneTrustGetAssessmentResponse } from './types'; /** * Retrieve details about a particular assessment. + * ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget * * @param param - the information about the OneTrust client and assessment to retrieve * @returns details about the assessment */ -export const getAssessment = async ({ +export const getOneTrustAssessment = async ({ oneTrust, assessmentId, }: { diff --git a/src/oneTrust/getOneTrustRisk.ts b/src/oneTrust/getOneTrustRisk.ts new file mode 100644 index 00000000..d6ce3f0c --- /dev/null +++ b/src/oneTrust/getOneTrustRisk.ts @@ -0,0 +1,23 @@ +import { Got } from 'got'; +import { OneTrustGetAssessmentResponse } from './types'; + +/** + * Retrieve details about a particular assessment. + * ref: https://developer.onetrust.com/onetrust/reference/getriskusingget + * + * @param param - the information about the OneTrust client and assessment to retrieve + * @returns details about the assessment + */ +export const getOneTrustRisk = async ({ + oneTrust, + riskId, +}: { + /** The OneTrust client instance */ + oneTrust: Got; + /** The ID of the OneTrust risk to retrieve */ + riskId: string; +}): Promise => { + const { body } = await oneTrust.get(`api/risk/v2/risks/${riskId}`); + + return JSON.parse(body) as OneTrustGetAssessmentResponse; +}; diff --git a/src/oneTrust/index.ts b/src/oneTrust/index.ts index 24486cc7..e98e34fe 100644 --- a/src/oneTrust/index.ts +++ b/src/oneTrust/index.ts @@ -1,5 +1,6 @@ -export * from './getListOfAssessments'; export * from './createOneTrustGotInstance'; -export * from './getAssessment'; +export * from './getOneTrustAssessment'; export * from './writeOneTrustAssessment'; export * from './parseCliPullOtArguments'; +export * from './getListOfOneTrustAssessments'; +export * from './getOneTrustRisk'; diff --git a/src/oneTrust/types.ts b/src/oneTrust/types.ts index 017bcd24..62d6fd53 100644 --- a/src/oneTrust/types.ts +++ b/src/oneTrust/types.ts @@ -448,4 +448,322 @@ export interface OneTrustGetAssessmentResponse { /** Welcome text if any in the assessment. */ welcomeText: string | null; } + +// ref: https://developer.onetrust.com/onetrust/reference/getriskusingget +export interface OneTrustGetRiskResponse { + /** List of associated inventories to the risk. */ + associatedInventories: { + /** ID of the Inventory. */ + inventoryId: string; + /** Name of the Inventory. */ + inventoryName: string; + /** Type of the Inventory. */ + inventoryType: 'ASSETS PROCESSING_ACTIVITIES VENDORS ENTITIES'; + /** ID of the Inventory's Organization. */ + organizationId: string; + /** The source type */ + sourceType: { + /** Indicates whether entity type is eligible for linking/relating with risk or not */ + eligibleForEntityLink: boolean; + /** Indicates whether the entity type is enabled or not. */ + enabled: boolean; + /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ + id: string; + /** Entity Type Name. */ + label: string; + /** Name of the module. */ + moduleName: boolean; + /** Indicates whether this type can be risk type or not in Risk */ + riskType: boolean; + /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ + seeded: boolean; + /** Indicates whether this type can be source type or not in Risk */ + sourceType: boolean; + /** Translation Key of Entity Type ID. */ + translationKey: string; + }; + }[]; + /** The attribute values associated with the risk */ + attributeValues: { + /** List of custom attributes. */ + additionalProp: { + /** Additional information like Source Questions, Approver Ids, Inventory Type. This will be a Map of String Key and Object value. */ + additionalAttributes: object; + /** Attribute option GUID. */ + id: string; + /** Attribute selection value and it is mandatory if the numeric value is not distinct for Numerical Single Select attribute. */ + optionSelectionValue: string; + /** Attribute option value. */ + value: string; + /** Attribute option value key for translation. */ + valueKey: string; + }[]; + }; + /** List of categories. */ + categories: { + /** Identifier for Risk Category. */ + id: string; + /** Risk Category Name. */ + name: string; + /** Risk Category Name Key value for translation. */ + nameKey: string; + }[]; + /** List of Control Identifiers. */ + controlsIdentifier: string[]; + /** Risk created time. */ + createdUTCDateTime: string; + /** Risk Creation Type. */ + creationType: string; + /** Date when the risk is closed. */ + dateClosed: string; + /** Deadline date for the risk. */ + deadline: string; + /** Risk delete type. */ + deleteType: 'SOFT'; + /** Risk description. */ + description: string; + /** ID of the risk. */ + id: string; + /** Residual impact level name. */ + impactLevel: string; + /** Residual impact level ID. */ + impactLevelId: number; + /** The inherent risk level */ + inherentRiskLevel: { + /** Risk Impact Level name. */ + impactLevel: string; + /** Risk Impact level ID. */ + impactLevelId: number; + /** Risk Level Name. */ + level: string; + /** Risk Level ID. */ + levelId: number; + /** Risk Probability Level Name. */ + probabilityLevel: string; + /** Risk Probability Level ID. */ + probabilityLevelId: number; + /** Risk Score. */ + riskScore: number; + }; + /** The risk justification */ + justification: string; + /** Residual level name. */ + level: string; + /** Residual level display name. */ + levelDisplayName: string; + /** Residual level ID. */ + levelId: number; + /** Risk mitigated date. */ + mitigatedDate: string; + /** Risk Mitigation details. */ + mitigation: string; + /** Short Name for a Risk. */ + name: string; + /** Integer risk identifier. */ + number: number; + /** The organization group */ + orgGroup: { + /** ID of an entity. */ + id: string; + /** Name of an entity. */ + name: string; + }; + /** The previous risk state */ + previousState: + | 'IDENTIFIED' + | 'RECOMMENDATION_ADDED' + | 'RECOMMENDATION_SENT' + | 'REMEDIATION_PROPOSED' + | 'EXCEPTION_REQUESTED' + | 'REDUCED' + | 'RETAINED' + | 'ARCHIVED_IN_VERSION'; + /** Residual probability level. */ + probabilityLevel: string; + /** Residual probability level ID. */ + probabilityLevelId: number; + /** Risk Recommendation. */ + recommendation: string; + /** Proposed remediation. */ + remediationProposal: string; + /** Deadline reminder days. */ + reminderDays: number; + /** Risk exception request. */ + requestedException: string; + /** Risk Result. */ + result: + | 'Accepted' + | 'Avoided' + | 'Reduced' + | 'Rejected' + | 'Transferred' + | 'Ignored'; + /** Risk approvers name csv. */ + riskApprovers: string; + /** Risk approvers ID. */ + riskApproversId: string[]; + /** List of risk owners ID. */ + riskOwnersId: string[]; + /** Risk owners name csv. */ + riskOwnersName: string; + /** Risk score. */ + riskScore: number; + /** The risk source type */ + riskSourceType: { + /** Indicates whether entity type is eligible for linking/relating with risk or not */ + eligibleForEntityLink: boolean; + /** Indicates whether the entity type is enabled or not. */ + enabled: boolean; + /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ + id: string; + /** Entity Type Name. */ + label: string; + /** Name of the module. */ + moduleName: boolean; + /** Indicates whether this type can be risk type or not in Risk */ + riskType: boolean; + /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ + seeded: boolean; + /** Indicates whether this type can be source type or not in Risk */ + sourceType: boolean; + /** Translation Key of Entity Type ID. */ + translationKey: string; + }; + /** The risk type */ + riskType: { + /** Indicates whether entity type is eligible for linking/relating with risk or not */ + eligibleForEntityLink: boolean; + /** Indicates whether the entity type is enabled or not. */ + enabled: boolean; + /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ + id: string; + /** Entity Type Name. */ + label: string; + /** Name of the module. */ + moduleName: boolean; + /** Indicates whether this type can be risk type or not in Risk */ + riskType: boolean; + /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ + seeded: boolean; + /** Indicates whether this type can be source type or not in Risk */ + sourceType: boolean; + /** Translation Key of Entity Type ID. */ + translationKey: string; + }; + /** For Auto risk, rule Id reference. */ + ruleRootVersionId: string; + /** The risk source */ + source: { + // eslint-disable-next-line max-len + /** Additional information about the Source Entity. This will be a Map of String Key and Object value. 'inventoryType' key is mandatory to be passed when sourceType is 'Inventory', and it can have one of the following values, 20 - Assets, 30 - Processing Activities, 50 - Vendors, 60 - Entities */ + additionalAttributes: object; + /** Source Entity ID. */ + id: string; + /** Source Entity Name. */ + name: string; + /** The risk source type */ + sourceType: { + /** Indicates whether entity type is eligible for linking/relating with risk or not */ + eligibleForEntityLink: boolean; + /** Indicates whether the entity type is enabled or not. */ + enabled: boolean; + /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ + id: string; + /** Entity Type Name. */ + label: string; + /** Name of the module. */ + moduleName: boolean; + /** Indicates whether this type can be risk type or not in Risk */ + riskType: boolean; + /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ + seeded: boolean; + /** Indicates whether this type can be source type or not in Risk */ + sourceType: boolean; + /** Translation Key of Entity Type ID. */ + translationKey: string; + }; + /** Source Entity Type. */ + type: 'PIA' | 'RA' | 'GRA' | 'INVENTORY' | 'INCIDENT' | 'GENERIC'; + }; + /** The risk stage */ + stage: { + /** ID of an entity. */ + id: string; + /** Name of an entity. */ + name: string; + /** Name Key of the entity for translation. */ + nameKey: string; + }; + /** The risk state */ + state: + | 'IDENTIFIED' + | 'RECOMMENDATION_ADDED' + | 'RECOMMENDATION_SENT' + | 'REMEDIATION_PROPOSED' + | 'EXCEPTION_REQUESTED' + | 'REDUCED' + | 'RETAINED' + | 'ARCHIVED_IN_VERSION'; + /** The target risk level */ + targetRiskLevel: { + /** Risk Impact Level name. */ + impactLevel: string; + /** Risk Impact level ID. */ + impactLevelId: number; + /** Risk Level Name. */ + level: string; + /** Risk Level ID. */ + levelId: number; + /** Risk Probability Level Name. */ + probabilityLevel: string; + /** Risk Probability Level ID. */ + probabilityLevelId: number; + /** Risk Score. */ + riskScore: number; + }; + /** The risk threat */ + threat: { + /** Threat ID. */ + id: string; + /** Threat Identifier. */ + identifier: string; + /** Threat Name. */ + name: string; + }; + /** Risk Treatment. */ + treatment: string; + /** Risk Treatment status. */ + treatmentStatus: + | 'InProgress' + | 'UnderReview' + | 'ExceptionRequested' + | 'Approved' + | 'ExceptionGranted'; + /** Risk Type. */ + type: + | 'ASSESSMENTS' + | 'ASSETS' + | 'PROCESSING_ACTIVITIES' + | 'VENDORS' + | 'ENTITIES' + | 'INCIDENTS'; + /** ID of an assessment. */ + typeRefIds: string[]; + /** List of vulnerabilities */ + vulnerabilities: { + /** Vulnerability ID. */ + id: string; + /** Vulnerability Identifier. */ + identifier: string; + /** Vulnerability Name. */ + name: string; + }[]; + /** The risk workflow */ + workflow: { + /** ID of an entity. */ + id: string; + /** Name of an entity. */ + name: string; + }; +} /* eslint-enable max-lines */ From f68e17c053cdf985fcf107c53a97de0a40bd2556 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 17:31:54 +0000 Subject: [PATCH 04/79] update return type of getOneTrustRIsk --- src/oneTrust/getOneTrustRisk.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/oneTrust/getOneTrustRisk.ts b/src/oneTrust/getOneTrustRisk.ts index d6ce3f0c..7768a898 100644 --- a/src/oneTrust/getOneTrustRisk.ts +++ b/src/oneTrust/getOneTrustRisk.ts @@ -1,5 +1,5 @@ import { Got } from 'got'; -import { OneTrustGetAssessmentResponse } from './types'; +import { OneTrustGetRiskResponse } from './types'; /** * Retrieve details about a particular assessment. @@ -16,8 +16,8 @@ export const getOneTrustRisk = async ({ oneTrust: Got; /** The ID of the OneTrust risk to retrieve */ riskId: string; -}): Promise => { +}): Promise => { const { body } = await oneTrust.get(`api/risk/v2/risks/${riskId}`); - return JSON.parse(body) as OneTrustGetAssessmentResponse; + return JSON.parse(body) as OneTrustGetRiskResponse; }; From 93333258ed473f43953f556ec11ca6495b1017f4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 19:49:02 +0000 Subject: [PATCH 05/79] convert OneTrust interfaces to codecs --- src/cli-pull-ot.ts | 26 +- src/oneTrust/codecs.ts | 848 +++++++++++++++++++ src/oneTrust/flattenOneTrustAssessment.ts | 43 +- src/oneTrust/getListOfOneTrustAssessments.ts | 17 +- src/oneTrust/getOneTrustAssessment.ts | 7 +- src/oneTrust/getOneTrustRisk.ts | 13 +- src/oneTrust/types.ts | 769 ----------------- src/oneTrust/writeOneTrustAssessment.ts | 51 +- 8 files changed, 971 insertions(+), 803 deletions(-) create mode 100644 src/oneTrust/codecs.ts delete mode 100644 src/oneTrust/types.ts diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index e2e98e2e..5981ef16 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -7,9 +7,11 @@ import { writeOneTrustAssessment, parseCliPullOtArguments, createOneTrustGotInstance, + getOneTrustRisk, } from './oneTrust'; import { OneTrustPullResource } from './enums'; -import { mapSeries } from 'bluebird'; +import { mapSeries, map } from 'bluebird'; +import uniq from 'lodash/uniq'; /** * Pull configuration from OneTrust down locally to disk @@ -44,9 +46,31 @@ async function main(): Promise { assessmentId: assessment.assessmentId, }); + // enrich assessments with risk information + const riskIds = uniq( + assessmentDetails.sections.flatMap((s) => + s.questions.flatMap((q) => + (q.risks ?? []).flatMap((r) => r.riskId), + ), + ), + ); + logger.info( + `Fetching details about ${riskIds} risks for assessment ${ + index + 1 + } of ${assessments.length}...`, + ); + const riskDetails = await map( + riskIds, + (riskId) => getOneTrustRisk({ oneTrust, riskId }), + { + concurrency: 5, + }, + ); + writeOneTrustAssessment({ assessment, assessmentDetails, + riskDetails, index, total: assessments.length, file, diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts new file mode 100644 index 00000000..191b51c9 --- /dev/null +++ b/src/oneTrust/codecs.ts @@ -0,0 +1,848 @@ +/* eslint-disable max-lines */ +import * as t from 'io-ts'; + +// TODO: move all to privacy-types + +export const OneTrustAssessmentCodec = t.type({ + /** ID of the assessment. */ + assessmentId: t.string, + /** Date that the assessment was created. */ + createDt: t.string, + /** Overall risk score without considering existing controls. */ + inherentRiskScore: t.number, + /** Date and time that the assessment was last updated. */ + lastUpdated: t.string, + /** Name of the assessment. */ + name: t.string, + /** Number assigned to the assessment. */ + number: t.number, + /** Number of risks that are open on the assessment. */ + openRiskCount: t.number, + /** Name of the organization group assigned to the assessment. */ + orgGroupName: t.string, + /** Details about the inventory record which is the primary record of the assessment. */ + primaryInventoryDetails: t.type({ + /** GUID of the inventory record. */ + primaryInventoryId: t.string, + /** Name of the inventory record. */ + primaryInventoryName: t.string, + /** Integer ID of the inventory record. */ + primaryInventoryNumber: t.number, + }), + /** Overall risk score after considering existing controls. */ + residualRiskScore: t.number, + /** Result of the assessment. NOTE: This field will be deprecated soon. Please reference the 'resultName' field instead. */ + result: t.union([ + t.literal('Approved'), + t.literal('AutoClosed'), + t.literal('Rejected'), + ]), + /** ID of the result. */ + resultId: t.string, + /** Name of the result. */ + resultName: t.union([ + t.literal('Approved - Remediation required'), + t.literal('Approved'), + t.literal('Rejected'), + t.literal('Assessment suspended - On Hold'), + t.null, + ]), + /** State of the assessment. */ + state: t.union([t.literal('ARCHIVE'), t.literal('ACTIVE')]), + /** Status of the assessment. */ + status: t.union([ + t.literal('Not Started'), + t.literal('In Progress'), + t.literal('Under Review'), + t.literal('Completed'), + ]), + /** Name of the tag attached to the assessment. */ + tags: t.array(t.string), + /** The desired risk score. */ + targetRiskScore: t.number, + /** ID used to launch an assessment using a specific version of a template. */ + templateId: t.string, + /** Name of the template that is being used on the assessment. */ + templateName: t.string, + /** ID used to launch an assessment using the latest published version of a template. */ + templateRootVersionId: t.string, +}); + +/** Type override */ +export type OneTrustAssessmentCodec = t.TypeOf; + +// ref: https://developer.onetrust.com/onetrust/reference/getallassessmentbasicdetailsusingget +export const OneTrustGetListOfAssessmentsResponseCodec = t.partial({ + /** The list of assessments in the current page. */ + content: t.array(OneTrustAssessmentCodec), + /** Details about the pages being fetched */ + page: t.type({ + /** Page number of the results list (0…N). */ + number: t.number, + /** Number of records per page (0…N). */ + size: t.number, + /** Total number of elements. */ + totalElements: t.number, + /** Total number of pages. */ + totalPages: t.number, + }), +}); + +/** Type override */ +export type OneTrustGetListOfAssessmentsResponseCodec = t.TypeOf< + typeof OneTrustGetListOfAssessmentsResponseCodec +>; + +const OneTrustAssessmentQuestionOptionCodec = t.type({ + /** ID of the option. */ + id: t.string, + /** Name of the option. */ + option: t.string, + /** Order in which the option appears. */ + sequence: t.number, + /** Attribute for which the option is available. */ + attributes: t.union([t.string, t.null]), + /** Type of option. */ + optionType: t.union([ + t.literal('NOT_SURE'), + t.literal('NOT_APPLICABLE'), + t.literal('OTHERS'), + t.literal('DEFAULT'), + ]), +}); + +export const OneTrustAssessmentQuestionRiskCodec = t.type({ + /** ID of the question for which the risk was flagged. */ + questionId: t.string, + /** ID of the flagged risk. */ + riskId: t.string, + /** Level of risk flagged on the question. */ + level: t.number, + /** Score of risk flagged on the question. */ + score: t.number, + /** Probability of risk flagged on the question. */ + probability: t.union([t.number, t.undefined]), + /** Impact Level of risk flagged on the question. */ + impactLevel: t.union([t.number, t.undefined]), +}); + +/** Type override */ +export type OneTrustAssessmentQuestionRiskCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionRiskCodec +>; + +export const OneTrustAssessmentQuestionResponsesCodec = t.type({ + /** The responses */ + responses: t.array( + t.type({ + /** ID of the response. */ + responseId: t.string, + /** Content of the response. */ + response: t.string, + /** Type of response. */ + type: t.union([ + t.literal('NOT_SURE'), + t.literal('JUSTIFICATION'), + t.literal('NOT_APPLICABLE'), + t.literal('DEFAULT'), + t.literal('OTHERS'), + ]), + /** Source from which the assessment is launched. */ + responseSourceType: t.union([ + t.literal('LAUNCH_FROM_INVENTORY'), + t.literal('FORCE_CREATED_SOURCE'), + t.null, + ]), + /** Error associated with the response. */ + errorCode: t.union([ + t.literal('ATTRIBUTE_DISABLED'), + t.literal('ATTRIBUTE_OPTION_DISABLED'), + t.literal('INVENTORY_NOT_EXISTS'), + t.literal('RELATED_INVENTORY_ATTRIBUTE_DISABLED'), + t.literal('DATA_ELEMENT_NOT_EXISTS'), + t.literal('DUPLICATE_INVENTORY'), + t.null, + ]), + /** This parameter is only applicable for inventory type responses (Example- ASSETS). */ + responseMap: t.object, + /** Indicates whether the response is valid. */ + valid: t.boolean, + /** The data subject */ + dataSubject: t.type({ + /** The ID of the data subject */ + id: t.string, + /** The ID of the data subject */ + name: t.string, + }), + /** The data category */ + dataCategory: t.type({ + /** The ID of the data category */ + id: t.string, + /** The name of the data category */ + name: t.string, + }), + /** The data element */ + dataElement: t.type({ + /** The ID of the data element */ + id: t.string, + /** The ID of the data element */ + name: t.string, + }), + }), + ), + /** Justification comments for the given response. */ + justification: t.union([t.string, t.null]), +}); + +/** Type override */ +export type OneTrustAssessmentQuestionResponsesCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionResponsesCodec +>; + +export const OneTrustAssessmentQuestionCodec = t.type({ + /** The question */ + question: t.type({ + /** ID of the question. */ + id: t.string, + /** ID of the root version of the question. */ + rootVersionId: t.string, + /** Order in which the question appears in the assessment. */ + sequence: t.number, + /** Type of question in the assessment. */ + questionType: t.union([ + t.literal('TEXTBOX'), + t.literal('MULTICHOICE'), + t.literal('YESNO'), + t.literal('DATE'), + t.literal('STATEMENT'), + t.literal('INVENTORY'), + t.literal('ATTRIBUTE'), + t.literal('PERSONAL_DATA'), + ]), + /** Indicates whether a response to the question is required. */ + required: t.boolean, + /** Data element attributes that are directly updated by the question. */ + attributes: t.string, + /** Short, descriptive name for the question. */ + friendlyName: t.union([t.string, t.null]), + /** Description of the question. */ + description: t.union([t.string, t.null]), + /** Tooltip text within a hint for the question. */ + hint: t.string, + /** ID of the parent question. */ + parentQuestionId: t.string, + /** Indicates whether the response to the question is prepopulated. */ + prePopulateResponse: t.boolean, + /** Indicates whether the assessment is linked to inventory records. */ + linkAssessmentToInventory: t.boolean, + /** The question options */ + options: t.union([t.array(OneTrustAssessmentQuestionOptionCodec), t.null]), + /** Indicates whether the question is valid. */ + valid: t.boolean, + /** Type of question in the assessment. */ + type: t.union([ + t.literal('TEXTBOX'), + t.literal('MULTICHOICE'), + t.literal('YESNO'), + t.literal('DATE'), + t.literal('STATEMENT'), + t.literal('INVENTORY'), + t.literal('ATTRIBUTE'), + t.literal('PERSONAL_DATA'), + ]), + /** Whether the response can be multi select */ + allowMultiSelect: t.boolean, + /** The text of a question. */ + content: t.string, + /** Indicates whether justification comments are required for the question. */ + requireJustification: t.boolean, + }), + /** Indicates whether the question is hidden on the assessment. */ + hidden: t.boolean, + /** Reason for locking the question in the assessment. */ + lockReason: t.union([ + t.literal('LAUNCH_FROM_INVENTORY'), + t.literal('FORCE_CREATION_LOCK'), + t.null, + ]), + /** The copy errors */ + copyErrors: t.union([t.string, t.null]), + /** Indicates whether navigation rules are enabled for the question. */ + hasNavigationRules: t.boolean, + /** The responses to this question */ + questionResponses: t.array(OneTrustAssessmentQuestionResponsesCodec), + /** The risks associated with this question */ + risks: t.union([t.array(OneTrustAssessmentQuestionRiskCodec), t.null]), + /** List of IDs associated with the question root requests. */ + rootRequestInformationIds: t.array(t.string), + /** Number of attachments added to the question. */ + totalAttachments: t.number, + /** IDs of the attachment(s) added to the question. */ + attachmentIds: t.array(t.string), +}); + +/** Type override */ +export type OneTrustAssessmentQuestionCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionCodec +>; + +export const OneTrustAssessmentSectionCodec = t.type({ + /** The Assessment section header */ + header: t.type({ + /** ID of the section in the assessment. */ + sectionId: t.string, + /** Name of the section. */ + name: t.string, + /** Description of the section header. */ + description: t.union([t.string, t.null]), + /** Sequence of the section within the form */ + sequence: t.number, + /** Indicates whether the section is hidden in the assessment. */ + hidden: t.boolean, + /** IDs of invalid questions in the section. */ + invalidQuestionIds: t.array(t.string), + /** IDs of required but unanswered questions in the section. */ + requiredUnansweredQuestionIds: t.array(t.string), + /** IDs of required questions in the section. */ + requiredQuestionIds: t.array(t.string), + /** IDs of unanswered questions in the section. */ + unansweredQuestionIds: t.array(t.string), + /** IDs of effectiveness questions in the section. */ + effectivenessQuestionIds: t.array(t.string), + /** Number of invalid questions in the section. */ + invalidQuestionCount: t.number, + /** The risk statistics */ + riskStatistics: t.union([ + t.type({ + /** Maximum level of risk in the section. */ + maxRiskLevel: t.number, + /** Number of risks in the section. */ + riskCount: t.number, + /** ID of the section in the assessment. */ + sectionId: t.string, + }), + t.null, + ]), + /** Whether the section was submitted */ + submitted: t.boolean, + }), + /** The questions within the section */ + questions: t.array(OneTrustAssessmentQuestionCodec), + /** Indicates whether navigation rules are enabled for the question. */ + hasNavigationRules: t.boolean, + /** Who submitted the section */ + submittedBy: t.union([ + t.type({ + /** The ID of the user who submitted the section */ + id: t.string, + /** THe name or email of the user who submitted the section */ + name: t.string, + }), + t.null, + ]), + /** Date of the submission */ + submittedDt: t.union([t.string, t.null]), + /** Name of the section. */ + name: t.string, + /** Indicates whether navigation rules are enabled for the question. */ + hidden: t.boolean, + /** Indicates whether the section is valid. */ + valid: t.boolean, + /** ID of the section in an assessment. */ + sectionId: t.string, + /** Sequence of the section within the form */ + sequence: t.number, + /** Whether the section was submitted */ + submitted: t.boolean, + /** Descriptions of the section. */ + description: t.union([t.string, t.null]), +}); + +/** Type override */ +export type OneTrustAssessmentSectionCodec = t.TypeOf< + typeof OneTrustAssessmentSectionCodec +>; + +export const OneTrustApproverCodec = t.type({ + /** ID of the user assigned as an approver. */ + id: t.string, + /** ID of the workflow stage */ + workflowStageId: t.string, + /** Name of the user assigned as an approver. */ + name: t.string, + /** More details about the approver */ + approver: t.type({ + /** ID of the user assigned as an approver. */ + id: t.string, + /** Full name of the user assigned as an approver. */ + fullName: t.string, + /** Email of the user assigned as an approver. */ + email: t.union([t.string, t.null]), + /** Whether the user assigned as an approver was deleted. */ + deleted: t.boolean, + }), + /** Assessment approval status. */ + approvalState: t.union([ + t.literal('OPEN'), + t.literal('APPROVED'), + t.literal('REJECTED'), + ]), + /** Date and time at which the assessment was approved. */ + approvedOn: t.string, + /** ID of the assessment result. */ + resultId: t.string, + /** Name of the assessment result. */ + resultName: t.union([ + t.literal('Approved - Remediation required'), + t.literal('Approved'), + t.literal('Rejected'), + t.literal('Assessment suspended - On Hold'), + t.null, + ]), + /** Name key of the assessment result. */ + resultNameKey: t.string, +}); + +/** Type override */ +export type OneTrustApproverCodec = t.TypeOf; + +// ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget + +export const OneTrustGetAssessmentResponseCodec = t.type({ + /** List of users assigned as approvers of the assessment. */ + approvers: t.array(OneTrustApproverCodec), + /** ID of an assessment. */ + assessmentId: t.string, + /** Number assigned to an assessment. */ + assessmentNumber: t.number, + /** Date and time at which the assessment was completed. */ + completedOn: t.union([t.string, t.null]), + /** Creator of the Assessment */ + createdBy: t.type({ + /** The ID of the creator */ + id: t.string, + /** The name of the creator */ + name: t.string, + }), + /** Date and time at which the assessment was created. */ + createdDT: t.string, + /** Date and time by which the assessment must be completed. */ + deadline: t.union([t.string, t.null]), + /** Description of the assessment. */ + description: t.union([t.string, t.null]), + /** Overall inherent risk score without considering the existing controls. */ + inherentRiskScore: t.union([t.number, t.null]), + /** Date and time at which the assessment was last updated. */ + lastUpdated: t.string, + /** Number of risks captured on the assessment with a low risk level. */ + lowRisk: t.number, + /** Number of risks captured on the assessment with a medium risk level. */ + mediumRisk: t.number, + /** Number of risks captured on the assessment with a high risk level. */ + highRisk: t.number, + /** Name of the assessment. */ + name: t.string, + /** Number of open risks that have not been addressed. */ + openRiskCount: t.number, + /** The organization group */ + orgGroup: t.type({ + /** The ID of the organization group */ + id: t.string, + /** The name of the organization group */ + name: t.string, + }), + /** The primary record */ + primaryEntityDetails: t.array( + t.type({ + /** Unique ID for the primary record. */ + id: t.string, + /** Name of the primary record. */ + name: t.string, + /** The number associated with the primary record. */ + number: t.number, + /** Name and number of the primary record. */ + displayName: t.string, + }), + ), + /** Type of inventory record designated as the primary record. */ + primaryRecordType: t.union([ + t.literal('ASSETS'), + t.literal('PROCESSING_ACTIVITY'), + t.literal('VENDORS'), + t.literal('ENTITIES'), + t.literal('ASSESS_CONTROL'), + t.literal('ENGAGEMENT'), + t.null, + ]), + /** Overall risk score after considering existing controls. */ + residualRiskScore: t.union([t.number, t.null]), + /** The respondent */ + respondent: t.type({ + /** The ID of the respondent */ + id: t.string, + /** The name or email of the respondent */ + name: t.string, + }), + /** The respondents */ + respondents: t.array( + t.type({ + /** The ID of the respondent */ + id: t.string, + /** The name or email of the respondent */ + name: t.string, + }), + ), + /** Result of the assessment. */ + result: t.union([t.string, t.null]), + /** ID of the result. */ + resultId: t.union([t.string, t.null]), + /** Name of the result. */ + resultName: t.union([ + t.literal('Approved - Remediation required'), + t.literal('Approved'), + t.literal('Rejected'), + t.literal('Assessment suspended - On Hold'), + t.null, + ]), + /** Risk level of the assessment. */ + riskLevel: t.union([ + t.literal('None'), + t.literal('Low'), + t.literal('Medium'), + t.literal('High'), + t.literal('Very High'), + ]), + /** List of sections in the assessment. */ + sections: t.array(OneTrustAssessmentSectionCodec), + status: t.union([ + t.literal('Not Started'), + t.literal('In Progress'), + t.literal('Under Review'), + t.literal('Completed'), + t.null, + ]), + /** Date and time at which the assessment was submitted. */ + submittedOn: t.union([t.string, t.null]), + /** List of tags associated with the assessment. */ + tags: t.array(t.string), + /** The desired target risk score. */ + targetRiskScore: t.union([t.number, t.null]), + /** The template */ + template: t.type({ + /** The ID of the template */ + id: t.string, + /** The name of the template */ + name: t.string, + }), + /** Number of total risks on the assessment. */ + totalRiskCount: t.number, + /** Number of very high risks on the assessment. */ + veryHighRisk: t.number, + /** Welcome text if any in the assessment. */ + welcomeText: t.union([t.string, t.null]), +}); + +/** Type override */ +export type OneTrustGetAssessmentResponseCodec = t.TypeOf< + typeof OneTrustGetAssessmentResponseCodec +>; + +const EntityTypeCodec = t.type({ + /** Indicates whether entity type is eligible for linking/relating with risk or not */ + eligibleForEntityLink: t.boolean, + /** Indicates whether the entity type is enabled or not. */ + enabled: t.boolean, + /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ + id: t.string, + /** Entity Type Name. */ + label: t.string, + /** Name of the module. */ + moduleName: t.boolean, + /** Indicates whether this type can be risk type or not in Risk */ + riskType: t.boolean, + /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ + seeded: t.boolean, + /** Indicates whether this type can be source type or not in Risk */ + sourceType: t.boolean, + /** Translation Key of Entity Type ID. */ + translationKey: t.string, +}); + +const RiskLevelCodec = t.type({ + /** Risk Impact Level name. */ + impactLevel: t.string, + /** Risk Impact level ID. */ + impactLevelId: t.number, + /** Risk Level Name. */ + level: t.string, + /** Risk Level ID. */ + levelId: t.number, + /** Risk Probability Level Name. */ + probabilityLevel: t.string, + /** Risk Probability Level ID. */ + probabilityLevelId: t.number, + /** Risk Score. */ + riskScore: t.number, +}); + +// ref: https://developer.onetrust.com/onetrust/reference/getriskusingget +export const OneTrustGetRiskResponseCodec = t.type({ + /** List of associated inventories to the risk. */ + associatedInventories: t.array( + t.type({ + /** ID of the Inventory. */ + inventoryId: t.string, + /** Name of the Inventory. */ + inventoryName: t.string, + /** Type of the Inventory. */ + inventoryType: t.literal('ASSETS PROCESSING_ACTIVITIES VENDORS ENTITIES'), + /** ID of the Inventory's Organization. */ + organizationId: t.string, + /** The source type */ + sourceType: EntityTypeCodec, + }), + ), + /** The attribute values associated with the risk */ + attributeValues: t.type({ + /** List of custom attributes. */ + additionalProp: t.array( + t.type({ + /** Additional information like Source Questions, Approver Ids, Inventory Type. This will be a Map of String Key and Object value. */ + additionalAttributes: t.object, + /** Attribute option GUID. */ + id: t.string, + /** Attribute selection value and it is mandatory if the numeric value is not distinct for Numerical Single Select attribute. */ + optionSelectionValue: t.string, + /** Attribute option value. */ + value: t.string, + /** Attribute option value key for translation. */ + valueKey: t.string, + }), + ), + }), + /** List of categories. */ + categories: t.array( + t.type({ + /** Identifier for Risk Category. */ + id: t.string, + /** Risk Category Name. */ + name: t.string, + /** Risk Category Name Key value for translation. */ + nameKey: t.string, + }), + ), + /** List of Control Identifiers. */ + controlsIdentifier: t.array(t.string), + /** Risk created time. */ + createdUTCDateTime: t.string, + /** Risk Creation Type. */ + creationType: t.string, + /** Date when the risk is closed. */ + dateClosed: t.string, + /** Deadline date for the risk. */ + deadline: t.string, + /** Risk delete type. */ + deleteType: t.literal('SOFT'), + /** Risk description. */ + description: t.string, + /** ID of the risk. */ + id: t.string, + /** Residual impact level name. */ + impactLevel: t.string, + /** Residual impact level ID. */ + impactLevelId: t.number, + /** The inherent risk level */ + inherentRiskLevel: RiskLevelCodec, + /** The risk justification */ + justification: t.string, + /** Residual level display name. */ + levelDisplayName: t.string, + /** Residual level ID. */ + levelId: t.number, + /** Risk mitigated date. */ + mitigatedDate: t.string, + /** Risk Mitigation details. */ + mitigation: t.string, + /** Short Name for a Risk. */ + name: t.string, + /** Integer risk identifier. */ + number: t.number, + /** The organization group */ + orgGroup: t.type({ + /** ID of an entity. */ + id: t.string, + /** Name of an entity. */ + name: t.string, + }), + /** The previous risk state */ + previousState: t.union([ + t.literal('IDENTIFIED'), + t.literal('RECOMMENDATION_ADDED'), + t.literal('RECOMMENDATION_SENT'), + t.literal('REMEDIATION_PROPOSED'), + t.literal('EXCEPTION_REQUESTED'), + t.literal('REDUCED'), + t.literal('RETAINED'), + t.literal('ARCHIVED_IN_VERSION'), + ]), + /** Residual probability level. */ + probabilityLevel: t.string, + /** Residual probability level ID. */ + probabilityLevelId: t.number, + /** Risk Recommendation. */ + recommendation: t.string, + /** Proposed remediation. */ + remediationProposal: t.string, + /** Deadline reminder days. */ + reminderDays: t.number, + /** Risk exception request. */ + requestedException: t.string, + /** Risk Result. */ + result: t.union([ + t.literal('Accepted'), + t.literal('Avoided'), + t.literal('Reduced'), + t.literal('Rejected'), + t.literal('Transferred'), + t.literal('Ignored'), + ]), + /** Risk approvers name csv. */ + riskApprovers: t.string, + /** Risk approvers ID. */ + riskApproversId: t.array(t.string), + /** List of risk owners ID. */ + riskOwnersId: t.array(t.string), + /** Risk owners name csv. */ + riskOwnersName: t.string, + /** Risk score. */ + riskScore: t.number, + /** The risk source type */ + riskSourceType: EntityTypeCodec, + /** The risk type */ + riskType: EntityTypeCodec, + /** For Auto risk, rule Id reference. */ + ruleRootVersionId: t.string, + /** The risk source */ + source: t.type({ + /** Additional information about the Source Entity */ + additionalAttributes: t.object, + /** Source Entity ID. */ + id: t.string, + /** Source Entity Name. */ + name: t.string, + /** The risk source type */ + sourceType: EntityTypeCodec, + /** Source Entity Type. */ + type: t.union([ + t.literal('PIA'), + t.literal('RA'), + t.literal('GRA'), + t.literal('INVENTORY'), + t.literal('INCIDENT'), + t.literal('GENERIC'), + ]), + }), + /** The risk stage */ + stage: t.type({ + /** ID of an entity. */ + id: t.string, + /** Name of an entity. */ + name: t.string, + /** Name Key of the entity for translation. */ + nameKey: t.string, + }), + /** The risk state */ + state: t.union([ + t.literal('IDENTIFIED'), + t.literal('RECOMMENDATION_ADDED'), + t.literal('RECOMMENDATION_SENT'), + t.literal('REMEDIATION_PROPOSED'), + t.literal('EXCEPTION_REQUESTED'), + t.literal('REDUCED'), + t.literal('RETAINED'), + t.literal('ARCHIVED_IN_VERSION'), + ]), + /** The target risk level */ + targetRiskLevel: RiskLevelCodec, + /** The risk threat */ + threat: t.type({ + /** Threat ID. */ + id: t.string, + /** Threat Identifier. */ + identifier: t.string, + /** Threat Name. */ + name: t.string, + }), + /** Risk Treatment. */ + treatment: t.string, + /** Risk Treatment status. */ + treatmentStatus: t.union([ + t.literal('InProgress'), + t.literal('UnderReview'), + t.literal('ExceptionRequested'), + t.literal('Approved'), + t.literal('ExceptionGranted'), + ]), + /** Risk Type. */ + type: t.union([ + t.literal('ASSESSMENTS'), + t.literal('ASSETS'), + t.literal('PROCESSING_ACTIVITIES'), + t.literal('VENDORS'), + t.literal('ENTITIES'), + t.literal('INCIDENTS'), + ]), + /** ID of an assessment. */ + typeRefIds: t.array(t.string), + /** List of vulnerabilities */ + vulnerabilities: t.array( + t.type({ + /** Vulnerability ID. */ + id: t.string, + /** Vulnerability Identifier. */ + identifier: t.string, + /** Vulnerability Name. */ + name: t.string, + }), + ), + /** The risk workflow */ + workflow: t.type({ + /** ID of an entity. */ + id: t.string, + /** Name of an entity. */ + name: t.string, + }), +}); + +/** Type override */ +export type OneTrustGetRiskResponseCodec = t.TypeOf< + typeof OneTrustGetRiskResponseCodec +>; + +export const OneTrustRiskDetailsCodec = t.type({ + /** Risk description. */ + description: OneTrustGetRiskResponseCodec.props.description, + /** Short Name for a Risk. */ + name: OneTrustGetRiskResponseCodec.props.name, + /** Risk Treatment. */ + treatment: OneTrustGetRiskResponseCodec.props.treatment, + /** Risk Treatment status. */ + treatmentStatus: OneTrustGetRiskResponseCodec.props.treatmentStatus, + /** Risk Type. */ + type: OneTrustGetRiskResponseCodec.props.type, + /** The risk stage */ + stage: OneTrustGetRiskResponseCodec.props.stage, + /** The risk state */ + state: OneTrustGetRiskResponseCodec.props.state, + /** Risk Result. */ + result: OneTrustGetRiskResponseCodec.props.result, + /** List of categories. */ + categories: OneTrustGetRiskResponseCodec.props.categories, +}); + +/** Type override */ +export type OneTrustRiskDetailsCodec = t.TypeOf< + typeof OneTrustRiskDetailsCodec +>; + +/* eslint-enable max-lines */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 0540b00c..ab83b114 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -2,13 +2,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - // OneTrustApprover, - OneTrustAssessment, - OneTrustAssessmentQuestion, - OneTrustAssessmentQuestionResponses, - OneTrustAssessmentSection, - OneTrustGetAssessmentResponse, -} from './types'; + OneTrustAssessmentCodec, + OneTrustAssessmentQuestionCodec, + OneTrustAssessmentQuestionResponsesCodec, + OneTrustAssessmentQuestionRiskCodec, + OneTrustAssessmentSectionCodec, + OneTrustGetAssessmentResponseCodec, + OneTrustRiskDetailsCodec, +} from './codecs'; // TODO: test what happens when a value is null -> it should convert to '' const flattenObject = (obj: any, prefix = ''): any => @@ -55,7 +56,7 @@ const flattenList = (list: any[], prefix: string): any => { }; const flattenOneTrustQuestionResponses = ( - questionResponses: OneTrustAssessmentQuestionResponses[], + questionResponses: OneTrustAssessmentQuestionResponsesCodec[], prefix: string, ): any => { if (questionResponses.length === 0) { @@ -71,7 +72,7 @@ const flattenOneTrustQuestionResponses = ( }; const flattenOneTrustQuestion = ( - oneTrustQuestion: OneTrustAssessmentQuestion, + oneTrustQuestion: OneTrustAssessmentQuestionCodec, prefix: string, ): any => { const { @@ -93,7 +94,7 @@ const flattenOneTrustQuestion = ( }; const flattenOneTrustQuestions = ( - questions: OneTrustAssessmentQuestion[], + questions: OneTrustAssessmentQuestionCodec[], prefix: string, ): any => questions.reduce( @@ -104,7 +105,9 @@ const flattenOneTrustQuestions = ( {}, ); -const flattenOneTrustSection = (section: OneTrustAssessmentSection): any => { +const flattenOneTrustSection = ( + section: OneTrustAssessmentSectionCodec, +): any => { const { questions, header, ...rest } = section; // the flattened section key has format like sections_${sequence}_sectionId @@ -115,14 +118,28 @@ const flattenOneTrustSection = (section: OneTrustAssessmentSection): any => { }; }; -const flattenOneTrustSections = (sections: OneTrustAssessmentSection[]): any => +const flattenOneTrustSections = ( + sections: OneTrustAssessmentSectionCodec[], +): any => sections.reduce( (acc, section) => ({ ...acc, ...flattenOneTrustSection(section) }), {}, ); export const flattenOneTrustAssessment = ( - assessment: OneTrustAssessment & OneTrustGetAssessmentResponse, + assessment: OneTrustAssessmentCodec & + OneTrustGetAssessmentResponseCodec & { + /** the sections enriched with risk details */ + sections: (OneTrustAssessmentSectionCodec & { + /** the questions enriched with risk details */ + questions: (OneTrustAssessmentQuestionCodec & { + /** the enriched risk details */ + risks: + | (OneTrustAssessmentQuestionRiskCodec & OneTrustRiskDetailsCodec)[] + | null; + })[]; + })[]; + }, ): any => { const { approvers, diff --git a/src/oneTrust/getListOfOneTrustAssessments.ts b/src/oneTrust/getListOfOneTrustAssessments.ts index e840690f..1b1eaa39 100644 --- a/src/oneTrust/getListOfOneTrustAssessments.ts +++ b/src/oneTrust/getListOfOneTrustAssessments.ts @@ -1,9 +1,10 @@ import { Got } from 'got'; import { logger } from '../logger'; import { - OneTrustAssessment, - OneTrustGetListOfAssessmentsResponse, -} from './types'; + OneTrustAssessmentCodec, + OneTrustGetListOfAssessmentsResponseCodec, +} from './codecs'; +import { decodeCodec } from '@transcend-io/type-utils'; /** * Fetch a list of all assessments from the OneTrust client. @@ -17,12 +18,12 @@ export const getListOfOneTrustAssessments = async ({ }: { /** The OneTrust client instance */ oneTrust: Got; -}): Promise => { +}): Promise => { let currentPage = 0; let totalPages = 1; let totalElements = 0; - const allAssessments: OneTrustAssessment[] = []; + const allAssessments: OneTrustAssessmentCodec[] = []; logger.info('Getting list of all assessments from OneTrust...'); while (currentPage < totalPages) { @@ -30,9 +31,11 @@ export const getListOfOneTrustAssessments = async ({ const { body } = await oneTrust.get( `api/assessment/v2/assessments?page=${currentPage}&size=2000`, ); - const { page, content } = JSON.parse( + + const { page, content } = decodeCodec( + OneTrustGetListOfAssessmentsResponseCodec, body, - ) as OneTrustGetListOfAssessmentsResponse; + ); allAssessments.push(...(content ?? [])); if (currentPage === 0) { totalPages = page?.totalPages ?? 0; diff --git a/src/oneTrust/getOneTrustAssessment.ts b/src/oneTrust/getOneTrustAssessment.ts index 5b7fd090..de99a6ba 100644 --- a/src/oneTrust/getOneTrustAssessment.ts +++ b/src/oneTrust/getOneTrustAssessment.ts @@ -1,5 +1,6 @@ import { Got } from 'got'; -import { OneTrustGetAssessmentResponse } from './types'; +import { OneTrustGetAssessmentResponseCodec } from './codecs'; +import { decodeCodec } from '@transcend-io/type-utils'; /** * Retrieve details about a particular assessment. @@ -16,10 +17,10 @@ export const getOneTrustAssessment = async ({ oneTrust: Got; /** The ID of the assessment to retrieve */ assessmentId: string; -}): Promise => { +}): Promise => { const { body } = await oneTrust.get( `api/assessment/v2/assessments/${assessmentId}/export?ExcludeSkippedQuestions=false`, ); - return JSON.parse(body) as OneTrustGetAssessmentResponse; + return decodeCodec(OneTrustGetAssessmentResponseCodec, body); }; diff --git a/src/oneTrust/getOneTrustRisk.ts b/src/oneTrust/getOneTrustRisk.ts index 7768a898..a649caef 100644 --- a/src/oneTrust/getOneTrustRisk.ts +++ b/src/oneTrust/getOneTrustRisk.ts @@ -1,12 +1,13 @@ import { Got } from 'got'; -import { OneTrustGetRiskResponse } from './types'; +import { OneTrustGetRiskResponseCodec } from './codecs'; +import { decodeCodec } from '@transcend-io/type-utils'; /** - * Retrieve details about a particular assessment. + * Retrieve details about a particular risk. * ref: https://developer.onetrust.com/onetrust/reference/getriskusingget * - * @param param - the information about the OneTrust client and assessment to retrieve - * @returns details about the assessment + * @param param - the information about the OneTrust client and risk to retrieve + * @returns the OneTrust risk */ export const getOneTrustRisk = async ({ oneTrust, @@ -16,8 +17,8 @@ export const getOneTrustRisk = async ({ oneTrust: Got; /** The ID of the OneTrust risk to retrieve */ riskId: string; -}): Promise => { +}): Promise => { const { body } = await oneTrust.get(`api/risk/v2/risks/${riskId}`); - return JSON.parse(body) as OneTrustGetRiskResponse; + return decodeCodec(OneTrustGetRiskResponseCodec, body); }; diff --git a/src/oneTrust/types.ts b/src/oneTrust/types.ts deleted file mode 100644 index 62d6fd53..00000000 --- a/src/oneTrust/types.ts +++ /dev/null @@ -1,769 +0,0 @@ -/* eslint-disable max-lines */ -export interface OneTrustAssessment { - /** ID of the assessment. */ - assessmentId: string; - /** Date that the assessment was created. */ - createDt: string; - /** Overall risk score without considering existing controls. */ - inherentRiskScore: number; - /** Date and time that the assessment was last updated. */ - lastUpdated: string; - /** Name of the assessment. */ - name: string; - /** Number assigned to the assessment. */ - number: number; - /** Number of risks that are open on the assessment. */ - openRiskCount: number; - /** Name of the organization group assigned to the assessment. */ - orgGroupName: string; - /** Details about the inventory record which is the primary record of the assessment. */ - primaryInventoryDetails: { - /** GUID of the inventory record. */ - primaryInventoryId: string; - /** Name of the inventory record. */ - primaryInventoryName: string; - /** Integer ID of the inventory record. */ - primaryInventoryNumber: number; - }; - /** Overall risk score after considering existing controls. */ - residualRiskScore: number; - /** Result of the assessment. NOTE: This field will be deprecated soon. Please reference the 'resultName' field instead. */ - result: 'Approved' | 'AutoClosed' | 'Rejected'; - /** ID of the result. */ - resultId: string; - /** Name of the result. */ - resultName: - | 'Approved - Remediation required' - | 'Approved' - | 'Rejected' - | 'Assessment suspended - On Hold' - | null; - /** State of the assessment. */ - state: 'ARCHIVE' | 'ACTIVE'; - /** Status of the assessment. */ - status: 'Not Started' | 'In Progress' | 'Under Review' | 'Completed'; - /** Name of the tag attached to the assessment. */ - tags: string[]; - /** The desired risk score. */ - targetRiskScore: number; - /** ID used to launch an assessment using a specific version of a template. */ - templateId: string; - /** Name of the template that is being used on the assessment. */ - templateName: string; - /** ID used to launch an assessment using the latest published version of a template. */ - templateRootVersionId: string; -} - -// ref: https://developer.onetrust.com/onetrust/reference/getallassessmentbasicdetailsusingget -export interface OneTrustGetListOfAssessmentsResponse { - /** The list of assessments in the current page. */ - content?: OneTrustAssessment[]; - /** Details about the pages being fetched */ - page?: { - /** Page number of the results list (0…N). */ - number: number; - /** Number of records per page (0…N). */ - size: number; - /** Total number of elements. */ - totalElements: number; - /** Total number of pages. */ - totalPages: number; - }; -} - -interface OneTrustAssessmentQuestionOption { - /** ID of the option. */ - id: string; - /** Name of the option. */ - option: string; - /** Order in which the option appears. */ - sequence: number; - /** Attribute for which the option is available. */ - attributes: string | null; - /** Type of option. */ - optionType: 'NOT_SURE' | 'NOT_APPLICABLE' | 'OTHERS' | 'DEFAULT'; -} - -interface OneTrustAssessmentQuestionRisks { - /** ID of the question for which the risk was flagged. */ - questionId: string; - /** ID of the flagged risk. */ - riskId: string; - /** Level of risk flagged on the question. */ - level: number; - /** Score of risk flagged on the question. */ - score: number; - /** Probability of risk flagged on the question. */ - probability?: number; - /** Impact Level of risk flagged on the question. */ - impactLevel?: number; -} - -export interface OneTrustAssessmentQuestionResponses { - /** The responses */ - responses: { - /** ID of the response. */ - responseId: string; - /** Content of the response. */ - response: string; - /** Type of response. */ - type: - | 'NOT_SURE' - | 'JUSTIFICATION' - | 'NOT_APPLICABLE' - | 'DEFAULT' - | 'OTHERS'; - /** Source from which the assessment is launched. */ - responseSourceType: 'LAUNCH_FROM_INVENTORY' | 'FORCE_CREATED_SOURCE' | null; - /** Error associated with the response. */ - errorCode: - | 'ATTRIBUTE_DISABLED' - | 'ATTRIBUTE_OPTION_DISABLED' - | 'INVENTORY_NOT_EXISTS' - | 'RELATED_INVENTORY_ATTRIBUTE_DISABLED' - | 'DATA_ELEMENT_NOT_EXISTS' - | 'DUPLICATE_INVENTORY' - | null; - /** This parameter is only applicable for inventory type responses (Example- ASSETS). */ - responseMap: object; - /** Indicates whether the response is valid. */ - valid: boolean; - /** The data subject */ - dataSubject: { - /** The ID of the data subject */ - id: string; - /** The ID of the data subject */ - name: string; - }; - /** The data category */ - dataCategory: { - /** The ID of the data category */ - id: string; - /** The name of the data category */ - name: string; - }; - /** The data element */ - dataElement: { - /** The ID of the data element */ - id: string; - /** The ID of the data element */ - name: string; - }; - }[]; - /** Justification comments for the given response. */ - justification: string | null; -} - -export interface OneTrustAssessmentQuestion { - /** The question */ - question: { - /** ID of the question. */ - id: string; - /** ID of the root version of the question. */ - rootVersionId: string; - /** Order in which the question appears in the assessment. */ - sequence: number; - /** Type of question in the assessment. */ - questionType: - | 'TEXTBOX' - | 'MULTICHOICE' - | 'YESNO' - | 'DATE' - | 'STATEMENT' - | 'INVENTORY' - | 'ATTRIBUTE' - | 'PERSONAL_DATA'; - /** Indicates whether a response to the question is required. */ - required: boolean; - /** Data element attributes that are directly updated by the question. */ - attributes: string; - /** Short, descriptive name for the question. */ - friendlyName: string | null; - /** Description of the question. */ - description: string | null; - /** Tooltip text within a hint for the question. */ - hint: string; - /** ID of the parent question. */ - parentQuestionId: string; - /** Indicates whether the response to the question is prepopulated. */ - prePopulateResponse: boolean; - /** Indicates whether the assessment is linked to inventory records. */ - linkAssessmentToInventory: boolean; - /** The question options */ - options: OneTrustAssessmentQuestionOption[] | null; - /** Indicates whether the question is valid. */ - valid: boolean; - /** Type of question in the assessment. */ - type: - | 'TEXTBOX' - | 'MULTICHOICE' - | 'YESNO' - | 'DATE' - | 'STATEMENT' - | 'INVENTORY' - | 'ATTRIBUTE' - | 'PERSONAL_DATA'; - /** Whether the response can be multi select */ - allowMultiSelect: boolean; - /** The text of a question. */ - content: string; - /** Indicates whether justification comments are required for the question. */ - requireJustification: boolean; - }; - /** Indicates whether the question is hidden on the assessment. */ - hidden: boolean; - /** Reason for locking the question in the assessment. */ - lockReason: 'LAUNCH_FROM_INVENTORY' | 'FORCE_CREATION_LOCK' | null; - /** The copy errors */ - copyErrors: string | null; - /** Indicates whether navigation rules are enabled for the question. */ - hasNavigationRules: boolean; - /** The responses to this question */ - questionResponses: OneTrustAssessmentQuestionResponses[]; - /** The risks associated with this question */ - risks: OneTrustAssessmentQuestionRisks[] | null; - /** List of IDs associated with the question root requests. */ - rootRequestInformationIds: string[]; - /** Number of attachments added to the question. */ - totalAttachments: number; - /** IDs of the attachment(s) added to the question. */ - attachmentIds: string[]; -} - -export interface OneTrustAssessmentSection { - /** The Assessment section header */ - header: { - /** ID of the section in the assessment. */ - sectionId: string; - /** Name of the section. */ - name: string; - /** Description of the section header. */ - description: string | null; - /** Sequence of the section within the form */ - sequence: number; - /** Indicates whether the section is hidden in the assessment. */ - hidden: boolean; - /** IDs of invalid questions in the section. */ - invalidQuestionIds: string[]; - /** IDs of required but unanswered questions in the section. */ - requiredUnansweredQuestionIds: string[]; - /** IDs of required questions in the section. */ - requiredQuestionIds: string[]; - /** IDs of unanswered questions in the section. */ - unansweredQuestionIds: string[]; - /** IDs of effectiveness questions in the section. */ - effectivenessQuestionIds: string[]; - /** Number of invalid questions in the section. */ - invalidQuestionCount: number; - /** The risk statistics */ - riskStatistics: null | { - /** Maximum level of risk in the section. */ - maxRiskLevel: number; - /** Number of risks in the section. */ - riskCount: number; - /** ID of the section in the assessment. */ - sectionId: string; - }; - /** Whether the section was submitted */ - submitted: boolean; - }; - /** The questions within the section */ - questions: OneTrustAssessmentQuestion[]; - /** Indicates whether navigation rules are enabled for the question. */ - hasNavigationRules: boolean; - /** Who submitted the section */ - submittedBy: null | { - /** The ID of the user who submitted the section */ - id: string; - /** THe name or email of the user who submitted the section */ - name: string; - }; - /** Date of the submission */ - submittedDt: string | null; - /** Name of the section. */ - name: string; - /** Indicates whether navigation rules are enabled for the question. */ - hidden: boolean; - /** Indicates whether the section is valid. */ - valid: boolean; - /** ID of the section in an assessment. */ - sectionId: string; - /** Sequence of the section within the form */ - sequence: number; - /** Whether the section was submitted */ - submitted: boolean; - /** Descriptions of the section. */ - description: string | null; -} - -export interface OneTrustApprover { - /** ID of the user assigned as an approver. */ - id: string; - /** ID of the workflow stage */ - workflowStageId: string; - /** Name of the user assigned as an approver. */ - name: string; - /** More details about the approver */ - approver: { - /** ID of the user assigned as an approver. */ - id: string; - /** Full name of the user assigned as an approver. */ - fullName: string; - /** Email of the user assigned as an approver. */ - email: string | null; - /** Whether the user assigned as an approver was deleted. */ - deleted: boolean; - }; - /** Assessment approval status. */ - approvalState: 'OPEN' | 'APPROVED' | 'REJECTED'; - /** Date and time at which the assessment was approved. */ - approvedOn: string; - /** ID of the assessment result. */ - resultId: string; - /** Name of the assessment result. */ - resultName: - | 'Approved - Remediation required' - | 'Approved' - | 'Rejected' - | 'Assessment suspended - On Hold' - | null; - /** Name key of the assessment result. */ - resultNameKey: string; -} - -// ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget -export interface OneTrustGetAssessmentResponse { - /** List of users assigned as approvers of the assessment. */ - approvers: OneTrustApprover[]; - /** ID of an assessment. */ - assessmentId: string; - /** Number assigned to an assessment. */ - assessmentNumber: number; - /** Date and time at which the assessment was completed. */ - completedOn: string | null; - /** Creator of the Assessment */ - createdBy: { - /** The ID of the creator */ - id: string; - /** The name of the creator */ - name: string; - }; - /** Date and time at which the assessment was created. */ - createdDT: string; - /** Date and time by which the assessment must be completed. */ - deadline: string | null; - /** Description of the assessment. */ - description: string | null; - /** Overall inherent risk score without considering the existing controls. */ - inherentRiskScore: number | null; - /** Date and time at which the assessment was last updated. */ - lastUpdated: string; - /** Number of risks captured on the assessment with a low risk level. */ - lowRisk: number; - /** Number of risks captured on the assessment with a medium risk level. */ - mediumRisk: number; - /** Number of risks captured on the assessment with a high risk level. */ - highRisk: number; - /** Name of the assessment. */ - name: string; - /** Number of open risks that have not been addressed. */ - openRiskCount: number; - /** The organization group */ - orgGroup: { - /** The ID of the organization group */ - id: string; - /** The name of the organization group */ - name: string; - }; - /** The primary record */ - primaryEntityDetails: { - /** Unique ID for the primary record. */ - id: string; - /** Name of the primary record. */ - name: string; - /** The number associated with the primary record. */ - number: number; - /** Name and number of the primary record. */ - displayName: string; - }[]; - /** Type of inventory record designated as the primary record. */ - primaryRecordType: - | 'ASSETS' - | 'PROCESSING_ACTIVITY' - | 'VENDORS' - | 'ENTITIES' - | 'ASSESS_CONTROL' - | 'ENGAGEMENT' - | null; - /** Overall risk score after considering existing controls. */ - residualRiskScore: number | null; - /** The respondent */ - respondent: { - /** The ID of the respondent */ - id: string; - /** The name or email of the respondent */ - name: string; - }; - /** The respondents */ - respondents: { - /** The ID of the respondent */ - id: string; - /** The name or email of the respondent */ - name: string; - }[]; - /** Result of the assessment. */ - result: string | null; - /** ID of the result. */ - resultId: string | null; - /** Name of the result. */ - resultName: - | 'Approved - Remediation required' - | 'Approved' - | 'Rejected' - | 'Assessment suspended - On Hold' - | null; - /** Risk level of the assessment. */ - riskLevel: 'None' | 'Low' | 'Medium' | 'High' | 'Very High'; - /** List of sections in the assessment. */ - sections: OneTrustAssessmentSection[]; - /** Status of the assessment. */ - status: 'Not Started' | 'In Progress' | 'Under Review' | 'Completed' | null; - /** Date and time at which the assessment was submitted. */ - submittedOn: string | null; - /** List of tags associated with the assessment. */ - tags: string[]; - /** The desired target risk score. */ - targetRiskScore: number | null; - /** The template */ - template: { - /** The ID of the template */ - id: string; - /** The name of the template */ - name: string; - }; - /** Number of total risks on the assessment. */ - totalRiskCount: number; - /** Number of very high risks on the assessment. */ - veryHighRisk: number; - /** Welcome text if any in the assessment. */ - welcomeText: string | null; -} - -// ref: https://developer.onetrust.com/onetrust/reference/getriskusingget -export interface OneTrustGetRiskResponse { - /** List of associated inventories to the risk. */ - associatedInventories: { - /** ID of the Inventory. */ - inventoryId: string; - /** Name of the Inventory. */ - inventoryName: string; - /** Type of the Inventory. */ - inventoryType: 'ASSETS PROCESSING_ACTIVITIES VENDORS ENTITIES'; - /** ID of the Inventory's Organization. */ - organizationId: string; - /** The source type */ - sourceType: { - /** Indicates whether entity type is eligible for linking/relating with risk or not */ - eligibleForEntityLink: boolean; - /** Indicates whether the entity type is enabled or not. */ - enabled: boolean; - /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ - id: string; - /** Entity Type Name. */ - label: string; - /** Name of the module. */ - moduleName: boolean; - /** Indicates whether this type can be risk type or not in Risk */ - riskType: boolean; - /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ - seeded: boolean; - /** Indicates whether this type can be source type or not in Risk */ - sourceType: boolean; - /** Translation Key of Entity Type ID. */ - translationKey: string; - }; - }[]; - /** The attribute values associated with the risk */ - attributeValues: { - /** List of custom attributes. */ - additionalProp: { - /** Additional information like Source Questions, Approver Ids, Inventory Type. This will be a Map of String Key and Object value. */ - additionalAttributes: object; - /** Attribute option GUID. */ - id: string; - /** Attribute selection value and it is mandatory if the numeric value is not distinct for Numerical Single Select attribute. */ - optionSelectionValue: string; - /** Attribute option value. */ - value: string; - /** Attribute option value key for translation. */ - valueKey: string; - }[]; - }; - /** List of categories. */ - categories: { - /** Identifier for Risk Category. */ - id: string; - /** Risk Category Name. */ - name: string; - /** Risk Category Name Key value for translation. */ - nameKey: string; - }[]; - /** List of Control Identifiers. */ - controlsIdentifier: string[]; - /** Risk created time. */ - createdUTCDateTime: string; - /** Risk Creation Type. */ - creationType: string; - /** Date when the risk is closed. */ - dateClosed: string; - /** Deadline date for the risk. */ - deadline: string; - /** Risk delete type. */ - deleteType: 'SOFT'; - /** Risk description. */ - description: string; - /** ID of the risk. */ - id: string; - /** Residual impact level name. */ - impactLevel: string; - /** Residual impact level ID. */ - impactLevelId: number; - /** The inherent risk level */ - inherentRiskLevel: { - /** Risk Impact Level name. */ - impactLevel: string; - /** Risk Impact level ID. */ - impactLevelId: number; - /** Risk Level Name. */ - level: string; - /** Risk Level ID. */ - levelId: number; - /** Risk Probability Level Name. */ - probabilityLevel: string; - /** Risk Probability Level ID. */ - probabilityLevelId: number; - /** Risk Score. */ - riskScore: number; - }; - /** The risk justification */ - justification: string; - /** Residual level name. */ - level: string; - /** Residual level display name. */ - levelDisplayName: string; - /** Residual level ID. */ - levelId: number; - /** Risk mitigated date. */ - mitigatedDate: string; - /** Risk Mitigation details. */ - mitigation: string; - /** Short Name for a Risk. */ - name: string; - /** Integer risk identifier. */ - number: number; - /** The organization group */ - orgGroup: { - /** ID of an entity. */ - id: string; - /** Name of an entity. */ - name: string; - }; - /** The previous risk state */ - previousState: - | 'IDENTIFIED' - | 'RECOMMENDATION_ADDED' - | 'RECOMMENDATION_SENT' - | 'REMEDIATION_PROPOSED' - | 'EXCEPTION_REQUESTED' - | 'REDUCED' - | 'RETAINED' - | 'ARCHIVED_IN_VERSION'; - /** Residual probability level. */ - probabilityLevel: string; - /** Residual probability level ID. */ - probabilityLevelId: number; - /** Risk Recommendation. */ - recommendation: string; - /** Proposed remediation. */ - remediationProposal: string; - /** Deadline reminder days. */ - reminderDays: number; - /** Risk exception request. */ - requestedException: string; - /** Risk Result. */ - result: - | 'Accepted' - | 'Avoided' - | 'Reduced' - | 'Rejected' - | 'Transferred' - | 'Ignored'; - /** Risk approvers name csv. */ - riskApprovers: string; - /** Risk approvers ID. */ - riskApproversId: string[]; - /** List of risk owners ID. */ - riskOwnersId: string[]; - /** Risk owners name csv. */ - riskOwnersName: string; - /** Risk score. */ - riskScore: number; - /** The risk source type */ - riskSourceType: { - /** Indicates whether entity type is eligible for linking/relating with risk or not */ - eligibleForEntityLink: boolean; - /** Indicates whether the entity type is enabled or not. */ - enabled: boolean; - /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ - id: string; - /** Entity Type Name. */ - label: string; - /** Name of the module. */ - moduleName: boolean; - /** Indicates whether this type can be risk type or not in Risk */ - riskType: boolean; - /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ - seeded: boolean; - /** Indicates whether this type can be source type or not in Risk */ - sourceType: boolean; - /** Translation Key of Entity Type ID. */ - translationKey: string; - }; - /** The risk type */ - riskType: { - /** Indicates whether entity type is eligible for linking/relating with risk or not */ - eligibleForEntityLink: boolean; - /** Indicates whether the entity type is enabled or not. */ - enabled: boolean; - /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ - id: string; - /** Entity Type Name. */ - label: string; - /** Name of the module. */ - moduleName: boolean; - /** Indicates whether this type can be risk type or not in Risk */ - riskType: boolean; - /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ - seeded: boolean; - /** Indicates whether this type can be source type or not in Risk */ - sourceType: boolean; - /** Translation Key of Entity Type ID. */ - translationKey: string; - }; - /** For Auto risk, rule Id reference. */ - ruleRootVersionId: string; - /** The risk source */ - source: { - // eslint-disable-next-line max-len - /** Additional information about the Source Entity. This will be a Map of String Key and Object value. 'inventoryType' key is mandatory to be passed when sourceType is 'Inventory', and it can have one of the following values, 20 - Assets, 30 - Processing Activities, 50 - Vendors, 60 - Entities */ - additionalAttributes: object; - /** Source Entity ID. */ - id: string; - /** Source Entity Name. */ - name: string; - /** The risk source type */ - sourceType: { - /** Indicates whether entity type is eligible for linking/relating with risk or not */ - eligibleForEntityLink: boolean; - /** Indicates whether the entity type is enabled or not. */ - enabled: boolean; - /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ - id: string; - /** Entity Type Name. */ - label: string; - /** Name of the module. */ - moduleName: boolean; - /** Indicates whether this type can be risk type or not in Risk */ - riskType: boolean; - /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ - seeded: boolean; - /** Indicates whether this type can be source type or not in Risk */ - sourceType: boolean; - /** Translation Key of Entity Type ID. */ - translationKey: string; - }; - /** Source Entity Type. */ - type: 'PIA' | 'RA' | 'GRA' | 'INVENTORY' | 'INCIDENT' | 'GENERIC'; - }; - /** The risk stage */ - stage: { - /** ID of an entity. */ - id: string; - /** Name of an entity. */ - name: string; - /** Name Key of the entity for translation. */ - nameKey: string; - }; - /** The risk state */ - state: - | 'IDENTIFIED' - | 'RECOMMENDATION_ADDED' - | 'RECOMMENDATION_SENT' - | 'REMEDIATION_PROPOSED' - | 'EXCEPTION_REQUESTED' - | 'REDUCED' - | 'RETAINED' - | 'ARCHIVED_IN_VERSION'; - /** The target risk level */ - targetRiskLevel: { - /** Risk Impact Level name. */ - impactLevel: string; - /** Risk Impact level ID. */ - impactLevelId: number; - /** Risk Level Name. */ - level: string; - /** Risk Level ID. */ - levelId: number; - /** Risk Probability Level Name. */ - probabilityLevel: string; - /** Risk Probability Level ID. */ - probabilityLevelId: number; - /** Risk Score. */ - riskScore: number; - }; - /** The risk threat */ - threat: { - /** Threat ID. */ - id: string; - /** Threat Identifier. */ - identifier: string; - /** Threat Name. */ - name: string; - }; - /** Risk Treatment. */ - treatment: string; - /** Risk Treatment status. */ - treatmentStatus: - | 'InProgress' - | 'UnderReview' - | 'ExceptionRequested' - | 'Approved' - | 'ExceptionGranted'; - /** Risk Type. */ - type: - | 'ASSESSMENTS' - | 'ASSETS' - | 'PROCESSING_ACTIVITIES' - | 'VENDORS' - | 'ENTITIES' - | 'INCIDENTS'; - /** ID of an assessment. */ - typeRefIds: string[]; - /** List of vulnerabilities */ - vulnerabilities: { - /** Vulnerability ID. */ - id: string; - /** Vulnerability Identifier. */ - identifier: string; - /** Vulnerability Name. */ - name: string; - }[]; - /** The risk workflow */ - workflow: { - /** ID of an entity. */ - id: string; - /** Name of an entity. */ - name: string; - }; -} -/* eslint-enable max-lines */ diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index fee14a29..66686549 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -1,7 +1,12 @@ import { logger } from '../logger'; +import keyBy from 'lodash/keyBy'; import colors from 'colors'; import { OneTrustFileFormat } from '../enums'; -import { OneTrustAssessment, OneTrustGetAssessmentResponse } from './types'; +import { + OneTrustAssessmentCodec, + OneTrustGetAssessmentResponseCodec, + OneTrustGetRiskResponseCodec, +} from './codecs'; import fs from 'fs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; @@ -16,6 +21,7 @@ export const writeOneTrustAssessment = ({ fileFormat, assessment, assessmentDetails, + riskDetails, index, total, }: { @@ -24,9 +30,11 @@ export const writeOneTrustAssessment = ({ /** The format of the output file */ fileFormat: OneTrustFileFormat; /** The basic assessment */ - assessment: OneTrustAssessment; + assessment: OneTrustAssessmentCodec; /** The assessment with details */ - assessmentDetails: OneTrustGetAssessmentResponse; + assessmentDetails: OneTrustGetAssessmentResponseCodec; + /** The details of risks found within the assessment */ + riskDetails: OneTrustGetRiskResponseCodec[]; /** The index of the assessment being written to the file */ index: number; /** The total amount of assessments that we will write */ @@ -40,10 +48,45 @@ export const writeOneTrustAssessment = ({ ), ); + // enrich the sections with risk details + const riskDetailsById = keyBy(riskDetails, 'id'); + const { sections, ...restAssessmentDetails } = assessmentDetails; + const enrichedSections = sections.map((section) => { + const { questions, ...restSection } = section; + const enrichedQuestions = questions.map((question) => { + const { risks, ...restQuestion } = question; + const enrichedRisks = (risks ?? []).map((risk) => { + const details = riskDetailsById[risk.riskId]; + // TODO: missing the risk meta data and links to the assessment + return { + ...risk, + description: details.description, + name: details.name, + treatment: details.treatment, + treatmentStatus: details.treatmentStatus, + type: details.type, + state: details.state, + stage: details.stage, + result: details.result, + categories: details.categories, + }; + }); + return { + ...restQuestion, + risks: enrichedRisks, + }; + }); + return { + ...restSection, + questions: enrichedQuestions, + }; + }); + // combine the two assessments into a single enriched result const enrichedAssessment = { - ...assessmentDetails, + ...restAssessmentDetails, ...assessment, + sections: enrichedSections, }; // For json format From 559ab87997f409e8f2d8acfe428ca0860410f17a Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 22:22:20 +0000 Subject: [PATCH 06/79] fix codecs --- src/cli-pull-ot.ts | 31 +- src/oneTrust/codecs.ts | 400 ++++++++++++---------- src/oneTrust/flattenOneTrustAssessment.ts | 218 +++++++----- src/oneTrust/writeOneTrustAssessment.ts | 7 +- 4 files changed, 375 insertions(+), 281 deletions(-) diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index 5981ef16..f6ffac0d 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -12,6 +12,7 @@ import { import { OneTrustPullResource } from './enums'; import { mapSeries, map } from 'bluebird'; import uniq from 'lodash/uniq'; +import { OneTrustGetRiskResponseCodec } from './oneTrust/codecs'; /** * Pull configuration from OneTrust down locally to disk @@ -34,6 +35,9 @@ async function main(): Promise { // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); + // TODO: undo + // const theAssessments = assessments.slice(660); + // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory await mapSeries(assessments, async (assessment, index) => { logger.info( @@ -47,6 +51,7 @@ async function main(): Promise { }); // enrich assessments with risk information + let riskDetails: OneTrustGetRiskResponseCodec[] = []; const riskIds = uniq( assessmentDetails.sections.flatMap((s) => s.questions.flatMap((q) => @@ -54,18 +59,20 @@ async function main(): Promise { ), ), ); - logger.info( - `Fetching details about ${riskIds} risks for assessment ${ - index + 1 - } of ${assessments.length}...`, - ); - const riskDetails = await map( - riskIds, - (riskId) => getOneTrustRisk({ oneTrust, riskId }), - { - concurrency: 5, - }, - ); + if (riskIds.length > 0) { + logger.info( + `Fetching details about ${riskIds.length} risks for assessment ${ + index + 1 + } of ${assessments.length}...`, + ); + riskDetails = await map( + riskIds, + (riskId) => getOneTrustRisk({ oneTrust, riskId }), + { + concurrency: 5, + }, + ); + } writeOneTrustAssessment({ assessment, diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 191b51c9..9d9ad0da 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -9,7 +9,7 @@ export const OneTrustAssessmentCodec = t.type({ /** Date that the assessment was created. */ createDt: t.string, /** Overall risk score without considering existing controls. */ - inherentRiskScore: t.number, + inherentRiskScore: t.union([t.number, t.null]), /** Date and time that the assessment was last updated. */ lastUpdated: t.string, /** Name of the assessment. */ @@ -21,30 +21,36 @@ export const OneTrustAssessmentCodec = t.type({ /** Name of the organization group assigned to the assessment. */ orgGroupName: t.string, /** Details about the inventory record which is the primary record of the assessment. */ - primaryInventoryDetails: t.type({ - /** GUID of the inventory record. */ - primaryInventoryId: t.string, - /** Name of the inventory record. */ - primaryInventoryName: t.string, - /** Integer ID of the inventory record. */ - primaryInventoryNumber: t.number, - }), + primaryInventoryDetails: t.union([ + t.type({ + /** GUID of the inventory record. */ + primaryInventoryId: t.string, + /** Name of the inventory record. */ + primaryInventoryName: t.string, + /** Integer ID of the inventory record. */ + primaryInventoryNumber: t.number, + }), + t.null, + ]), /** Overall risk score after considering existing controls. */ - residualRiskScore: t.number, + residualRiskScore: t.union([t.number, t.null]), /** Result of the assessment. NOTE: This field will be deprecated soon. Please reference the 'resultName' field instead. */ result: t.union([ t.literal('Approved'), t.literal('AutoClosed'), t.literal('Rejected'), + t.string, + t.null, ]), /** ID of the result. */ - resultId: t.string, + resultId: t.union([t.string, t.null]), /** Name of the result. */ resultName: t.union([ t.literal('Approved - Remediation required'), t.literal('Approved'), t.literal('Rejected'), t.literal('Assessment suspended - On Hold'), + t.string, t.null, ]), /** State of the assessment. */ @@ -55,11 +61,12 @@ export const OneTrustAssessmentCodec = t.type({ t.literal('In Progress'), t.literal('Under Review'), t.literal('Completed'), + t.null, ]), /** Name of the tag attached to the assessment. */ tags: t.array(t.string), /** The desired risk score. */ - targetRiskScore: t.number, + targetRiskScore: t.union([t.number, t.null]), /** ID used to launch an assessment using a specific version of a template. */ templateId: t.string, /** Name of the template that is being used on the assessment. */ @@ -99,7 +106,7 @@ const OneTrustAssessmentQuestionOptionCodec = t.type({ /** Name of the option. */ option: t.string, /** Order in which the option appears. */ - sequence: t.number, + sequence: t.union([t.number, t.null]), /** Attribute for which the option is available. */ attributes: t.union([t.string, t.null]), /** Type of option. */ @@ -111,20 +118,24 @@ const OneTrustAssessmentQuestionOptionCodec = t.type({ ]), }); -export const OneTrustAssessmentQuestionRiskCodec = t.type({ - /** ID of the question for which the risk was flagged. */ - questionId: t.string, - /** ID of the flagged risk. */ - riskId: t.string, - /** Level of risk flagged on the question. */ - level: t.number, - /** Score of risk flagged on the question. */ - score: t.number, - /** Probability of risk flagged on the question. */ - probability: t.union([t.number, t.undefined]), - /** Impact Level of risk flagged on the question. */ - impactLevel: t.union([t.number, t.undefined]), -}); +export const OneTrustAssessmentQuestionRiskCodec = t.intersection([ + t.type({ + /** ID of the question for which the risk was flagged. */ + questionId: t.string, + /** ID of the flagged risk. */ + riskId: t.string, + }), + t.partial({ + /** Level of risk flagged on the question. */ + level: t.union([t.number, t.null]), + /** Score of risk flagged on the question. */ + score: t.union([t.number, t.null]), + /** Probability of risk flagged on the question. */ + probability: t.union([t.number, t.undefined]), + /** Impact Level of risk flagged on the question. */ + impactLevel: t.union([t.number, t.undefined]), + }), +]); /** Type override */ export type OneTrustAssessmentQuestionRiskCodec = t.TypeOf< @@ -138,7 +149,7 @@ export const OneTrustAssessmentQuestionResponsesCodec = t.type({ /** ID of the response. */ responseId: t.string, /** Content of the response. */ - response: t.string, + response: t.union([t.string, t.null]), /** Type of response. */ type: t.union([ t.literal('NOT_SURE'), @@ -170,23 +181,23 @@ export const OneTrustAssessmentQuestionResponsesCodec = t.type({ /** The data subject */ dataSubject: t.type({ /** The ID of the data subject */ - id: t.string, + id: t.union([t.string, t.null]), /** The ID of the data subject */ - name: t.string, + name: t.union([t.string, t.null]), }), /** The data category */ dataCategory: t.type({ /** The ID of the data category */ - id: t.string, + id: t.union([t.string, t.null]), /** The name of the data category */ - name: t.string, + name: t.union([t.string, t.null]), }), /** The data element */ dataElement: t.type({ /** The ID of the data element */ - id: t.string, + id: t.union([t.string, t.null]), /** The ID of the data element */ - name: t.string, + name: t.union([t.string, t.null]), }), }), ), @@ -218,6 +229,9 @@ export const OneTrustAssessmentQuestionCodec = t.type({ t.literal('INVENTORY'), t.literal('ATTRIBUTE'), t.literal('PERSONAL_DATA'), + t.literal('ENGAGEMENT'), + t.literal('ASSESS_CONTROL'), + t.null, ]), /** Indicates whether a response to the question is required. */ required: t.boolean, @@ -228,9 +242,9 @@ export const OneTrustAssessmentQuestionCodec = t.type({ /** Description of the question. */ description: t.union([t.string, t.null]), /** Tooltip text within a hint for the question. */ - hint: t.string, + hint: t.union([t.string, t.null]), /** ID of the parent question. */ - parentQuestionId: t.string, + parentQuestionId: t.union([t.string, t.null]), /** Indicates whether the response to the question is prepopulated. */ prePopulateResponse: t.boolean, /** Indicates whether the assessment is linked to inventory records. */ @@ -249,6 +263,8 @@ export const OneTrustAssessmentQuestionCodec = t.type({ t.literal('INVENTORY'), t.literal('ATTRIBUTE'), t.literal('PERSONAL_DATA'), + t.literal('ENGAGEMENT'), + t.literal('ASSESS_CONTROL'), ]), /** Whether the response can be multi select */ allowMultiSelect: t.boolean, @@ -286,46 +302,50 @@ export type OneTrustAssessmentQuestionCodec = t.TypeOf< typeof OneTrustAssessmentQuestionCodec >; +export const OneTrustAssessmentSectionHeaderCodec = t.type({ + /** ID of the section in the assessment. */ + sectionId: t.string, + /** Name of the section. */ + name: t.string, + /** Description of the section header. */ + description: t.union([t.string, t.null]), + /** Sequence of the section within the form */ + sequence: t.number, + /** Indicates whether the section is hidden in the assessment. */ + hidden: t.boolean, + /** IDs of invalid questions in the section. */ + invalidQuestionIds: t.array(t.string), + /** IDs of required but unanswered questions in the section. */ + requiredUnansweredQuestionIds: t.array(t.string), + /** IDs of required questions in the section. */ + requiredQuestionIds: t.array(t.string), + /** IDs of unanswered questions in the section. */ + unansweredQuestionIds: t.array(t.string), + /** IDs of effectiveness questions in the section. */ + effectivenessQuestionIds: t.array(t.string), + /** The risk statistics */ + riskStatistics: t.union([ + t.type({ + /** Maximum level of risk in the section. */ + maxRiskLevel: t.number, + /** Number of risks in the section. */ + riskCount: t.number, + /** ID of the section in the assessment. */ + sectionId: t.string, + }), + t.null, + ]), + /** Whether the section was submitted */ + submitted: t.boolean, +}); +/** Type override */ +export type OneTrustAssessmentSectionHeaderCodec = t.TypeOf< + typeof OneTrustAssessmentSectionHeaderCodec +>; + export const OneTrustAssessmentSectionCodec = t.type({ /** The Assessment section header */ - header: t.type({ - /** ID of the section in the assessment. */ - sectionId: t.string, - /** Name of the section. */ - name: t.string, - /** Description of the section header. */ - description: t.union([t.string, t.null]), - /** Sequence of the section within the form */ - sequence: t.number, - /** Indicates whether the section is hidden in the assessment. */ - hidden: t.boolean, - /** IDs of invalid questions in the section. */ - invalidQuestionIds: t.array(t.string), - /** IDs of required but unanswered questions in the section. */ - requiredUnansweredQuestionIds: t.array(t.string), - /** IDs of required questions in the section. */ - requiredQuestionIds: t.array(t.string), - /** IDs of unanswered questions in the section. */ - unansweredQuestionIds: t.array(t.string), - /** IDs of effectiveness questions in the section. */ - effectivenessQuestionIds: t.array(t.string), - /** Number of invalid questions in the section. */ - invalidQuestionCount: t.number, - /** The risk statistics */ - riskStatistics: t.union([ - t.type({ - /** Maximum level of risk in the section. */ - maxRiskLevel: t.number, - /** Number of risks in the section. */ - riskCount: t.number, - /** ID of the section in the assessment. */ - sectionId: t.string, - }), - t.null, - ]), - /** Whether the section was submitted */ - submitted: t.boolean, - }), + header: OneTrustAssessmentSectionHeaderCodec, /** The questions within the section */ questions: t.array(OneTrustAssessmentQuestionCodec), /** Indicates whether navigation rules are enabled for the question. */ @@ -363,6 +383,26 @@ export type OneTrustAssessmentSectionCodec = t.TypeOf< typeof OneTrustAssessmentSectionCodec >; +// TODO: do not move to privacy-types +/** The OneTrustAssessmentSectionCodec type without header or questions */ +export const OneTrustFlatAssessmentSectionCodec = t.type({ + hasNavigationRules: OneTrustAssessmentSectionCodec.props.hasNavigationRules, + submittedBy: OneTrustAssessmentSectionCodec.props.submittedBy, + submittedDt: OneTrustAssessmentSectionCodec.props.submittedDt, + name: OneTrustAssessmentSectionCodec.props.name, + hidden: OneTrustAssessmentSectionCodec.props.hidden, + valid: OneTrustAssessmentSectionCodec.props.valid, + sectionId: OneTrustAssessmentSectionCodec.props.sectionId, + sequence: OneTrustAssessmentSectionCodec.props.sequence, + submitted: OneTrustAssessmentSectionCodec.props.submitted, + description: OneTrustAssessmentSectionCodec.props.description, +}); + +/** Type override */ +export type OneTrustFlatAssessmentSectionCodec = t.TypeOf< + typeof OneTrustFlatAssessmentSectionCodec +>; + export const OneTrustApproverCodec = t.type({ /** ID of the user assigned as an approver. */ id: t.string, @@ -388,26 +428,37 @@ export const OneTrustApproverCodec = t.type({ t.literal('REJECTED'), ]), /** Date and time at which the assessment was approved. */ - approvedOn: t.string, + approvedOn: t.union([t.string, t.null]), /** ID of the assessment result. */ - resultId: t.string, + resultId: t.union([t.string, t.null]), /** Name of the assessment result. */ resultName: t.union([ t.literal('Approved - Remediation required'), t.literal('Approved'), t.literal('Rejected'), t.literal('Assessment suspended - On Hold'), + t.string, t.null, ]), /** Name key of the assessment result. */ - resultNameKey: t.string, + resultNameKey: t.union([t.string, t.null]), }); /** Type override */ export type OneTrustApproverCodec = t.TypeOf; -// ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget +export const OneTrustAssessmentStatusCodec = t.union([ + t.literal('NOT_STARTED'), + t.literal('IN_PROGRESS'), + t.literal('UNDER_REVIEW'), + t.literal('COMPLETED'), +]); +/** Type override */ +export type OneTrustAssessmentStatusCodec = t.TypeOf< + typeof OneTrustAssessmentStatusCodec +>; +// ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget export const OneTrustGetAssessmentResponseCodec = t.type({ /** List of users assigned as approvers of the assessment. */ approvers: t.array(OneTrustApproverCodec), @@ -417,6 +468,8 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ assessmentNumber: t.number, /** Date and time at which the assessment was completed. */ completedOn: t.union([t.string, t.null]), + /** Status of the assessment. */ + status: OneTrustAssessmentStatusCodec, /** Creator of the Assessment */ createdBy: t.type({ /** The ID of the creator */ @@ -472,6 +525,7 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ t.literal('ENTITIES'), t.literal('ASSESS_CONTROL'), t.literal('ENGAGEMENT'), + t.literal('projects'), t.null, ]), /** Overall risk score after considering existing controls. */ @@ -502,6 +556,7 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ t.literal('Approved'), t.literal('Rejected'), t.literal('Assessment suspended - On Hold'), + t.string, t.null, ]), /** Risk level of the assessment. */ @@ -514,13 +569,6 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ ]), /** List of sections in the assessment. */ sections: t.array(OneTrustAssessmentSectionCodec), - status: t.union([ - t.literal('Not Started'), - t.literal('In Progress'), - t.literal('Under Review'), - t.literal('Completed'), - t.null, - ]), /** Date and time at which the assessment was submitted. */ submittedOn: t.union([t.string, t.null]), /** List of tags associated with the assessment. */ @@ -557,7 +605,7 @@ const EntityTypeCodec = t.type({ /** Entity Type Name. */ label: t.string, /** Name of the module. */ - moduleName: t.boolean, + moduleName: t.union([t.string, t.null]), /** Indicates whether this type can be risk type or not in Risk */ riskType: t.boolean, /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ @@ -570,19 +618,19 @@ const EntityTypeCodec = t.type({ const RiskLevelCodec = t.type({ /** Risk Impact Level name. */ - impactLevel: t.string, + impactLevel: t.union([t.string, t.null]), /** Risk Impact level ID. */ - impactLevelId: t.number, + impactLevelId: t.union([t.number, t.null]), /** Risk Level Name. */ - level: t.string, + level: t.union([t.string, t.null]), /** Risk Level ID. */ - levelId: t.number, + levelId: t.union([t.number, t.null]), /** Risk Probability Level Name. */ - probabilityLevel: t.string, + probabilityLevel: t.union([t.string, t.null]), /** Risk Probability Level ID. */ - probabilityLevelId: t.number, + probabilityLevelId: t.union([t.number, t.null]), /** Risk Score. */ - riskScore: t.number, + riskScore: t.union([t.number, t.null]), }); // ref: https://developer.onetrust.com/onetrust/reference/getriskusingget @@ -595,31 +643,21 @@ export const OneTrustGetRiskResponseCodec = t.type({ /** Name of the Inventory. */ inventoryName: t.string, /** Type of the Inventory. */ - inventoryType: t.literal('ASSETS PROCESSING_ACTIVITIES VENDORS ENTITIES'), + inventoryType: t.union([ + t.literal('ASSETS'), + t.literal('PROCESSING_ACTIVITIES'), + t.literal('VENDORS'), + t.literal('ENTITIES'), + t.null, + ]), /** ID of the Inventory's Organization. */ - organizationId: t.string, + organizationId: t.union([t.string, t.null]), /** The source type */ sourceType: EntityTypeCodec, }), ), /** The attribute values associated with the risk */ - attributeValues: t.type({ - /** List of custom attributes. */ - additionalProp: t.array( - t.type({ - /** Additional information like Source Questions, Approver Ids, Inventory Type. This will be a Map of String Key and Object value. */ - additionalAttributes: t.object, - /** Attribute option GUID. */ - id: t.string, - /** Attribute selection value and it is mandatory if the numeric value is not distinct for Numerical Single Select attribute. */ - optionSelectionValue: t.string, - /** Attribute option value. */ - value: t.string, - /** Attribute option value key for translation. */ - valueKey: t.string, - }), - ), - }), + attributeValues: t.object, /** List of categories. */ categories: t.array( t.type({ @@ -634,37 +672,37 @@ export const OneTrustGetRiskResponseCodec = t.type({ /** List of Control Identifiers. */ controlsIdentifier: t.array(t.string), /** Risk created time. */ - createdUTCDateTime: t.string, + createdUTCDateTime: t.union([t.string, t.null]), /** Risk Creation Type. */ - creationType: t.string, + creationType: t.union([t.string, t.null]), /** Date when the risk is closed. */ - dateClosed: t.string, + dateClosed: t.union([t.string, t.null]), /** Deadline date for the risk. */ - deadline: t.string, + deadline: t.union([t.string, t.null]), /** Risk delete type. */ - deleteType: t.literal('SOFT'), + deleteType: t.union([t.literal('SOFT'), t.null]), /** Risk description. */ - description: t.string, + description: t.union([t.string, t.null]), /** ID of the risk. */ id: t.string, /** Residual impact level name. */ - impactLevel: t.string, + impactLevel: t.union([t.string, t.null]), /** Residual impact level ID. */ - impactLevelId: t.number, + impactLevelId: t.union([t.number, t.null]), /** The inherent risk level */ inherentRiskLevel: RiskLevelCodec, /** The risk justification */ - justification: t.string, + justification: t.union([t.string, t.null]), /** Residual level display name. */ - levelDisplayName: t.string, + levelDisplayName: t.union([t.string, t.null]), /** Residual level ID. */ - levelId: t.number, + levelId: t.union([t.number, t.null]), /** Risk mitigated date. */ - mitigatedDate: t.string, + mitigatedDate: t.union([t.string, t.null]), /** Risk Mitigation details. */ - mitigation: t.string, + mitigation: t.union([t.string, t.null]), /** Short Name for a Risk. */ - name: t.string, + name: t.union([t.string, t.null]), /** Integer risk identifier. */ number: t.number, /** The organization group */ @@ -684,19 +722,20 @@ export const OneTrustGetRiskResponseCodec = t.type({ t.literal('REDUCED'), t.literal('RETAINED'), t.literal('ARCHIVED_IN_VERSION'), + t.null, ]), /** Residual probability level. */ - probabilityLevel: t.string, + probabilityLevel: t.union([t.string, t.null]), /** Residual probability level ID. */ - probabilityLevelId: t.number, + probabilityLevelId: t.union([t.number, t.null]), /** Risk Recommendation. */ - recommendation: t.string, + recommendation: t.union([t.string, t.null]), /** Proposed remediation. */ - remediationProposal: t.string, + remediationProposal: t.union([t.string, t.null]), /** Deadline reminder days. */ - reminderDays: t.number, + reminderDays: t.union([t.number, t.null]), /** Risk exception request. */ - requestedException: t.string, + requestedException: t.union([t.string, t.null]), /** Risk Result. */ result: t.union([ t.literal('Accepted'), @@ -705,23 +744,24 @@ export const OneTrustGetRiskResponseCodec = t.type({ t.literal('Rejected'), t.literal('Transferred'), t.literal('Ignored'), + t.null, ]), /** Risk approvers name csv. */ - riskApprovers: t.string, + riskApprovers: t.union([t.string, t.null]), /** Risk approvers ID. */ riskApproversId: t.array(t.string), /** List of risk owners ID. */ - riskOwnersId: t.array(t.string), + riskOwnersId: t.union([t.array(t.string), t.null]), /** Risk owners name csv. */ - riskOwnersName: t.string, + riskOwnersName: t.union([t.string, t.null]), /** Risk score. */ - riskScore: t.number, + riskScore: t.union([t.number, t.null]), /** The risk source type */ riskSourceType: EntityTypeCodec, /** The risk type */ riskType: EntityTypeCodec, /** For Auto risk, rule Id reference. */ - ruleRootVersionId: t.string, + ruleRootVersionId: t.union([t.string, t.null]), /** The risk source */ source: t.type({ /** Additional information about the Source Entity */ @@ -765,16 +805,19 @@ export const OneTrustGetRiskResponseCodec = t.type({ /** The target risk level */ targetRiskLevel: RiskLevelCodec, /** The risk threat */ - threat: t.type({ - /** Threat ID. */ - id: t.string, - /** Threat Identifier. */ - identifier: t.string, - /** Threat Name. */ - name: t.string, - }), + threat: t.union([ + t.type({ + /** Threat ID. */ + id: t.string, + /** Threat Identifier. */ + identifier: t.string, + /** Threat Name. */ + name: t.string, + }), + t.null, + ]), /** Risk Treatment. */ - treatment: t.string, + treatment: t.union([t.string, t.null]), /** Risk Treatment status. */ treatmentStatus: t.union([ t.literal('InProgress'), @@ -782,6 +825,7 @@ export const OneTrustGetRiskResponseCodec = t.type({ t.literal('ExceptionRequested'), t.literal('Approved'), t.literal('ExceptionGranted'), + t.null, ]), /** Risk Type. */ type: t.union([ @@ -791,20 +835,25 @@ export const OneTrustGetRiskResponseCodec = t.type({ t.literal('VENDORS'), t.literal('ENTITIES'), t.literal('INCIDENTS'), + t.literal('ENGAGEMENTS'), + t.null, ]), /** ID of an assessment. */ typeRefIds: t.array(t.string), /** List of vulnerabilities */ - vulnerabilities: t.array( - t.type({ - /** Vulnerability ID. */ - id: t.string, - /** Vulnerability Identifier. */ - identifier: t.string, - /** Vulnerability Name. */ - name: t.string, - }), - ), + vulnerabilities: t.union([ + t.array( + t.type({ + /** Vulnerability ID. */ + id: t.string, + /** Vulnerability Identifier. */ + identifier: t.string, + /** Vulnerability Name. */ + name: t.string, + }), + ), + t.null, + ]), /** The risk workflow */ workflow: t.type({ /** ID of an entity. */ @@ -819,30 +868,25 @@ export type OneTrustGetRiskResponseCodec = t.TypeOf< typeof OneTrustGetRiskResponseCodec >; -export const OneTrustRiskDetailsCodec = t.type({ - /** Risk description. */ - description: OneTrustGetRiskResponseCodec.props.description, - /** Short Name for a Risk. */ - name: OneTrustGetRiskResponseCodec.props.name, - /** Risk Treatment. */ - treatment: OneTrustGetRiskResponseCodec.props.treatment, - /** Risk Treatment status. */ - treatmentStatus: OneTrustGetRiskResponseCodec.props.treatmentStatus, - /** Risk Type. */ - type: OneTrustGetRiskResponseCodec.props.type, - /** The risk stage */ - stage: OneTrustGetRiskResponseCodec.props.stage, - /** The risk state */ - state: OneTrustGetRiskResponseCodec.props.state, - /** Risk Result. */ - result: OneTrustGetRiskResponseCodec.props.result, - /** List of categories. */ - categories: OneTrustGetRiskResponseCodec.props.categories, -}); +// TODO: do not move to privacy-types +export const OneTrustEnrichedRiskCodec = t.intersection([ + OneTrustAssessmentQuestionRiskCodec, + t.type({ + description: OneTrustGetRiskResponseCodec.props.description, + name: OneTrustGetRiskResponseCodec.props.name, + treatment: OneTrustGetRiskResponseCodec.props.treatment, + treatmentStatus: OneTrustGetRiskResponseCodec.props.treatmentStatus, + type: OneTrustGetRiskResponseCodec.props.type, + stage: OneTrustGetRiskResponseCodec.props.stage, + state: OneTrustGetRiskResponseCodec.props.state, + result: OneTrustGetRiskResponseCodec.props.result, + categories: OneTrustGetRiskResponseCodec.props.categories, + }), +]); /** Type override */ -export type OneTrustRiskDetailsCodec = t.TypeOf< - typeof OneTrustRiskDetailsCodec +export type OneTrustEnrichedRiskCodec = t.TypeOf< + typeof OneTrustEnrichedRiskCodec >; /* eslint-enable max-lines */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index ab83b114..acd4d828 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -1,14 +1,14 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/no-explicit-any */ - import { OneTrustAssessmentCodec, OneTrustAssessmentQuestionCodec, - OneTrustAssessmentQuestionResponsesCodec, - OneTrustAssessmentQuestionRiskCodec, + // OneTrustAssessmentQuestionResponsesCodec, OneTrustAssessmentSectionCodec, + OneTrustAssessmentSectionHeaderCodec, + OneTrustEnrichedRiskCodec, + OneTrustFlatAssessmentSectionCodec, OneTrustGetAssessmentResponseCodec, - OneTrustRiskDetailsCodec, } from './codecs'; // TODO: test what happens when a value is null -> it should convert to '' @@ -55,109 +55,149 @@ const flattenList = (list: any[], prefix: string): any => { }, {} as Record); }; -const flattenOneTrustQuestionResponses = ( - questionResponses: OneTrustAssessmentQuestionResponsesCodec[], - prefix: string, -): any => { - if (questionResponses.length === 0) { - return {}; - } - - // despite being an array, questionResponses only returns one element - const { responses, ...rest } = questionResponses[0]; - return { - ...flattenList(responses, prefix), - ...flattenObject(rest, prefix), - }; -}; +// const flattenOneTrustQuestionResponses = ( +// questionResponses: OneTrustAssessmentQuestionResponsesCodec[], +// prefix: string, +// ): any => { +// if (questionResponses.length === 0) { +// return {}; +// } + +// // despite being an array, questionResponses only returns one element +// const { responses, ...rest } = questionResponses[0]; +// return { +// ...flattenList(responses, prefix), +// ...flattenObject(rest, prefix), +// }; +// }; + +// const flattenOneTrustQuestion = ( +// oneTrustQuestion: OneTrustAssessmentQuestionCodec, +// prefix: string, +// ): any => { +// const { +// question: { options: questionOptions, ...restQuestion }, +// questionResponses, +// // risks, +// ...rest +// } = oneTrustQuestion; +// const newPrefix = `${prefix}_${restQuestion.sequence}`; + +// return { +// ...flattenObject({ ...restQuestion, ...rest }, newPrefix), +// ...flattenList(questionOptions ?? [], `${newPrefix}_options`), +// ...flattenOneTrustQuestionResponses( +// questionResponses ?? [], +// `${newPrefix}_responses`, +// ), +// }; +// }; + +// const flattenOneTrustQuestions = ( +// questions: OneTrustAssessmentQuestionCodec[], +// prefix: string, +// ): any => +// questions.reduce( +// (acc, question) => ({ +// ...acc, +// ...flattenOneTrustQuestion(question, prefix), +// }), +// {}, +// ); + +// const flattenOneTrustSection = ( +// section: OneTrustAssessmentSectionCodec, +// ): any => { +// const { questions, header, ...rest } = section; + +// // the flattened section key has format like sections_${sequence}_sectionId +// const prefix = `sections_${section.sequence}`; +// return { +// ...flattenObject({ ...header, ...rest }, prefix), +// ...flattenOneTrustQuestions(questions, `${prefix}_questions`), +// }; +// }; + +// const flattenOneTrustSections = ( +// sections: OneTrustAssessmentSectionCodec[], +// ): any => +// sections.reduce( +// (acc, section) => ({ ...acc, ...flattenOneTrustSection(section) }), +// {}, +// ); -const flattenOneTrustQuestion = ( - oneTrustQuestion: OneTrustAssessmentQuestionCodec, +const flattenOneTrustSections = ( + sections: OneTrustAssessmentSectionCodec[], prefix: string, ): any => { const { - question: { options: questionOptions, ...restQuestion }, - questionResponses, - // risks, - ...rest - } = oneTrustQuestion; - const newPrefix = `${prefix}_${restQuestion.sequence}`; - - return { - ...flattenObject({ ...restQuestion, ...rest }, newPrefix), - ...flattenList(questionOptions ?? [], `${newPrefix}_options`), - ...flattenOneTrustQuestionResponses( - questionResponses ?? [], - `${newPrefix}_responses`, - ), - }; -}; - -const flattenOneTrustQuestions = ( - questions: OneTrustAssessmentQuestionCodec[], - prefix: string, -): any => - questions.reduce( - (acc, question) => ({ - ...acc, - ...flattenOneTrustQuestion(question, prefix), - }), - {}, + // allQuestions, + // headers, + unnestedSections, + } = sections.reduce<{ + /** The sections questions */ + allQuestions: OneTrustAssessmentQuestionCodec[][]; + /** The sections headers */ + headers: OneTrustAssessmentSectionHeaderCodec[]; + /** The sections */ + unnestedSections: OneTrustFlatAssessmentSectionCodec[]; + }>( + (acc, section) => { + const { questions, header, ...rest } = section; + return { + allQuestions: [...acc.allQuestions, questions], + headers: [...acc.headers, header], + unnestedSections: [...acc.unnestedSections, rest], + }; + }, + { + allQuestions: [], + headers: [], + unnestedSections: [], + }, ); + const flattenedSections = flattenList(unnestedSections, prefix); -const flattenOneTrustSection = ( - section: OneTrustAssessmentSectionCodec, -): any => { - const { questions, header, ...rest } = section; - - // the flattened section key has format like sections_${sequence}_sectionId - const prefix = `sections_${section.sequence}`; - return { - ...flattenObject({ ...header, ...rest }, prefix), - ...flattenOneTrustQuestions(questions, `${prefix}_questions`), - }; + return { ...flattenedSections }; }; -const flattenOneTrustSections = ( - sections: OneTrustAssessmentSectionCodec[], -): any => - sections.reduce( - (acc, section) => ({ ...acc, ...flattenOneTrustSection(section) }), - {}, - ); - -export const flattenOneTrustAssessment = ( - assessment: OneTrustAssessmentCodec & - OneTrustGetAssessmentResponseCodec & { - /** the sections enriched with risk details */ - sections: (OneTrustAssessmentSectionCodec & { - /** the questions enriched with risk details */ - questions: (OneTrustAssessmentQuestionCodec & { - /** the enriched risk details */ - risks: - | (OneTrustAssessmentQuestionRiskCodec & OneTrustRiskDetailsCodec)[] - | null; - })[]; +export const flattenOneTrustAssessment = ({ + assessment, + assessmentDetails, +}: { + /** the assessment */ + assessment: OneTrustAssessmentCodec; + /** the assessment with details */ + assessmentDetails: OneTrustGetAssessmentResponseCodec & { + /** the sections enriched with risk details */ + sections: (OneTrustAssessmentSectionCodec & { + /** the questions enriched with risk details */ + questions: (OneTrustAssessmentQuestionCodec & { + /** the enriched risk details */ + risks: OneTrustEnrichedRiskCodec[] | null; })[]; - }, -): any => { + })[]; + }; +}): any => { const { - approvers, - primaryEntityDetails, - respondents, + // TODO: handle these + // approvers, + // primaryEntityDetails, + // respondents, // eslint-disable-next-line @typescript-eslint/no-unused-vars respondent, sections, ...rest - } = assessment; + } = assessmentDetails; // console.log({ approvers: flattenApprovers(approvers) }); return { + ...flattenObject(assessment), ...flattenObject(rest), - ...flattenList(approvers, 'approvers'), - ...flattenList(primaryEntityDetails, 'primaryEntityDetails'), - ...flattenList(respondents, 'respondents'), - ...flattenOneTrustSections(sections), + // ...flattenList(approvers, 'approvers'), + // ...flattenList(primaryEntityDetails, 'primaryEntityDetails'), + // ...flattenList(respondents, 'respondents'), + ...flattenOneTrustSections(sections, 'sections'), }; }; diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index 66686549..75e5f866 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -85,7 +85,6 @@ export const writeOneTrustAssessment = ({ // combine the two assessments into a single enriched result const enrichedAssessment = { ...restAssessmentDetails, - ...assessment, sections: enrichedSections, }; @@ -115,8 +114,12 @@ export const writeOneTrustAssessment = ({ fs.writeFileSync('./oneTrust.json', '[\n'); } - const flattened = flattenOneTrustAssessment(enrichedAssessment); + const flattened = flattenOneTrustAssessment({ + assessment, + assessmentDetails: enrichedAssessment, + }); const stringifiedFlattened = JSON.stringify(flattened, null, 2); + // TODO: do not forget to ensure we have the same set of keys!!! // const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); From 35bab064c0969911a188ef10b7f33f999b41c774 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 22:48:56 +0000 Subject: [PATCH 07/79] fix codecs --- src/cli-pull-ot.ts | 4 +- src/oneTrust/codecs.ts | 49 ++++++++++++++--------- src/oneTrust/flattenOneTrustAssessment.ts | 6 ++- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index f6ffac0d..535d76e0 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -35,8 +35,8 @@ async function main(): Promise { // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); - // TODO: undo - // const theAssessments = assessments.slice(660); + // // TODO: undo + // const theAssessments = assessments.slice(1896); // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory await mapSeries(assessments, async (assessment, index) => { diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 9d9ad0da..f42dd00c 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -179,26 +179,39 @@ export const OneTrustAssessmentQuestionResponsesCodec = t.type({ /** Indicates whether the response is valid. */ valid: t.boolean, /** The data subject */ - dataSubject: t.type({ - /** The ID of the data subject */ - id: t.union([t.string, t.null]), - /** The ID of the data subject */ - name: t.union([t.string, t.null]), - }), + dataSubject: t.union([ + t.type({ + /** The ID of the data subject */ + id: t.union([t.string, t.null]), + /** The ID of the data subject */ + name: t.union([t.string, t.null]), + /** The nameKey of the data category */ + nameKey: t.union([t.string, t.null]), + }), + t.null, + ]), /** The data category */ - dataCategory: t.type({ - /** The ID of the data category */ - id: t.union([t.string, t.null]), - /** The name of the data category */ - name: t.union([t.string, t.null]), - }), + dataCategory: t.union([ + t.type({ + /** The ID of the data category */ + id: t.union([t.string, t.null]), + /** The name of the data category */ + name: t.union([t.string, t.null]), + /** The nameKey of the data category */ + nameKey: t.union([t.string, t.null]), + }), + t.null, + ]), /** The data element */ - dataElement: t.type({ - /** The ID of the data element */ - id: t.union([t.string, t.null]), - /** The ID of the data element */ - name: t.union([t.string, t.null]), - }), + dataElement: t.union([ + t.type({ + /** The ID of the data element */ + id: t.union([t.string, t.null]), + /** The ID of the data element */ + name: t.union([t.string, t.null]), + }), + t.null, + ]), }), ), /** Justification comments for the given response. */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index acd4d828..7df32a5e 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -132,7 +132,7 @@ const flattenOneTrustSections = ( ): any => { const { // allQuestions, - // headers, + headers, unnestedSections, } = sections.reduce<{ /** The sections questions */ @@ -157,8 +157,10 @@ const flattenOneTrustSections = ( }, ); const flattenedSections = flattenList(unnestedSections, prefix); + // TODO: test + const flattenedHeaders = flattenList(headers, prefix); - return { ...flattenedSections }; + return { ...flattenedSections, ...flattenedHeaders }; }; export const flattenOneTrustAssessment = ({ From 5d9def9874be1c17d6ccebafed00dde98f897e18 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 23:12:13 +0000 Subject: [PATCH 08/79] create flattenOneTrustSectionHeaders helper --- src/cli-pull-ot.ts | 4 +- src/oneTrust/codecs.ts | 54 ++++++++++++++++++----- src/oneTrust/flattenOneTrustAssessment.ts | 45 ++++++++++++++++++- 3 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index 535d76e0..a1279844 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -35,8 +35,8 @@ async function main(): Promise { // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); - // // TODO: undo - // const theAssessments = assessments.slice(1896); + // TODO: undo + // const theAssessments = assessments.slice(1902); // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory await mapSeries(assessments, async (assessment, index) => { diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index f42dd00c..aa2a26b1 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -315,6 +315,23 @@ export type OneTrustAssessmentQuestionCodec = t.TypeOf< typeof OneTrustAssessmentQuestionCodec >; +export const OneTrustAssessmentSectionHeaderRiskStatisticsCodec = t.union([ + t.type({ + /** Maximum level of risk in the section. */ + maxRiskLevel: t.union([t.number, t.null]), + /** Number of risks in the section. */ + riskCount: t.union([t.number, t.null]), + /** ID of the section in the assessment. */ + sectionId: t.union([t.string, t.null]), + }), + t.null, +]); + +/** Type override */ +export type OneTrustAssessmentSectionHeaderRiskStatisticsCodec = t.TypeOf< + typeof OneTrustAssessmentSectionHeaderRiskStatisticsCodec +>; + export const OneTrustAssessmentSectionHeaderCodec = t.type({ /** ID of the section in the assessment. */ sectionId: t.string, @@ -337,17 +354,7 @@ export const OneTrustAssessmentSectionHeaderCodec = t.type({ /** IDs of effectiveness questions in the section. */ effectivenessQuestionIds: t.array(t.string), /** The risk statistics */ - riskStatistics: t.union([ - t.type({ - /** Maximum level of risk in the section. */ - maxRiskLevel: t.number, - /** Number of risks in the section. */ - riskCount: t.number, - /** ID of the section in the assessment. */ - sectionId: t.string, - }), - t.null, - ]), + riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec, /** Whether the section was submitted */ submitted: t.boolean, }); @@ -356,6 +363,31 @@ export type OneTrustAssessmentSectionHeaderCodec = t.TypeOf< typeof OneTrustAssessmentSectionHeaderCodec >; +// TODO: do not add to privacy-types +/** The OneTrustAssessmentSectionHeaderCodec without nested riskStatistics */ +export const OneTrustAssessmentSectionFlatHeaderCodec = t.type({ + sectionId: OneTrustAssessmentSectionHeaderCodec.props.sectionId, + name: OneTrustAssessmentSectionHeaderCodec.props.name, + description: OneTrustAssessmentSectionHeaderCodec.props.description, + sequence: OneTrustAssessmentSectionHeaderCodec.props.sequence, + hidden: OneTrustAssessmentSectionHeaderCodec.props.hidden, + invalidQuestionIds: + OneTrustAssessmentSectionHeaderCodec.props.invalidQuestionIds, + requiredUnansweredQuestionIds: + OneTrustAssessmentSectionHeaderCodec.props.requiredUnansweredQuestionIds, + requiredQuestionIds: + OneTrustAssessmentSectionHeaderCodec.props.requiredQuestionIds, + unansweredQuestionIds: + OneTrustAssessmentSectionHeaderCodec.props.unansweredQuestionIds, + effectivenessQuestionIds: + OneTrustAssessmentSectionHeaderCodec.props.effectivenessQuestionIds, + submitted: OneTrustAssessmentSectionHeaderCodec.props.submitted, +}); +/** Type override */ +export type OneTrustAssessmentSectionFlatHeaderCodec = t.TypeOf< + typeof OneTrustAssessmentSectionFlatHeaderCodec +>; + export const OneTrustAssessmentSectionCodec = t.type({ /** The Assessment section header */ header: OneTrustAssessmentSectionHeaderCodec, diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 7df32a5e..57b68ab8 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -5,7 +5,9 @@ import { OneTrustAssessmentQuestionCodec, // OneTrustAssessmentQuestionResponsesCodec, OneTrustAssessmentSectionCodec, + OneTrustAssessmentSectionFlatHeaderCodec, OneTrustAssessmentSectionHeaderCodec, + OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustEnrichedRiskCodec, OneTrustFlatAssessmentSectionCodec, OneTrustGetAssessmentResponseCodec, @@ -126,6 +128,46 @@ const flattenList = (list: any[], prefix: string): any => { // {}, // ); +const flattenOneTrustSectionHeaders = ( + headers: OneTrustAssessmentSectionHeaderCodec[], + prefix: string, +): any => { + // TODO: do this for EVERY nested object that may be null + const defaultRiskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec = + { + maxRiskLevel: null, + riskCount: null, + sectionId: null, + }; + + const { riskStatistics, flatHeaders } = headers.reduce<{ + /** The risk statistics of all headers */ + riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec[]; + /** The headers without risk statistics */ + flatHeaders: OneTrustAssessmentSectionFlatHeaderCodec[]; + }>( + (acc, header) => { + const { riskStatistics, ...rest } = header; + return { + riskStatistics: [ + ...acc.riskStatistics, + riskStatistics ?? defaultRiskStatistics, + ], + flatHeaders: [...acc.flatHeaders, rest], + }; + }, + { + riskStatistics: [], + flatHeaders: [], + }, + ); + + return { + ...flattenList(flatHeaders, prefix), + ...flattenList(riskStatistics, `${prefix}_riskStatistics`), + }; +}; + const flattenOneTrustSections = ( sections: OneTrustAssessmentSectionCodec[], prefix: string, @@ -157,8 +199,7 @@ const flattenOneTrustSections = ( }, ); const flattenedSections = flattenList(unnestedSections, prefix); - // TODO: test - const flattenedHeaders = flattenList(headers, prefix); + const flattenedHeaders = flattenOneTrustSectionHeaders(headers, prefix); return { ...flattenedSections, ...flattenedHeaders }; }; From 4822fba49a808ddbe51d46f9c519d23ba4955da6 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 23:52:18 +0000 Subject: [PATCH 09/79] improve codecs and create flattenOneTrustQuestions helper --- src/cli-pull-ot.ts | 3 - src/oneTrust/codecs.ts | 155 +++++++++++++--------- src/oneTrust/flattenOneTrustAssessment.ts | 139 ++++++++++--------- 3 files changed, 168 insertions(+), 129 deletions(-) diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index a1279844..1116a817 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -35,9 +35,6 @@ async function main(): Promise { // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); - // TODO: undo - // const theAssessments = assessments.slice(1902); - // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory await mapSeries(assessments, async (assessment, index) => { logger.info( diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index aa2a26b1..cdddcf34 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -142,7 +142,7 @@ export type OneTrustAssessmentQuestionRiskCodec = t.TypeOf< typeof OneTrustAssessmentQuestionRiskCodec >; -export const OneTrustAssessmentQuestionResponsesCodec = t.type({ +export const OneTrustAssessmentQuestionResponseCodec = t.type({ /** The responses */ responses: t.array( t.type({ @@ -219,73 +219,80 @@ export const OneTrustAssessmentQuestionResponsesCodec = t.type({ }); /** Type override */ -export type OneTrustAssessmentQuestionResponsesCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionResponsesCodec +export type OneTrustAssessmentQuestionResponseCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionResponseCodec +>; + +export const OneTrustAssessmentNestedQuestionCodec = t.type({ + /** ID of the question. */ + id: t.string, + /** ID of the root version of the question. */ + rootVersionId: t.string, + /** Order in which the question appears in the assessment. */ + sequence: t.number, + /** Type of question in the assessment. */ + questionType: t.union([ + t.literal('TEXTBOX'), + t.literal('MULTICHOICE'), + t.literal('YESNO'), + t.literal('DATE'), + t.literal('STATEMENT'), + t.literal('INVENTORY'), + t.literal('ATTRIBUTE'), + t.literal('PERSONAL_DATA'), + t.literal('ENGAGEMENT'), + t.literal('ASSESS_CONTROL'), + t.null, + ]), + /** Indicates whether a response to the question is required. */ + required: t.boolean, + /** Data element attributes that are directly updated by the question. */ + attributes: t.string, + /** Short, descriptive name for the question. */ + friendlyName: t.union([t.string, t.null]), + /** Description of the question. */ + description: t.union([t.string, t.null]), + /** Tooltip text within a hint for the question. */ + hint: t.union([t.string, t.null]), + /** ID of the parent question. */ + parentQuestionId: t.union([t.string, t.null]), + /** Indicates whether the response to the question is prepopulated. */ + prePopulateResponse: t.boolean, + /** Indicates whether the assessment is linked to inventory records. */ + linkAssessmentToInventory: t.boolean, + /** The question options */ + options: t.union([t.array(OneTrustAssessmentQuestionOptionCodec), t.null]), + /** Indicates whether the question is valid. */ + valid: t.boolean, + /** Type of question in the assessment. */ + type: t.union([ + t.literal('TEXTBOX'), + t.literal('MULTICHOICE'), + t.literal('YESNO'), + t.literal('DATE'), + t.literal('STATEMENT'), + t.literal('INVENTORY'), + t.literal('ATTRIBUTE'), + t.literal('PERSONAL_DATA'), + t.literal('ENGAGEMENT'), + t.literal('ASSESS_CONTROL'), + ]), + /** Whether the response can be multi select */ + allowMultiSelect: t.boolean, + /** The text of a question. */ + content: t.string, + /** Indicates whether justification comments are required for the question. */ + requireJustification: t.boolean, +}); + +/** Type override */ +export type OneTrustAssessmentNestedQuestionCodec = t.TypeOf< + typeof OneTrustAssessmentNestedQuestionCodec >; export const OneTrustAssessmentQuestionCodec = t.type({ /** The question */ - question: t.type({ - /** ID of the question. */ - id: t.string, - /** ID of the root version of the question. */ - rootVersionId: t.string, - /** Order in which the question appears in the assessment. */ - sequence: t.number, - /** Type of question in the assessment. */ - questionType: t.union([ - t.literal('TEXTBOX'), - t.literal('MULTICHOICE'), - t.literal('YESNO'), - t.literal('DATE'), - t.literal('STATEMENT'), - t.literal('INVENTORY'), - t.literal('ATTRIBUTE'), - t.literal('PERSONAL_DATA'), - t.literal('ENGAGEMENT'), - t.literal('ASSESS_CONTROL'), - t.null, - ]), - /** Indicates whether a response to the question is required. */ - required: t.boolean, - /** Data element attributes that are directly updated by the question. */ - attributes: t.string, - /** Short, descriptive name for the question. */ - friendlyName: t.union([t.string, t.null]), - /** Description of the question. */ - description: t.union([t.string, t.null]), - /** Tooltip text within a hint for the question. */ - hint: t.union([t.string, t.null]), - /** ID of the parent question. */ - parentQuestionId: t.union([t.string, t.null]), - /** Indicates whether the response to the question is prepopulated. */ - prePopulateResponse: t.boolean, - /** Indicates whether the assessment is linked to inventory records. */ - linkAssessmentToInventory: t.boolean, - /** The question options */ - options: t.union([t.array(OneTrustAssessmentQuestionOptionCodec), t.null]), - /** Indicates whether the question is valid. */ - valid: t.boolean, - /** Type of question in the assessment. */ - type: t.union([ - t.literal('TEXTBOX'), - t.literal('MULTICHOICE'), - t.literal('YESNO'), - t.literal('DATE'), - t.literal('STATEMENT'), - t.literal('INVENTORY'), - t.literal('ATTRIBUTE'), - t.literal('PERSONAL_DATA'), - t.literal('ENGAGEMENT'), - t.literal('ASSESS_CONTROL'), - ]), - /** Whether the response can be multi select */ - allowMultiSelect: t.boolean, - /** The text of a question. */ - content: t.string, - /** Indicates whether justification comments are required for the question. */ - requireJustification: t.boolean, - }), + question: OneTrustAssessmentNestedQuestionCodec, /** Indicates whether the question is hidden on the assessment. */ hidden: t.boolean, /** Reason for locking the question in the assessment. */ @@ -299,7 +306,7 @@ export const OneTrustAssessmentQuestionCodec = t.type({ /** Indicates whether navigation rules are enabled for the question. */ hasNavigationRules: t.boolean, /** The responses to this question */ - questionResponses: t.array(OneTrustAssessmentQuestionResponsesCodec), + questionResponses: t.array(OneTrustAssessmentQuestionResponseCodec), /** The risks associated with this question */ risks: t.union([t.array(OneTrustAssessmentQuestionRiskCodec), t.null]), /** List of IDs associated with the question root requests. */ @@ -315,6 +322,24 @@ export type OneTrustAssessmentQuestionCodec = t.TypeOf< typeof OneTrustAssessmentQuestionCodec >; +// TODO: do not add to privacy types +// The OneTrustAssessmentQuestionCodec without nested properties +export const OneTrustAssessmentQuestionFlatCodec = t.type({ + hidden: OneTrustAssessmentQuestionCodec.props.hidden, + lockReason: OneTrustAssessmentQuestionCodec.props.lockReason, + copyErrors: OneTrustAssessmentQuestionCodec.props.copyErrors, + hasNavigationRules: OneTrustAssessmentQuestionCodec.props.hasNavigationRules, + rootRequestInformationIds: + OneTrustAssessmentQuestionCodec.props.rootRequestInformationIds, + totalAttachments: OneTrustAssessmentQuestionCodec.props.totalAttachments, + attachmentIds: OneTrustAssessmentQuestionCodec.props.attachmentIds, +}); + +/** Type override */ +export type OneTrustAssessmentQuestionFlatCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionFlatCodec +>; + export const OneTrustAssessmentSectionHeaderRiskStatisticsCodec = t.union([ t.type({ /** Maximum level of risk in the section. */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 57b68ab8..fdf1148b 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -2,8 +2,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { OneTrustAssessmentCodec, + OneTrustAssessmentNestedQuestionCodec, OneTrustAssessmentQuestionCodec, - // OneTrustAssessmentQuestionResponsesCodec, + OneTrustAssessmentQuestionFlatCodec, + OneTrustAssessmentQuestionResponseCodec, + OneTrustAssessmentQuestionRiskCodec, OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionFlatHeaderCodec, OneTrustAssessmentSectionHeaderCodec, @@ -45,6 +48,7 @@ const flattenList = (list: any[], prefix: string): any => { const flattenedList = list.map((obj) => flattenObject(obj, prefix)); // get all possible keys from the flattenedList + // TODO: make helper const allKeys = Array.from( new Set(flattenedList.flatMap((a) => Object.keys(a))), ); @@ -58,7 +62,7 @@ const flattenList = (list: any[], prefix: string): any => { }; // const flattenOneTrustQuestionResponses = ( -// questionResponses: OneTrustAssessmentQuestionResponsesCodec[], +// questionResponses: OneTrustAssessmentQuestionResponseCodec[], // prefix: string, // ): any => { // if (questionResponses.length === 0) { @@ -73,60 +77,74 @@ const flattenList = (list: any[], prefix: string): any => { // }; // }; -// const flattenOneTrustQuestion = ( -// oneTrustQuestion: OneTrustAssessmentQuestionCodec, -// prefix: string, -// ): any => { -// const { -// question: { options: questionOptions, ...restQuestion }, -// questionResponses, -// // risks, -// ...rest -// } = oneTrustQuestion; -// const newPrefix = `${prefix}_${restQuestion.sequence}`; - -// return { -// ...flattenObject({ ...restQuestion, ...rest }, newPrefix), -// ...flattenList(questionOptions ?? [], `${newPrefix}_options`), -// ...flattenOneTrustQuestionResponses( -// questionResponses ?? [], -// `${newPrefix}_responses`, -// ), -// }; -// }; +const flattenOneTrustQuestions = ( + allSectionQuestions: OneTrustAssessmentQuestionCodec[][], + prefix: string, +): any => { + // each entry of sectionQuestions is the list of questions of one section + const allSectionQuestionsFlat = allSectionQuestions.map( + (sectionQuestions) => { + // extract nested properties (TODO: try to make a helper for this!!!) + const { + // TODO: flatten the questions, allQuestionResponses, and risks too! + // questions, + // allQuestionResponses, + // allRisks, + unnestedSectionQuestions, + } = sectionQuestions.reduce<{ + /** The nested questions */ + questions: OneTrustAssessmentNestedQuestionCodec[]; + /** The responses of all questions in the section */ + allQuestionResponses: OneTrustAssessmentQuestionResponseCodec[][]; + /** The risks of all questions in the section */ + allRisks: OneTrustAssessmentQuestionRiskCodec[][]; + /** The parent questions without nested questions */ + unnestedSectionQuestions: OneTrustAssessmentQuestionFlatCodec[]; + }>( + (acc, sectionQuestion) => { + const { question, questionResponses, risks, ...rest } = + sectionQuestion; + return { + questions: [...acc.questions, question], + allQuestionResponses: [ + ...acc.allQuestionResponses, + questionResponses, + ], + allRisks: [...acc.allRisks, risks ?? []], + unnestedSectionQuestions: [...acc.unnestedSectionQuestions, rest], + }; + }, + { + questions: [], + allQuestionResponses: [], + allRisks: [], + unnestedSectionQuestions: [], + }, + ); -// const flattenOneTrustQuestions = ( -// questions: OneTrustAssessmentQuestionCodec[], -// prefix: string, -// ): any => -// questions.reduce( -// (acc, question) => ({ -// ...acc, -// ...flattenOneTrustQuestion(question, prefix), -// }), -// {}, -// ); - -// const flattenOneTrustSection = ( -// section: OneTrustAssessmentSectionCodec, -// ): any => { -// const { questions, header, ...rest } = section; + return flattenList(unnestedSectionQuestions, prefix); + }, + ); -// // the flattened section key has format like sections_${sequence}_sectionId -// const prefix = `sections_${section.sequence}`; -// return { -// ...flattenObject({ ...header, ...rest }, prefix), -// ...flattenOneTrustQuestions(questions, `${prefix}_questions`), -// }; -// }; + // extract all keys across allSectionQuestionsFlat + const allKeys = Array.from( + new Set(allSectionQuestionsFlat.flatMap((a) => Object.keys(a))), + ); -// const flattenOneTrustSections = ( -// sections: OneTrustAssessmentSectionCodec[], -// ): any => -// sections.reduce( -// (acc, section) => ({ ...acc, ...flattenOneTrustSection(section) }), -// {}, -// ); + // TODO: comment + return allSectionQuestionsFlat.reduce( + (acc, flatSectionQuestions) => + Object.fromEntries( + allKeys.map((key) => [ + key, + `${acc[key] === undefined ? '' : `${acc[key]},`}[${ + flatSectionQuestions[key] ?? '' + }]`, + ]), + ), + {}, + ); +}; const flattenOneTrustSectionHeaders = ( headers: OneTrustAssessmentSectionHeaderCodec[], @@ -172,11 +190,7 @@ const flattenOneTrustSections = ( sections: OneTrustAssessmentSectionCodec[], prefix: string, ): any => { - const { - // allQuestions, - headers, - unnestedSections, - } = sections.reduce<{ + const { allQuestions, headers, unnestedSections } = sections.reduce<{ /** The sections questions */ allQuestions: OneTrustAssessmentQuestionCodec[][]; /** The sections headers */ @@ -200,8 +214,12 @@ const flattenOneTrustSections = ( ); const flattenedSections = flattenList(unnestedSections, prefix); const flattenedHeaders = flattenOneTrustSectionHeaders(headers, prefix); + const flattenedQuestions = flattenOneTrustQuestions( + allQuestions, + `${prefix}_questions`, + ); - return { ...flattenedSections, ...flattenedHeaders }; + return { ...flattenedSections, ...flattenedHeaders, ...flattenedQuestions }; }; export const flattenOneTrustAssessment = ({ @@ -227,7 +245,7 @@ export const flattenOneTrustAssessment = ({ // approvers, // primaryEntityDetails, // respondents, - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslintdisablenextline @typescripteslint/nounusedvars respondent, sections, ...rest @@ -243,7 +261,6 @@ export const flattenOneTrustAssessment = ({ ...flattenOneTrustSections(sections, 'sections'), }; }; - /** * * From 5636c78386bd3adbe447d992f011419f1f057d87 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 10 Jan 2025 23:53:21 +0000 Subject: [PATCH 10/79] improve variable names --- src/oneTrust/flattenOneTrustAssessment.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index fdf1148b..66ee9112 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -45,17 +45,15 @@ const flattenList = (list: any[], prefix: string): any => { if (list.length === 0) { return {}; } - const flattenedList = list.map((obj) => flattenObject(obj, prefix)); + const listFlat = list.map((obj) => flattenObject(obj, prefix)); - // get all possible keys from the flattenedList + // get all possible keys from the listFlat // TODO: make helper - const allKeys = Array.from( - new Set(flattenedList.flatMap((a) => Object.keys(a))), - ); + const allKeys = Array.from(new Set(listFlat.flatMap((a) => Object.keys(a)))); - // build a single object where all the keys contain the respective values of flattenedList + // build a single object where all the keys contain the respective values of listFlat return allKeys.reduce((acc, key) => { - const values = flattenedList.map((a) => a[key] ?? '').join(','); + const values = listFlat.map((a) => a[key] ?? '').join(','); acc[key] = values; return acc; }, {} as Record); @@ -212,14 +210,14 @@ const flattenOneTrustSections = ( unnestedSections: [], }, ); - const flattenedSections = flattenList(unnestedSections, prefix); - const flattenedHeaders = flattenOneTrustSectionHeaders(headers, prefix); - const flattenedQuestions = flattenOneTrustQuestions( + const sectionsFlat = flattenList(unnestedSections, prefix); + const headersFlat = flattenOneTrustSectionHeaders(headers, prefix); + const questionsFlat = flattenOneTrustQuestions( allQuestions, `${prefix}_questions`, ); - return { ...flattenedSections, ...flattenedHeaders, ...flattenedQuestions }; + return { ...sectionsFlat, ...headersFlat, ...questionsFlat }; }; export const flattenOneTrustAssessment = ({ @@ -245,7 +243,7 @@ export const flattenOneTrustAssessment = ({ // approvers, // primaryEntityDetails, // respondents, - // eslintdisablenextline @typescripteslint/nounusedvars + // eslint-disable-next-line @typescript-eslint/no-unused-vars respondent, sections, ...rest From 2ca1f6499fc70d16fc3ff3f11a943e8c49872704 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 16:41:58 +0000 Subject: [PATCH 11/79] create extractProperties helper --- src/helpers/extractProperties.ts | 77 ++++++++++++++++++++ src/helpers/index.ts | 1 + src/oneTrust/codecs.ts | 32 ++++++++ src/oneTrust/flattenOneTrustAssessment.ts | 89 +++++++---------------- 4 files changed, 137 insertions(+), 62 deletions(-) create mode 100644 src/helpers/extractProperties.ts diff --git a/src/helpers/extractProperties.ts b/src/helpers/extractProperties.ts new file mode 100644 index 00000000..54a26e77 --- /dev/null +++ b/src/helpers/extractProperties.ts @@ -0,0 +1,77 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Type that represents the extracted properties from an object type T. + * For each property K from T, creates an array of that property's type. + * Also includes a 'rest' property containing an array of objects with all non-extracted properties. + * + * + * @template T - The source object type + * @template K - The keys to extract from T + * @example + * // Given an array of objects: + * const items = [ + * { id: 1, name: 'John', age: 25, city: 'NY' }, + * { id: 2, name: 'Jane', age: 30, city: 'LA' } + * ]; + * + * // And extracting 'id' and 'name': + * type Result = ExtractedArrayProperties; + * + * // Result will be typed as: + * { + * id: number[]; // [1, 2] + * name: string[]; // ['John', 'Jane'] + * rest: Array<{ // [{ age: 25, city: 'NY' }, { age: 30, city: 'LA' }] + * age: number; + * city: string; + * }>; + * } + */ +type ExtractedArrayProperties = { + [P in K]: Array; +} & { + /** The array of non-extracted properties */ + rest: Array>; +}; + +/** + * Extracts specified properties from an array of objects into separate arrays. + * Also collects all non-extracted properties into a 'rest' array. + * + * @template T - The type of objects in the input array + * @template K - The keys of properties to extract + * @param items - Array of objects to extract properties from + * @param properties - Array of property keys to extract + * @returns An object containing arrays of extracted properties and a rest array + * @example + * const items = [ + * { id: 1, name: 'John', age: 25, city: 'NY' }, + * { id: 2, name: 'Jane', age: 30, city: 'LA' } + * ] + * const result = extractProperties(items, ['id', 'name']); + * // Returns: { id: number[], name: string[], rest: {age: number, city: string}[] } + */ +export const extractProperties = ( + items: T[], + properties: K[], +): ExtractedArrayProperties => + items.reduce((acc, item) => { + const result = { ...acc } as ExtractedArrayProperties; + + properties.forEach((prop) => { + const currentArray = (acc[prop] || []) as T[K][]; + result[prop] = [...currentArray, item[prop]] as any; + }); + + const restObject = {} as Omit; + Object.entries(item).forEach(([key, value]) => { + if (!properties.includes(key as K)) { + (restObject as any)[key] = value; + } + }); + + result.rest = [...(acc.rest || []), restObject]; + + return result; + }, {} as ExtractedArrayProperties); diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 73d30623..07a8fd6a 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -2,3 +2,4 @@ export * from './buildAIIntegrationType'; export * from './buildEnabledRouteType'; export * from './inquirer'; export * from './parseVariablesFromString'; +export * from './extractProperties'; diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index cdddcf34..8ede2c9d 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -290,6 +290,38 @@ export type OneTrustAssessmentNestedQuestionCodec = t.TypeOf< typeof OneTrustAssessmentNestedQuestionCodec >; +// TODO: do not add to privacy-types +/** OneTrustAssessmentNestedQuestionCodec without nested options */ +export const OneTrustAssessmentNestedQuestionFlatCodec = t.type({ + id: OneTrustAssessmentNestedQuestionCodec.props.id, + rootVersionId: OneTrustAssessmentNestedQuestionCodec.props.rootVersionId, + sequence: OneTrustAssessmentNestedQuestionCodec.props.sequence, + questionType: OneTrustAssessmentNestedQuestionCodec.props.questionType, + required: OneTrustAssessmentNestedQuestionCodec.props.required, + attributes: OneTrustAssessmentNestedQuestionCodec.props.attributes, + friendlyName: OneTrustAssessmentNestedQuestionCodec.props.friendlyName, + description: OneTrustAssessmentNestedQuestionCodec.props.description, + hint: OneTrustAssessmentNestedQuestionCodec.props.hint, + parentQuestionId: + OneTrustAssessmentNestedQuestionCodec.props.parentQuestionId, + prePopulateResponse: + OneTrustAssessmentNestedQuestionCodec.props.prePopulateResponse, + linkAssessmentToInventory: + OneTrustAssessmentNestedQuestionCodec.props.linkAssessmentToInventory, + valid: OneTrustAssessmentNestedQuestionCodec.props.valid, + type: OneTrustAssessmentNestedQuestionCodec.props.type, + allowMultiSelect: + OneTrustAssessmentNestedQuestionCodec.props.allowMultiSelect, + content: OneTrustAssessmentNestedQuestionCodec.props.content, + requireJustification: + OneTrustAssessmentNestedQuestionCodec.props.requireJustification, +}); + +/** Type override */ +export type OneTrustAssessmentNestedQuestionFlatCodec = t.TypeOf< + typeof OneTrustAssessmentNestedQuestionFlatCodec +>; + export const OneTrustAssessmentQuestionCodec = t.type({ /** The question */ question: OneTrustAssessmentNestedQuestionCodec, diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 66ee9112..3e1d3792 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -1,18 +1,18 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/no-explicit-any */ +import { extractProperties } from '../helpers'; import { OneTrustAssessmentCodec, - OneTrustAssessmentNestedQuestionCodec, + // OneTrustAssessmentNestedQuestionCodec, OneTrustAssessmentQuestionCodec, - OneTrustAssessmentQuestionFlatCodec, - OneTrustAssessmentQuestionResponseCodec, - OneTrustAssessmentQuestionRiskCodec, + // OneTrustAssessmentQuestionFlatCodec, + // OneTrustAssessmentQuestionResponseCodec, + // OneTrustAssessmentQuestionRiskCodec, OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionFlatHeaderCodec, OneTrustAssessmentSectionHeaderCodec, OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustEnrichedRiskCodec, - OneTrustFlatAssessmentSectionCodec, OneTrustGetAssessmentResponseCodec, } from './codecs'; @@ -75,6 +75,13 @@ const flattenList = (list: any[], prefix: string): any => { // }; // }; +// const flattenOneTrustNestedQuestions = ( +// questions: OneTrustAssessmentNestedQuestionCodec[], +// prefix: string, +// ): any => { +// // extract nested pro +// }; + const flattenOneTrustQuestions = ( allSectionQuestions: OneTrustAssessmentQuestionCodec[][], prefix: string, @@ -84,41 +91,15 @@ const flattenOneTrustQuestions = ( (sectionQuestions) => { // extract nested properties (TODO: try to make a helper for this!!!) const { - // TODO: flatten the questions, allQuestionResponses, and risks too! - // questions, - // allQuestionResponses, - // allRisks, - unnestedSectionQuestions, - } = sectionQuestions.reduce<{ - /** The nested questions */ - questions: OneTrustAssessmentNestedQuestionCodec[]; - /** The responses of all questions in the section */ - allQuestionResponses: OneTrustAssessmentQuestionResponseCodec[][]; - /** The risks of all questions in the section */ - allRisks: OneTrustAssessmentQuestionRiskCodec[][]; - /** The parent questions without nested questions */ - unnestedSectionQuestions: OneTrustAssessmentQuestionFlatCodec[]; - }>( - (acc, sectionQuestion) => { - const { question, questionResponses, risks, ...rest } = - sectionQuestion; - return { - questions: [...acc.questions, question], - allQuestionResponses: [ - ...acc.allQuestionResponses, - questionResponses, - ], - allRisks: [...acc.allRisks, risks ?? []], - unnestedSectionQuestions: [...acc.unnestedSectionQuestions, rest], - }; - }, - { - questions: [], - allQuestionResponses: [], - allRisks: [], - unnestedSectionQuestions: [], - }, - ); + // question: questions, + // questionResponses: allQuestionResponses, + // risks: allRisks, + rest: unnestedSectionQuestions, + } = extractProperties(sectionQuestions, [ + 'question', + 'questionResponses', + 'risks', + ]); return flattenList(unnestedSectionQuestions, prefix); }, @@ -188,28 +169,12 @@ const flattenOneTrustSections = ( sections: OneTrustAssessmentSectionCodec[], prefix: string, ): any => { - const { allQuestions, headers, unnestedSections } = sections.reduce<{ - /** The sections questions */ - allQuestions: OneTrustAssessmentQuestionCodec[][]; - /** The sections headers */ - headers: OneTrustAssessmentSectionHeaderCodec[]; - /** The sections */ - unnestedSections: OneTrustFlatAssessmentSectionCodec[]; - }>( - (acc, section) => { - const { questions, header, ...rest } = section; - return { - allQuestions: [...acc.allQuestions, questions], - headers: [...acc.headers, header], - unnestedSections: [...acc.unnestedSections, rest], - }; - }, - { - allQuestions: [], - headers: [], - unnestedSections: [], - }, - ); + const { + questions: allQuestions, + header: headers, + rest: unnestedSections, + } = extractProperties(sections, ['questions', 'header']); + const sectionsFlat = flattenList(unnestedSections, prefix); const headersFlat = flattenOneTrustSectionHeaders(headers, prefix); const questionsFlat = flattenOneTrustQuestions( From 531a240760b77492ba4f2538e93042bea24f19c8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 17:24:15 +0000 Subject: [PATCH 12/79] implement flattenOneTrustNestedQuestionsOptions --- src/oneTrust/codecs.ts | 6 ++- src/oneTrust/flattenOneTrustAssessment.ts | 60 +++++++++++++++++++---- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 8ede2c9d..a5885c82 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -100,7 +100,7 @@ export type OneTrustGetListOfAssessmentsResponseCodec = t.TypeOf< typeof OneTrustGetListOfAssessmentsResponseCodec >; -const OneTrustAssessmentQuestionOptionCodec = t.type({ +export const OneTrustAssessmentQuestionOptionCodec = t.type({ /** ID of the option. */ id: t.string, /** Name of the option. */ @@ -117,6 +117,10 @@ const OneTrustAssessmentQuestionOptionCodec = t.type({ t.literal('DEFAULT'), ]), }); +/** Type override */ +export type OneTrustAssessmentQuestionOptionCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionOptionCodec +>; export const OneTrustAssessmentQuestionRiskCodec = t.intersection([ t.type({ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 3e1d3792..b60271e6 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -3,8 +3,10 @@ import { extractProperties } from '../helpers'; import { OneTrustAssessmentCodec, + OneTrustAssessmentNestedQuestionCodec, // OneTrustAssessmentNestedQuestionCodec, OneTrustAssessmentQuestionCodec, + OneTrustAssessmentQuestionOptionCodec, // OneTrustAssessmentQuestionFlatCodec, // OneTrustAssessmentQuestionResponseCodec, // OneTrustAssessmentQuestionRiskCodec, @@ -16,6 +18,8 @@ import { OneTrustGetAssessmentResponseCodec, } from './codecs'; +// TODO: will have to use something like csv-stringify + // TODO: test what happens when a value is null -> it should convert to '' const flattenObject = (obj: any, prefix = ''): any => Object.keys(obj).reduce((acc, key) => { @@ -75,23 +79,58 @@ const flattenList = (list: any[], prefix: string): any => { // }; // }; -// const flattenOneTrustNestedQuestions = ( -// questions: OneTrustAssessmentNestedQuestionCodec[], -// prefix: string, -// ): any => { -// // extract nested pro -// }; +const flattenOneTrustNestedQuestionsOptions = ( + allOptions: (OneTrustAssessmentQuestionOptionCodec[] | null)[], + prefix: string, +): any => { + const allOptionsFlat = allOptions.map((options) => + flattenList(options ?? [], prefix), + ); + + // extract all keys across allSectionQuestionsFlat + const allKeys = Array.from( + new Set(allOptionsFlat.flatMap((a) => Object.keys(a))), + ); + + // TODO: comment + return allOptionsFlat.reduce( + (acc, optionsFlat) => + Object.fromEntries( + allKeys.map((key) => [ + key, + `${acc[key] === undefined ? '' : `${acc[key]},`}[${ + optionsFlat[key] ?? '' + }]`, + ]), + ), + {}, + ); +}; + +const flattenOneTrustNestedQuestions = ( + questions: OneTrustAssessmentNestedQuestionCodec[], + prefix: string, +): any => { + // TODO: how do extract properties handle null + const { options: allOptions, rest } = extractProperties(questions, [ + 'options', + ]); + + return { + ...flattenList(rest, prefix), + ...flattenOneTrustNestedQuestionsOptions(allOptions, `${prefix}_options`), + }; +}; const flattenOneTrustQuestions = ( allSectionQuestions: OneTrustAssessmentQuestionCodec[][], prefix: string, ): any => { - // each entry of sectionQuestions is the list of questions of one section const allSectionQuestionsFlat = allSectionQuestions.map( (sectionQuestions) => { // extract nested properties (TODO: try to make a helper for this!!!) const { - // question: questions, + question: questions, // questionResponses: allQuestionResponses, // risks: allRisks, rest: unnestedSectionQuestions, @@ -101,7 +140,10 @@ const flattenOneTrustQuestions = ( 'risks', ]); - return flattenList(unnestedSectionQuestions, prefix); + return { + ...flattenList(unnestedSectionQuestions, prefix), + ...flattenOneTrustNestedQuestions(questions, prefix), + }; }, ); From ab072dffa0fd3d73e2e4a626b91719861938f4ba Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 18:44:45 +0000 Subject: [PATCH 13/79] improve flattenList --- src/oneTrust/flattenOneTrustAssessment.ts | 34 +++++++---------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index b60271e6..bda52e1f 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -46,9 +46,6 @@ const flattenObject = (obj: any, prefix = ''): any => }, {} as Record); const flattenList = (list: any[], prefix: string): any => { - if (list.length === 0) { - return {}; - } const listFlat = list.map((obj) => flattenObject(obj, prefix)); // get all possible keys from the listFlat @@ -92,17 +89,11 @@ const flattenOneTrustNestedQuestionsOptions = ( new Set(allOptionsFlat.flatMap((a) => Object.keys(a))), ); - // TODO: comment - return allOptionsFlat.reduce( - (acc, optionsFlat) => - Object.fromEntries( - allKeys.map((key) => [ - key, - `${acc[key] === undefined ? '' : `${acc[key]},`}[${ - optionsFlat[key] ?? '' - }]`, - ]), - ), + return allKeys.reduce( + (acc, key) => ({ + ...acc, + [key]: allOptionsFlat.map((o) => `[${o[key] ?? ''}]`).join(','), + }), {}, ); }; @@ -153,16 +144,11 @@ const flattenOneTrustQuestions = ( ); // TODO: comment - return allSectionQuestionsFlat.reduce( - (acc, flatSectionQuestions) => - Object.fromEntries( - allKeys.map((key) => [ - key, - `${acc[key] === undefined ? '' : `${acc[key]},`}[${ - flatSectionQuestions[key] ?? '' - }]`, - ]), - ), + return allKeys.reduce( + (acc, key) => ({ + ...acc, + [key]: allSectionQuestionsFlat.map((q) => `[${q[key] ?? ''}]`).join(','), + }), {}, ); }; From edd5c70e148775f83aa276925d4b2cf6832c51a3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 18:48:14 +0000 Subject: [PATCH 14/79] improve flattenList --- src/oneTrust/flattenOneTrustAssessment.ts | 46 +++++++++++++++++------ 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index bda52e1f..6d040366 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -60,6 +60,28 @@ const flattenList = (list: any[], prefix: string): any => { }, {} as Record); }; +// TODO: comment +const aggregateObjects = ({ + objs, + wrap = false, +}: { + /** the objects to aggregate in a single one */ + objs: any[]; + /** whether to wrap the values in a [] */ + wrap: boolean; +}): any => { + const allKeys = Array.from(new Set(objs.flatMap((a) => Object.keys(a)))); + + // build a single object where all the keys contain the respective values of objs + return allKeys.reduce((acc, key) => { + const values = objs + .map((a) => (wrap ? `[${a[key] ?? ''}]` : a[key] ?? '')) + .join(','); + acc[key] = values; + return acc; + }, {} as Record); +}; + // const flattenOneTrustQuestionResponses = ( // questionResponses: OneTrustAssessmentQuestionResponseCodec[], // prefix: string, @@ -84,18 +106,20 @@ const flattenOneTrustNestedQuestionsOptions = ( flattenList(options ?? [], prefix), ); - // extract all keys across allSectionQuestionsFlat - const allKeys = Array.from( - new Set(allOptionsFlat.flatMap((a) => Object.keys(a))), - ); + return aggregateObjects({ objs: allOptionsFlat, wrap: true }); - return allKeys.reduce( - (acc, key) => ({ - ...acc, - [key]: allOptionsFlat.map((o) => `[${o[key] ?? ''}]`).join(','), - }), - {}, - ); + // // extract all keys across allSectionQuestionsFlat + // const allKeys = Array.from( + // new Set(allOptionsFlat.flatMap((a) => Object.keys(a))), + // ); + + // return allKeys.reduce( + // (acc, key) => ({ + // ...acc, + // [key]: allOptionsFlat.map((o) => `[${o[key] ?? ''}]`).join(','), + // }), + // {}, + // ); }; const flattenOneTrustNestedQuestions = ( From 22ae8b1315954c5133d40b7afec2f4a0f8875e3f Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 18:54:47 +0000 Subject: [PATCH 15/79] update flattenOneTrustNestedQuestionsOptions to use aggregateObjects --- src/oneTrust/flattenOneTrustAssessment.ts | 69 ++++++++--------------- 1 file changed, 24 insertions(+), 45 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 6d040366..96477545 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -45,21 +45,6 @@ const flattenObject = (obj: any, prefix = ''): any => return acc; }, {} as Record); -const flattenList = (list: any[], prefix: string): any => { - const listFlat = list.map((obj) => flattenObject(obj, prefix)); - - // get all possible keys from the listFlat - // TODO: make helper - const allKeys = Array.from(new Set(listFlat.flatMap((a) => Object.keys(a)))); - - // build a single object where all the keys contain the respective values of listFlat - return allKeys.reduce((acc, key) => { - const values = listFlat.map((a) => a[key] ?? '').join(','); - acc[key] = values; - return acc; - }, {} as Record); -}; - // TODO: comment const aggregateObjects = ({ objs, @@ -68,7 +53,7 @@ const aggregateObjects = ({ /** the objects to aggregate in a single one */ objs: any[]; /** whether to wrap the values in a [] */ - wrap: boolean; + wrap?: boolean; }): any => { const allKeys = Array.from(new Set(objs.flatMap((a) => Object.keys(a)))); @@ -98,28 +83,31 @@ const aggregateObjects = ({ // }; // }; +const flattenList = (list: any[], prefix: string): any => { + const listFlat = list.map((obj) => flattenObject(obj, prefix)); + + // get all possible keys from the listFlat + // TODO: make helper + const allKeys = Array.from(new Set(listFlat.flatMap((a) => Object.keys(a)))); + + // build a single object where all the keys contain the respective values of listFlat + return allKeys.reduce((acc, key) => { + const values = listFlat.map((a) => a[key] ?? '').join(','); + acc[key] = values; + return acc; + }, {} as Record); +}; + const flattenOneTrustNestedQuestionsOptions = ( allOptions: (OneTrustAssessmentQuestionOptionCodec[] | null)[], prefix: string, ): any => { - const allOptionsFlat = allOptions.map((options) => - flattenList(options ?? [], prefix), - ); + const allOptionsFlat = allOptions.map((options) => { + const flatOptions = (options ?? []).map((o) => flattenObject(o, prefix)); + return aggregateObjects({ objs: flatOptions }); + }); return aggregateObjects({ objs: allOptionsFlat, wrap: true }); - - // // extract all keys across allSectionQuestionsFlat - // const allKeys = Array.from( - // new Set(allOptionsFlat.flatMap((a) => Object.keys(a))), - // ); - - // return allKeys.reduce( - // (acc, key) => ({ - // ...acc, - // [key]: allOptionsFlat.map((o) => `[${o[key] ?? ''}]`).join(','), - // }), - // {}, - // ); }; const flattenOneTrustNestedQuestions = ( @@ -162,19 +150,10 @@ const flattenOneTrustQuestions = ( }, ); - // extract all keys across allSectionQuestionsFlat - const allKeys = Array.from( - new Set(allSectionQuestionsFlat.flatMap((a) => Object.keys(a))), - ); - - // TODO: comment - return allKeys.reduce( - (acc, key) => ({ - ...acc, - [key]: allSectionQuestionsFlat.map((q) => `[${q[key] ?? ''}]`).join(','), - }), - {}, - ); + return aggregateObjects({ + objs: allSectionQuestionsFlat, + wrap: true, + }); }; const flattenOneTrustSectionHeaders = ( From 460f25c5c5cfd3f8fad64f444108f441616aace2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 19:14:49 +0000 Subject: [PATCH 16/79] more changes --- src/oneTrust/flattenOneTrustAssessment.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 96477545..739cf680 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -115,12 +115,14 @@ const flattenOneTrustNestedQuestions = ( prefix: string, ): any => { // TODO: how do extract properties handle null - const { options: allOptions, rest } = extractProperties(questions, [ - 'options', - ]); + const { options: allOptions, rest: restQuestions } = extractProperties( + questions, + ['options'], + ); + const restQuestionsFlat = restQuestions.map((r) => flattenObject(r, prefix)); return { - ...flattenList(rest, prefix), + ...aggregateObjects({ objs: restQuestionsFlat }), ...flattenOneTrustNestedQuestionsOptions(allOptions, `${prefix}_options`), }; }; @@ -136,15 +138,18 @@ const flattenOneTrustQuestions = ( question: questions, // questionResponses: allQuestionResponses, // risks: allRisks, - rest: unnestedSectionQuestions, + rest: restSectionQuestions, } = extractProperties(sectionQuestions, [ 'question', 'questionResponses', 'risks', ]); + const restSectionQuestionsFlat = restSectionQuestions.map((q) => + flattenObject(q, prefix), + ); return { - ...flattenList(unnestedSectionQuestions, prefix), + ...aggregateObjects({ objs: restSectionQuestionsFlat }), ...flattenOneTrustNestedQuestions(questions, prefix), }; }, @@ -190,8 +195,9 @@ const flattenOneTrustSectionHeaders = ( }, ); + const flatFlatHeaders = flatHeaders.map((h) => flattenObject(h, prefix)); return { - ...flattenList(flatHeaders, prefix), + ...aggregateObjects({ objs: flatFlatHeaders }), ...flattenList(riskStatistics, `${prefix}_riskStatistics`), }; }; From ccd64ae745000909c9135133e0b754b58acb7e55 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 19:18:50 +0000 Subject: [PATCH 17/79] update const flattenOneTrustSectionHeaders = ( --- src/oneTrust/flattenOneTrustAssessment.ts | 36 +++++++---------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 739cf680..87bc7834 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -11,9 +11,10 @@ import { // OneTrustAssessmentQuestionResponseCodec, // OneTrustAssessmentQuestionRiskCodec, OneTrustAssessmentSectionCodec, - OneTrustAssessmentSectionFlatHeaderCodec, + // OneTrustAssessmentSectionFlatHeaderCodec, OneTrustAssessmentSectionHeaderCodec, OneTrustAssessmentSectionHeaderRiskStatisticsCodec, + // OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustEnrichedRiskCodec, OneTrustGetAssessmentResponseCodec, } from './codecs'; @@ -165,7 +166,7 @@ const flattenOneTrustSectionHeaders = ( headers: OneTrustAssessmentSectionHeaderCodec[], prefix: string, ): any => { - // TODO: do this for EVERY nested object that may be null + // // TODO: do this for EVERY nested object that may be null const defaultRiskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec = { maxRiskLevel: null, @@ -173,32 +174,17 @@ const flattenOneTrustSectionHeaders = ( sectionId: null, }; - const { riskStatistics, flatHeaders } = headers.reduce<{ - /** The risk statistics of all headers */ - riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec[]; - /** The headers without risk statistics */ - flatHeaders: OneTrustAssessmentSectionFlatHeaderCodec[]; - }>( - (acc, header) => { - const { riskStatistics, ...rest } = header; - return { - riskStatistics: [ - ...acc.riskStatistics, - riskStatistics ?? defaultRiskStatistics, - ], - flatHeaders: [...acc.flatHeaders, rest], - }; - }, - { - riskStatistics: [], - flatHeaders: [], - }, - ); + const { riskStatistics, rest: restHeaders } = extractProperties(headers, [ + 'riskStatistics', + ]); - const flatFlatHeaders = flatHeaders.map((h) => flattenObject(h, prefix)); + const flatFlatHeaders = restHeaders.map((h) => flattenObject(h, prefix)); + const flatRiskStatistics = riskStatistics.map((r) => + flattenObject(r ?? defaultRiskStatistics, `${prefix}_riskStatistics`), + ); return { ...aggregateObjects({ objs: flatFlatHeaders }), - ...flattenList(riskStatistics, `${prefix}_riskStatistics`), + ...aggregateObjects({ objs: flatRiskStatistics }), }; }; From f90fcd5a55ed9927ab9226aa45a775f4f65420ef Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 19:19:52 +0000 Subject: [PATCH 18/79] update const flattenOneTrustSectionHeaders = ( --- src/oneTrust/flattenOneTrustAssessment.ts | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 87bc7834..e4c7ab46 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -84,21 +84,6 @@ const aggregateObjects = ({ // }; // }; -const flattenList = (list: any[], prefix: string): any => { - const listFlat = list.map((obj) => flattenObject(obj, prefix)); - - // get all possible keys from the listFlat - // TODO: make helper - const allKeys = Array.from(new Set(listFlat.flatMap((a) => Object.keys(a)))); - - // build a single object where all the keys contain the respective values of listFlat - return allKeys.reduce((acc, key) => { - const values = listFlat.map((a) => a[key] ?? '').join(','); - acc[key] = values; - return acc; - }, {} as Record); -}; - const flattenOneTrustNestedQuestionsOptions = ( allOptions: (OneTrustAssessmentQuestionOptionCodec[] | null)[], prefix: string, @@ -166,7 +151,7 @@ const flattenOneTrustSectionHeaders = ( headers: OneTrustAssessmentSectionHeaderCodec[], prefix: string, ): any => { - // // TODO: do this for EVERY nested object that may be null + // TODO: set a default for EVERY nested object that may be null const defaultRiskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec = { maxRiskLevel: null, @@ -195,10 +180,11 @@ const flattenOneTrustSections = ( const { questions: allQuestions, header: headers, - rest: unnestedSections, + rest: restSections, } = extractProperties(sections, ['questions', 'header']); - const sectionsFlat = flattenList(unnestedSections, prefix); + const restSectionsFlat = restSections.map((s) => flattenObject(s, prefix)); + const sectionsFlat = aggregateObjects({ objs: restSectionsFlat }); const headersFlat = flattenOneTrustSectionHeaders(headers, prefix); const questionsFlat = flattenOneTrustQuestions( allQuestions, From 3b08994f45f94bd5e05eda2f5ab6e1608351a401 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 21:34:14 +0000 Subject: [PATCH 19/79] commit --- src/oneTrust/flattenOneTrustAssessment.ts | 63 ++++++++++++++++------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index e4c7ab46..4dd861f0 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -7,6 +7,7 @@ import { // OneTrustAssessmentNestedQuestionCodec, OneTrustAssessmentQuestionCodec, OneTrustAssessmentQuestionOptionCodec, + OneTrustAssessmentQuestionResponseCodec, // OneTrustAssessmentQuestionFlatCodec, // OneTrustAssessmentQuestionResponseCodec, // OneTrustAssessmentQuestionRiskCodec, @@ -68,22 +69,6 @@ const aggregateObjects = ({ }, {} as Record); }; -// const flattenOneTrustQuestionResponses = ( -// questionResponses: OneTrustAssessmentQuestionResponseCodec[], -// prefix: string, -// ): any => { -// if (questionResponses.length === 0) { -// return {}; -// } - -// // despite being an array, questionResponses only returns one element -// const { responses, ...rest } = questionResponses[0]; -// return { -// ...flattenList(responses, prefix), -// ...flattenObject(rest, prefix), -// }; -// }; - const flattenOneTrustNestedQuestionsOptions = ( allOptions: (OneTrustAssessmentQuestionOptionCodec[] | null)[], prefix: string, @@ -113,6 +98,41 @@ const flattenOneTrustNestedQuestions = ( }; }; +// flatten questionResponses of every question within a section +const flattenOneTrustQuestionResponses = ( + allQuestionResponses: OneTrustAssessmentQuestionResponseCodec[][], + prefix: string, +): any => { + const allQuestionResponsesFlat = allQuestionResponses.map( + (questionResponses) => { + const { responses, rest: restQuestionResponses } = extractProperties( + questionResponses, + ['responses'], + ); + + // TODO: replace possible null values within responses + // const defaultObject = { + // id: null, + // name: null, + // nameKey: null, + // }; + + // TODO: do we handle it right when empty? + const responsesFlat = (responses ?? []).map((r) => + flattenObject(r, prefix), + ); + const restQuestionResponsesFlat = (restQuestionResponses ?? []).map((q) => + flattenObject(q, prefix), + ); + return { + ...aggregateObjects({ objs: responsesFlat }), + ...aggregateObjects({ objs: restQuestionResponsesFlat }), + }; + }, + ); + return aggregateObjects({ objs: allQuestionResponsesFlat, wrap: true }); +}; + const flattenOneTrustQuestions = ( allSectionQuestions: OneTrustAssessmentQuestionCodec[][], prefix: string, @@ -121,10 +141,10 @@ const flattenOneTrustQuestions = ( (sectionQuestions) => { // extract nested properties (TODO: try to make a helper for this!!!) const { + rest: restSectionQuestions, question: questions, - // questionResponses: allQuestionResponses, + questionResponses: allQuestionResponses, // risks: allRisks, - rest: restSectionQuestions, } = extractProperties(sectionQuestions, [ 'question', 'questionResponses', @@ -134,9 +154,16 @@ const flattenOneTrustQuestions = ( const restSectionQuestionsFlat = restSectionQuestions.map((q) => flattenObject(q, prefix), ); + + const result = flattenOneTrustQuestionResponses( + allQuestionResponses, + `${prefix}_questionResponses`, + ); + return { ...aggregateObjects({ objs: restSectionQuestionsFlat }), ...flattenOneTrustNestedQuestions(questions, prefix), + ...result, }; }, ); From e80c95e21ecee81132dd9f3a165387955533741f Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 21:35:25 +0000 Subject: [PATCH 20/79] commit --- src/oneTrust/flattenOneTrustAssessment.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 4dd861f0..6a8e991f 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -138,7 +138,7 @@ const flattenOneTrustQuestions = ( prefix: string, ): any => { const allSectionQuestionsFlat = allSectionQuestions.map( - (sectionQuestions) => { + (sectionQuestions, i) => { // extract nested properties (TODO: try to make a helper for this!!!) const { rest: restSectionQuestions, @@ -151,6 +151,12 @@ const flattenOneTrustQuestions = ( 'risks', ]); + console.log({ + section: i, + questions: sectionQuestions.length, + allQuestionResponses: allQuestionResponses.map((r) => r.length), + }); + const restSectionQuestionsFlat = restSectionQuestions.map((q) => flattenObject(q, prefix), ); From ec8e77c6139e1a173c79bf3117c26416452aa3de Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 23:07:12 +0000 Subject: [PATCH 21/79] create more helpers and add tests --- src/helpers/createDefaultCodec.ts | 86 +++++++++++++++ src/helpers/enrichWithDefault.ts | 68 ++++++++++++ src/helpers/index.ts | 2 + src/helpers/tests/createDefaultCodec.test.ts | 100 ++++++++++++++++++ src/oneTrust/codecs.ts | 72 +++++++++---- src/oneTrust/flattenOneTrustAssessment.ts | 31 +++--- ...dingPreferenceUpdatesCauseConflict.test.ts | 2 +- 7 files changed, 324 insertions(+), 37 deletions(-) create mode 100644 src/helpers/createDefaultCodec.ts create mode 100644 src/helpers/enrichWithDefault.ts create mode 100644 src/helpers/tests/createDefaultCodec.test.ts diff --git a/src/helpers/createDefaultCodec.ts b/src/helpers/createDefaultCodec.ts new file mode 100644 index 00000000..5d0dc0b6 --- /dev/null +++ b/src/helpers/createDefaultCodec.ts @@ -0,0 +1,86 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as t from 'io-ts'; + +/** + * Creates a default value for an io-ts codec. + * + * @param codec - the codec whose default we want to create + * @returns an object honoring the io-ts codec + */ +export const createDefaultCodec = ( + codec: C, +): t.TypeOf => { + if (codec instanceof t.UnionType) { + // First, look for object types in the union + const objectType = codec.types.find( + (type: any) => + type instanceof t.InterfaceType || + type instanceof t.PartialType || + type instanceof t.IntersectionType, + ); + if (objectType) { + return createDefaultCodec(objectType); + } + + // For unions, null has higher preference as default. Otherwise,first type's default + // If null is one of the union types, it should be the default + const hasNull = codec.types.some( + (type: any) => type instanceof t.NullType || type.name === 'null', + ); + if (hasNull) { + return null as t.TypeOf; + } + + // If no null type found, default to first type + return createDefaultCodec(codec.types[0]); + } + + if (codec instanceof t.InterfaceType || codec instanceof t.PartialType) { + const defaults: Record = {}; + Object.entries(codec.props).forEach(([key, type]) => { + defaults[key] = createDefaultCodec(type as any); + }); + return defaults as t.TypeOf; + } + + if (codec instanceof t.IntersectionType) { + // Merge defaults of all types in the intersection + return codec.types.reduce( + (acc: t.TypeOf, type: any) => ({ + ...acc, + ...createDefaultCodec(type), + }), + {}, + ); + } + + if (codec instanceof t.ArrayType) { + // Check if the array element type is an object type + const elementType = codec.type; + const isObjectType = + elementType instanceof t.InterfaceType || + elementType instanceof t.PartialType || + elementType instanceof t.IntersectionType; + + return ( + isObjectType ? [createDefaultCodec(elementType)] : [] + ) as t.TypeOf; + } + + // Handle primitive and common types + switch (codec.name) { + case 'string': + return '' as t.TypeOf; + case 'number': + return null as t.TypeOf; + case 'boolean': + return null as t.TypeOf; + case 'null': + return null as t.TypeOf; + case 'undefined': + return undefined as t.TypeOf; + default: + return null as t.TypeOf; + } +}; diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts new file mode 100644 index 00000000..4dff89e9 --- /dev/null +++ b/src/helpers/enrichWithDefault.ts @@ -0,0 +1,68 @@ +import * as t from 'io-ts'; +import { + OneTrustAssessmentNestedQuestionCodec, + OneTrustAssessmentQuestionOptionCodec, + OneTrustAssessmentQuestionResponseCodec, + OneTrustAssessmentQuestionResponsesCodec, + OneTrustAssessmentQuestionRiskCodec, + OneTrustAssessmentQuestionRisksCodec, + OneTrustAssessmentSectionCodec, + OneTrustAssessmentSectionSubmittedByCodec, + OneTrustPrimaryEntityDetailsCodec, +} from '../oneTrust/codecs'; +import { createDefaultCodec } from './createDefaultCodec'; + +// TODO: test the shit out of this +const enrichQuestionWithDefault = ({ + options, + ...rest +}: OneTrustAssessmentNestedQuestionCodec): OneTrustAssessmentNestedQuestionCodec => ({ + options: + options === null || options.length === 0 + ? createDefaultCodec(t.array(OneTrustAssessmentQuestionOptionCodec)) + : options, + ...rest, +}); + +// TODO: test the shit out of this +const enrichQuestionResponsesWithDefault = ( + questionResponses: OneTrustAssessmentQuestionResponsesCodec, +): OneTrustAssessmentQuestionResponsesCodec => + questionResponses.length === 0 + ? createDefaultCodec(t.array(OneTrustAssessmentQuestionResponseCodec)) + : questionResponses; + +// TODO: test the shit out of this +const enrichRisksWithDefault = ( + risks: OneTrustAssessmentQuestionRisksCodec, +): OneTrustAssessmentQuestionRisksCodec => + risks === null || risks.length === 0 + ? createDefaultCodec(t.array(OneTrustAssessmentQuestionRiskCodec)) + : risks; + +// TODO: test the shit out of this +export const enrichSectionsWithDefault = ( + sections: OneTrustAssessmentSectionCodec[], +): OneTrustAssessmentSectionCodec[] => + sections.map((s) => ({ + ...s, + questions: s.questions.map((q) => ({ + ...q, + question: enrichQuestionWithDefault(q.question), + questionResponses: enrichQuestionResponsesWithDefault( + q.questionResponses, + ), + risks: enrichRisksWithDefault(q.risks), + })), + submittedBy: + s.submittedBy === null + ? createDefaultCodec(OneTrustAssessmentSectionSubmittedByCodec) + : s.submittedBy, + })); + +export const enrichPrimaryEntityDetailsWithDefault = ( + primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec, +): OneTrustPrimaryEntityDetailsCodec => + primaryEntityDetails.length === 0 + ? createDefaultCodec(OneTrustPrimaryEntityDetailsCodec) + : primaryEntityDetails; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 07a8fd6a..7fcc69d8 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -3,3 +3,5 @@ export * from './buildEnabledRouteType'; export * from './inquirer'; export * from './parseVariablesFromString'; export * from './extractProperties'; +export * from './createDefaultCodec'; +export * from './enrichWithDefault'; diff --git a/src/helpers/tests/createDefaultCodec.test.ts b/src/helpers/tests/createDefaultCodec.test.ts new file mode 100644 index 00000000..2e6c0a46 --- /dev/null +++ b/src/helpers/tests/createDefaultCodec.test.ts @@ -0,0 +1,100 @@ +import * as t from 'io-ts'; +import chai, { expect } from 'chai'; +import deepEqualInAnyOrder from 'deep-equal-in-any-order'; + +import { createDefaultCodec } from '../createDefaultCodec'; + +chai.use(deepEqualInAnyOrder); + +describe('buildDefaultCodec', () => { + it('should correctly build a default codec for null', () => { + const result = createDefaultCodec(t.null); + expect(result).to.equal(null); + }); + + it('should correctly build a default codec for number', () => { + const result = createDefaultCodec(t.number); + expect(result).to.equal(null); + }); + + it('should correctly build a default codec for boolean', () => { + const result = createDefaultCodec(t.boolean); + expect(result).to.equal(null); + }); + + it('should correctly build a default codec for undefined', () => { + const result = createDefaultCodec(t.undefined); + expect(result).to.equal(undefined); + }); + + it('should correctly build a default codec for string', () => { + const result = createDefaultCodec(t.string); + expect(result).to.equal(''); + }); + + it('should correctly build a default codec for a union with null', () => { + const result = createDefaultCodec(t.union([t.string, t.null])); + // should default to null if the union contains null + expect(result).to.equal(null); + }); + + it('should correctly build a default codec for a union with type', () => { + const result = createDefaultCodec( + t.union([t.string, t.null, t.type({ name: t.string })]), + ); + // should default to the type if the union contains a type + expect(result).to.deep.equal({ name: '' }); + }); + + it('should correctly build a default codec for a union without null', () => { + const result = createDefaultCodec(t.union([t.string, t.number])); + // should default to the first value if the union does not contains null + expect(result).to.equal(''); + }); + + it('should correctly build a default codec for an array of object types', () => { + const result = createDefaultCodec( + t.array(t.type({ name: t.string, age: t.number })), + ); + // should default to the first value if the union does not contains null + expect(result).to.deep.equalInAnyOrder([{ name: '', age: null }]); + }); + + it('should correctly build a default codec for an array of object partials', () => { + const result = createDefaultCodec( + t.array(t.partial({ name: t.string, age: t.number })), + ); + // should default to the first value if the union does not contains null + expect(result).to.deep.equalInAnyOrder([{ name: '', age: null }]); + }); + + it('should correctly build a default codec for an array of object intersections', () => { + const result = createDefaultCodec( + t.array( + t.intersection([ + t.partial({ name: t.string, age: t.number }), + t.type({ city: t.string }), + ]), + ), + ); + // should default to the first value if the union does not contains null + expect(result).to.deep.equalInAnyOrder([{ name: '', age: null, city: '' }]); + }); + + it('should correctly build a default codec for an array of strings', () => { + const result = createDefaultCodec(t.array(t.string)); + // should default to the first value if the union does not contains null + expect(result).to.deep.equal([]); + }); + + it('should correctly build a default codec for an intersection', () => { + const result = createDefaultCodec( + t.intersection([ + t.type({ id: t.string, name: t.string }), + t.partial({ age: t.number }), + ]), + ); + // should default to the first value if the union does not contains null + expect(result).to.deep.equalInAnyOrder({ id: '', name: '', age: null }); + }); +}); diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index a5885c82..4f6f34b1 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -146,6 +146,15 @@ export type OneTrustAssessmentQuestionRiskCodec = t.TypeOf< typeof OneTrustAssessmentQuestionRiskCodec >; +export const OneTrustAssessmentQuestionRisksCodec = t.union([ + t.array(OneTrustAssessmentQuestionRiskCodec), + t.null, +]); +/** Type override */ +export type OneTrustAssessmentQuestionRisksCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionRisksCodec +>; + export const OneTrustAssessmentQuestionResponseCodec = t.type({ /** The responses */ responses: t.array( @@ -227,6 +236,14 @@ export type OneTrustAssessmentQuestionResponseCodec = t.TypeOf< typeof OneTrustAssessmentQuestionResponseCodec >; +export const OneTrustAssessmentQuestionResponsesCodec = t.array( + OneTrustAssessmentQuestionResponseCodec, +); +/** Type override */ +export type OneTrustAssessmentQuestionResponsesCodec = t.TypeOf< + typeof OneTrustAssessmentQuestionResponsesCodec +>; + export const OneTrustAssessmentNestedQuestionCodec = t.type({ /** ID of the question. */ id: t.string, @@ -449,6 +466,21 @@ export type OneTrustAssessmentSectionFlatHeaderCodec = t.TypeOf< typeof OneTrustAssessmentSectionFlatHeaderCodec >; +export const OneTrustAssessmentSectionSubmittedByCodec = t.union([ + t.type({ + /** The ID of the user who submitted the section */ + id: t.string, + /** THe name or email of the user who submitted the section */ + name: t.string, + }), + t.null, +]); + +/** Type override */ +export type OneTrustAssessmentSectionSubmittedByCodec = t.TypeOf< + typeof OneTrustAssessmentSectionSubmittedByCodec +>; + export const OneTrustAssessmentSectionCodec = t.type({ /** The Assessment section header */ header: OneTrustAssessmentSectionHeaderCodec, @@ -457,15 +489,7 @@ export const OneTrustAssessmentSectionCodec = t.type({ /** Indicates whether navigation rules are enabled for the question. */ hasNavigationRules: t.boolean, /** Who submitted the section */ - submittedBy: t.union([ - t.type({ - /** The ID of the user who submitted the section */ - id: t.string, - /** THe name or email of the user who submitted the section */ - name: t.string, - }), - t.null, - ]), + submittedBy: OneTrustAssessmentSectionSubmittedByCodec, /** Date of the submission */ submittedDt: t.union([t.string, t.null]), /** Name of the section. */ @@ -564,6 +588,23 @@ export type OneTrustAssessmentStatusCodec = t.TypeOf< typeof OneTrustAssessmentStatusCodec >; +export const OneTrustPrimaryEntityDetailsCodec = t.array( + t.type({ + /** Unique ID for the primary record. */ + id: t.string, + /** Name of the primary record. */ + name: t.string, + /** The number associated with the primary record. */ + number: t.number, + /** Name and number of the primary record. */ + displayName: t.string, + }), +); +/** Type override */ +export type OneTrustPrimaryEntityDetailsCodec = t.TypeOf< + typeof OneTrustPrimaryEntityDetailsCodec +>; + // ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget export const OneTrustGetAssessmentResponseCodec = t.type({ /** List of users assigned as approvers of the assessment. */ @@ -611,18 +652,7 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ name: t.string, }), /** The primary record */ - primaryEntityDetails: t.array( - t.type({ - /** Unique ID for the primary record. */ - id: t.string, - /** Name of the primary record. */ - name: t.string, - /** The number associated with the primary record. */ - number: t.number, - /** Name and number of the primary record. */ - displayName: t.string, - }), - ), + primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec, /** Type of inventory record designated as the primary record. */ primaryRecordType: t.union([ t.literal('ASSETS'), diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 6a8e991f..98e5f2c5 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -1,21 +1,19 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/no-explicit-any */ -import { extractProperties } from '../helpers'; +import { + enrichPrimaryEntityDetailsWithDefault, + enrichSectionsWithDefault, + extractProperties, +} from '../helpers'; import { OneTrustAssessmentCodec, OneTrustAssessmentNestedQuestionCodec, - // OneTrustAssessmentNestedQuestionCodec, OneTrustAssessmentQuestionCodec, OneTrustAssessmentQuestionOptionCodec, OneTrustAssessmentQuestionResponseCodec, - // OneTrustAssessmentQuestionFlatCodec, - // OneTrustAssessmentQuestionResponseCodec, - // OneTrustAssessmentQuestionRiskCodec, OneTrustAssessmentSectionCodec, - // OneTrustAssessmentSectionFlatHeaderCodec, OneTrustAssessmentSectionHeaderCodec, OneTrustAssessmentSectionHeaderRiskStatisticsCodec, - // OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustEnrichedRiskCodec, OneTrustGetAssessmentResponseCodec, } from './codecs'; @@ -138,7 +136,7 @@ const flattenOneTrustQuestions = ( prefix: string, ): any => { const allSectionQuestionsFlat = allSectionQuestions.map( - (sectionQuestions, i) => { + (sectionQuestions) => { // extract nested properties (TODO: try to make a helper for this!!!) const { rest: restSectionQuestions, @@ -151,12 +149,6 @@ const flattenOneTrustQuestions = ( 'risks', ]); - console.log({ - section: i, - questions: sectionQuestions.length, - allQuestionResponses: allQuestionResponses.map((r) => r.length), - }); - const restSectionQuestionsFlat = restSectionQuestions.map((q) => flattenObject(q, prefix), ); @@ -245,6 +237,15 @@ export const flattenOneTrustAssessment = ({ })[]; }; }): any => { + // add default values to assessments + const transformedAssessmentDetails = { + ...assessmentDetails, + primaryEntityDetails: enrichPrimaryEntityDetailsWithDefault( + assessmentDetails.primaryEntityDetails, + ), + sections: enrichSectionsWithDefault(assessmentDetails.sections), + }; + const { // TODO: handle these // approvers, @@ -254,7 +255,7 @@ export const flattenOneTrustAssessment = ({ respondent, sections, ...rest - } = assessmentDetails; + } = transformedAssessmentDetails; // console.log({ approvers: flattenApprovers(approvers) }); return { diff --git a/src/preference-management/tests/checkIfPendingPreferenceUpdatesCauseConflict.test.ts b/src/preference-management/tests/checkIfPendingPreferenceUpdatesCauseConflict.test.ts index bbc109c7..2cfc27b9 100644 --- a/src/preference-management/tests/checkIfPendingPreferenceUpdatesCauseConflict.test.ts +++ b/src/preference-management/tests/checkIfPendingPreferenceUpdatesCauseConflict.test.ts @@ -75,7 +75,7 @@ const PREFERENCE_TOPICS: PreferenceTopic[] = [ }, ]; -describe.only('checkIfPendingPreferenceUpdatesCauseConflict', () => { +describe('checkIfPendingPreferenceUpdatesCauseConflict', () => { it('should return false for simple purpose comparison', () => { expect( checkIfPendingPreferenceUpdatesCauseConflict({ From 35d5fd724b04e8f2bb4e078c69ee1d1ae2ec84b4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 23:10:28 +0000 Subject: [PATCH 22/79] create enrichRiskStatisticsWithDefault --- src/helpers/enrichWithDefault.ts | 13 +++++++++++++ src/oneTrust/flattenOneTrustAssessment.ts | 18 +----------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts index 4dff89e9..2f0fb4b4 100644 --- a/src/helpers/enrichWithDefault.ts +++ b/src/helpers/enrichWithDefault.ts @@ -7,6 +7,7 @@ import { OneTrustAssessmentQuestionRiskCodec, OneTrustAssessmentQuestionRisksCodec, OneTrustAssessmentSectionCodec, + OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustAssessmentSectionSubmittedByCodec, OneTrustPrimaryEntityDetailsCodec, } from '../oneTrust/codecs'; @@ -40,12 +41,24 @@ const enrichRisksWithDefault = ( ? createDefaultCodec(t.array(OneTrustAssessmentQuestionRiskCodec)) : risks; +// TODO: test the shit out of this +const enrichRiskStatisticsWithDefault = ( + riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec, +): OneTrustAssessmentSectionHeaderRiskStatisticsCodec => + riskStatistics === null + ? createDefaultCodec(OneTrustAssessmentSectionHeaderRiskStatisticsCodec) + : riskStatistics; + // TODO: test the shit out of this export const enrichSectionsWithDefault = ( sections: OneTrustAssessmentSectionCodec[], ): OneTrustAssessmentSectionCodec[] => sections.map((s) => ({ ...s, + header: { + ...s.header, + riskStatistics: enrichRiskStatisticsWithDefault(s.header.riskStatistics), + }, questions: s.questions.map((q) => ({ ...q, question: enrichQuestionWithDefault(q.question), diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 98e5f2c5..2fcb3908 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -13,7 +13,6 @@ import { OneTrustAssessmentQuestionResponseCodec, OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionHeaderCodec, - OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustEnrichedRiskCodec, OneTrustGetAssessmentResponseCodec, } from './codecs'; @@ -108,13 +107,6 @@ const flattenOneTrustQuestionResponses = ( ['responses'], ); - // TODO: replace possible null values within responses - // const defaultObject = { - // id: null, - // name: null, - // nameKey: null, - // }; - // TODO: do we handle it right when empty? const responsesFlat = (responses ?? []).map((r) => flattenObject(r, prefix), @@ -176,21 +168,13 @@ const flattenOneTrustSectionHeaders = ( headers: OneTrustAssessmentSectionHeaderCodec[], prefix: string, ): any => { - // TODO: set a default for EVERY nested object that may be null - const defaultRiskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec = - { - maxRiskLevel: null, - riskCount: null, - sectionId: null, - }; - const { riskStatistics, rest: restHeaders } = extractProperties(headers, [ 'riskStatistics', ]); const flatFlatHeaders = restHeaders.map((h) => flattenObject(h, prefix)); const flatRiskStatistics = riskStatistics.map((r) => - flattenObject(r ?? defaultRiskStatistics, `${prefix}_riskStatistics`), + flattenObject(r, `${prefix}_riskStatistics`), ); return { ...aggregateObjects({ objs: flatFlatHeaders }), From c0e9d260500c562fb8592e8b2ea420df26b9e92d Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 23:32:45 +0000 Subject: [PATCH 23/79] fix bug --- src/helpers/enrichWithDefault.ts | 9 +- src/helpers/tests/createDefaultCodec.test.ts | 8 + src/oneTrust/codecs.ts | 146 ++++++++++--------- src/oneTrust/flattenOneTrustAssessment.ts | 16 +- 4 files changed, 101 insertions(+), 78 deletions(-) diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts index 2f0fb4b4..578b5602 100644 --- a/src/helpers/enrichWithDefault.ts +++ b/src/helpers/enrichWithDefault.ts @@ -6,6 +6,7 @@ import { OneTrustAssessmentQuestionResponsesCodec, OneTrustAssessmentQuestionRiskCodec, OneTrustAssessmentQuestionRisksCodec, + OneTrustAssessmentResponsesCodec, OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustAssessmentSectionSubmittedByCodec, @@ -31,7 +32,13 @@ const enrichQuestionResponsesWithDefault = ( ): OneTrustAssessmentQuestionResponsesCodec => questionResponses.length === 0 ? createDefaultCodec(t.array(OneTrustAssessmentQuestionResponseCodec)) - : questionResponses; + : questionResponses.map((questionResponse) => ({ + ...questionResponse, + responses: + questionResponse.responses.length === 0 + ? createDefaultCodec(OneTrustAssessmentResponsesCodec) + : questionResponse.responses, + })); // TODO: test the shit out of this const enrichRisksWithDefault = ( diff --git a/src/helpers/tests/createDefaultCodec.test.ts b/src/helpers/tests/createDefaultCodec.test.ts index 2e6c0a46..68e10202 100644 --- a/src/helpers/tests/createDefaultCodec.test.ts +++ b/src/helpers/tests/createDefaultCodec.test.ts @@ -3,6 +3,7 @@ import chai, { expect } from 'chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { createDefaultCodec } from '../createDefaultCodec'; +import { OneTrustAssessmentQuestionResponseCodec } from '../../oneTrust/codecs'; chai.use(deepEqualInAnyOrder); @@ -97,4 +98,11 @@ describe('buildDefaultCodec', () => { // should default to the first value if the union does not contains null expect(result).to.deep.equalInAnyOrder({ id: '', name: '', age: null }); }); + it.only('should correctly build a default codec for an intersection', () => { + const result = createDefaultCodec( + t.array(OneTrustAssessmentQuestionResponseCodec), + ); + + console.log({ result: JSON.stringify(result, null, 2) }); + }); }); diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 4f6f34b1..f331a3f4 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -155,78 +155,84 @@ export type OneTrustAssessmentQuestionRisksCodec = t.TypeOf< typeof OneTrustAssessmentQuestionRisksCodec >; +export const OneTrustAssessmentResponsesCodec = t.array( + t.type({ + /** ID of the response. */ + responseId: t.string, + /** Content of the response. */ + response: t.union([t.string, t.null]), + /** Type of response. */ + type: t.union([ + t.literal('NOT_SURE'), + t.literal('JUSTIFICATION'), + t.literal('NOT_APPLICABLE'), + t.literal('DEFAULT'), + t.literal('OTHERS'), + ]), + /** Source from which the assessment is launched. */ + responseSourceType: t.union([ + t.literal('LAUNCH_FROM_INVENTORY'), + t.literal('FORCE_CREATED_SOURCE'), + t.null, + ]), + /** Error associated with the response. */ + errorCode: t.union([ + t.literal('ATTRIBUTE_DISABLED'), + t.literal('ATTRIBUTE_OPTION_DISABLED'), + t.literal('INVENTORY_NOT_EXISTS'), + t.literal('RELATED_INVENTORY_ATTRIBUTE_DISABLED'), + t.literal('DATA_ELEMENT_NOT_EXISTS'), + t.literal('DUPLICATE_INVENTORY'), + t.null, + ]), + /** This parameter is only applicable for inventory type responses (Example- ASSETS). */ + responseMap: t.object, + /** Indicates whether the response is valid. */ + valid: t.boolean, + /** The data subject */ + dataSubject: t.union([ + t.type({ + /** The ID of the data subject */ + id: t.union([t.string, t.null]), + /** The ID of the data subject */ + name: t.union([t.string, t.null]), + /** The nameKey of the data category */ + nameKey: t.union([t.string, t.null]), + }), + t.null, + ]), + /** The data category */ + dataCategory: t.union([ + t.type({ + /** The ID of the data category */ + id: t.union([t.string, t.null]), + /** The name of the data category */ + name: t.union([t.string, t.null]), + /** The nameKey of the data category */ + nameKey: t.union([t.string, t.null]), + }), + t.null, + ]), + /** The data element */ + dataElement: t.union([ + t.type({ + /** The ID of the data element */ + id: t.union([t.string, t.null]), + /** The ID of the data element */ + name: t.union([t.string, t.null]), + }), + t.null, + ]), + }), +); +/** Type override */ +export type OneTrustAssessmentResponsesCodec = t.TypeOf< + typeof OneTrustAssessmentResponsesCodec +>; + export const OneTrustAssessmentQuestionResponseCodec = t.type({ /** The responses */ - responses: t.array( - t.type({ - /** ID of the response. */ - responseId: t.string, - /** Content of the response. */ - response: t.union([t.string, t.null]), - /** Type of response. */ - type: t.union([ - t.literal('NOT_SURE'), - t.literal('JUSTIFICATION'), - t.literal('NOT_APPLICABLE'), - t.literal('DEFAULT'), - t.literal('OTHERS'), - ]), - /** Source from which the assessment is launched. */ - responseSourceType: t.union([ - t.literal('LAUNCH_FROM_INVENTORY'), - t.literal('FORCE_CREATED_SOURCE'), - t.null, - ]), - /** Error associated with the response. */ - errorCode: t.union([ - t.literal('ATTRIBUTE_DISABLED'), - t.literal('ATTRIBUTE_OPTION_DISABLED'), - t.literal('INVENTORY_NOT_EXISTS'), - t.literal('RELATED_INVENTORY_ATTRIBUTE_DISABLED'), - t.literal('DATA_ELEMENT_NOT_EXISTS'), - t.literal('DUPLICATE_INVENTORY'), - t.null, - ]), - /** This parameter is only applicable for inventory type responses (Example- ASSETS). */ - responseMap: t.object, - /** Indicates whether the response is valid. */ - valid: t.boolean, - /** The data subject */ - dataSubject: t.union([ - t.type({ - /** The ID of the data subject */ - id: t.union([t.string, t.null]), - /** The ID of the data subject */ - name: t.union([t.string, t.null]), - /** The nameKey of the data category */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - /** The data category */ - dataCategory: t.union([ - t.type({ - /** The ID of the data category */ - id: t.union([t.string, t.null]), - /** The name of the data category */ - name: t.union([t.string, t.null]), - /** The nameKey of the data category */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - /** The data element */ - dataElement: t.union([ - t.type({ - /** The ID of the data element */ - id: t.union([t.string, t.null]), - /** The ID of the data element */ - name: t.union([t.string, t.null]), - }), - t.null, - ]), - }), - ), + responses: OneTrustAssessmentResponsesCodec, /** Justification comments for the given response. */ justification: t.union([t.string, t.null]), }); diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 2fcb3908..d0050ec5 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -103,7 +103,11 @@ const flattenOneTrustQuestionResponses = ( const allQuestionResponsesFlat = allQuestionResponses.map( (questionResponses) => { const { responses, rest: restQuestionResponses } = extractProperties( - questionResponses, + questionResponses.map((q) => ({ + ...q, + // there is always just one response within responses + responses: q.responses[0], + })), ['responses'], ); @@ -145,15 +149,13 @@ const flattenOneTrustQuestions = ( flattenObject(q, prefix), ); - const result = flattenOneTrustQuestionResponses( - allQuestionResponses, - `${prefix}_questionResponses`, - ); - return { ...aggregateObjects({ objs: restSectionQuestionsFlat }), ...flattenOneTrustNestedQuestions(questions, prefix), - ...result, + ...flattenOneTrustQuestionResponses( + allQuestionResponses, + `${prefix}_questionResponses`, + ), }; }, ); From 930a1ba21fcc9baacc03cfc1eaf739cdb918ad01 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 11 Jan 2025 23:33:06 +0000 Subject: [PATCH 24/79] remove extra test --- src/helpers/tests/createDefaultCodec.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/helpers/tests/createDefaultCodec.test.ts b/src/helpers/tests/createDefaultCodec.test.ts index 68e10202..2e6c0a46 100644 --- a/src/helpers/tests/createDefaultCodec.test.ts +++ b/src/helpers/tests/createDefaultCodec.test.ts @@ -3,7 +3,6 @@ import chai, { expect } from 'chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { createDefaultCodec } from '../createDefaultCodec'; -import { OneTrustAssessmentQuestionResponseCodec } from '../../oneTrust/codecs'; chai.use(deepEqualInAnyOrder); @@ -98,11 +97,4 @@ describe('buildDefaultCodec', () => { // should default to the first value if the union does not contains null expect(result).to.deep.equalInAnyOrder({ id: '', name: '', age: null }); }); - it.only('should correctly build a default codec for an intersection', () => { - const result = createDefaultCodec( - t.array(OneTrustAssessmentQuestionResponseCodec), - ); - - console.log({ result: JSON.stringify(result, null, 2) }); - }); }); From 6538e417789b23b93c76a1a25ce54c1ed83ac68c Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 00:07:32 +0000 Subject: [PATCH 25/79] create more codecs --- src/helpers/enrichWithDefault.ts | 13 ++++---- src/oneTrust/codecs.ts | 40 +++++++++++++++++++++++ src/oneTrust/flattenOneTrustAssessment.ts | 25 +++++++------- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts index 578b5602..2d1596c8 100644 --- a/src/helpers/enrichWithDefault.ts +++ b/src/helpers/enrichWithDefault.ts @@ -4,12 +4,13 @@ import { OneTrustAssessmentQuestionOptionCodec, OneTrustAssessmentQuestionResponseCodec, OneTrustAssessmentQuestionResponsesCodec, - OneTrustAssessmentQuestionRiskCodec, - OneTrustAssessmentQuestionRisksCodec, OneTrustAssessmentResponsesCodec, OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustAssessmentSectionSubmittedByCodec, + OneTrustEnrichedAssessmentSectionCodec, + OneTrustEnrichedRiskCodec, + OneTrustEnrichedRisksCodec, OneTrustPrimaryEntityDetailsCodec, } from '../oneTrust/codecs'; import { createDefaultCodec } from './createDefaultCodec'; @@ -42,10 +43,10 @@ const enrichQuestionResponsesWithDefault = ( // TODO: test the shit out of this const enrichRisksWithDefault = ( - risks: OneTrustAssessmentQuestionRisksCodec, -): OneTrustAssessmentQuestionRisksCodec => + risks: OneTrustEnrichedRisksCodec, +): OneTrustEnrichedRisksCodec => risks === null || risks.length === 0 - ? createDefaultCodec(t.array(OneTrustAssessmentQuestionRiskCodec)) + ? createDefaultCodec(t.array(OneTrustEnrichedRiskCodec)) : risks; // TODO: test the shit out of this @@ -58,7 +59,7 @@ const enrichRiskStatisticsWithDefault = ( // TODO: test the shit out of this export const enrichSectionsWithDefault = ( - sections: OneTrustAssessmentSectionCodec[], + sections: OneTrustEnrichedAssessmentSectionCodec[], ): OneTrustAssessmentSectionCodec[] => sections.map((s) => ({ ...s, diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index f331a3f4..a7dc1661 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -1031,4 +1031,44 @@ export type OneTrustEnrichedRiskCodec = t.TypeOf< typeof OneTrustEnrichedRiskCodec >; +// TODO: do not move to privacy-types +export const OneTrustEnrichedRisksCodec = t.union([ + t.array(OneTrustEnrichedRiskCodec), + t.null, +]); +/** Type override */ +export type OneTrustEnrichedRisksCodec = t.TypeOf< + typeof OneTrustEnrichedRisksCodec +>; + +// TODO: do not add to privacy-types +export const OneTrustEnrichedAssessmentQuestionCodec = t.type({ + ...OneTrustAssessmentQuestionCodec.props, + risks: t.union([t.array(OneTrustEnrichedRiskCodec), t.null]), +}); +/** Type override */ +export type OneTrustEnrichedAssessmentQuestionCodec = t.TypeOf< + typeof OneTrustEnrichedAssessmentQuestionCodec +>; + +// TODO: do not add to privacy-types +export const OneTrustEnrichedAssessmentSectionCodec = t.type({ + ...OneTrustAssessmentSectionCodec.props, + questions: t.array(OneTrustEnrichedAssessmentQuestionCodec), +}); +/** Type override */ +export type OneTrustEnrichedAssessmentSectionCodec = t.TypeOf< + typeof OneTrustEnrichedAssessmentSectionCodec +>; + +// TODO: do not add to privacy-types +export const OneTrustEnrichedAssessmentResponseCodec = t.type({ + ...OneTrustGetAssessmentResponseCodec.props, + sections: t.array(OneTrustEnrichedAssessmentSectionCodec), +}); +/** Type override */ +export type OneTrustEnrichedAssessmentResponseCodec = t.TypeOf< + typeof OneTrustEnrichedAssessmentResponseCodec +>; + /* eslint-enable max-lines */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index d0050ec5..c128a9f8 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -13,8 +13,7 @@ import { OneTrustAssessmentQuestionResponseCodec, OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionHeaderCodec, - OneTrustEnrichedRiskCodec, - OneTrustGetAssessmentResponseCodec, + OneTrustEnrichedAssessmentResponseCodec, } from './codecs'; // TODO: will have to use something like csv-stringify @@ -111,7 +110,6 @@ const flattenOneTrustQuestionResponses = ( ['responses'], ); - // TODO: do we handle it right when empty? const responsesFlat = (responses ?? []).map((r) => flattenObject(r, prefix), ); @@ -138,6 +136,7 @@ const flattenOneTrustQuestions = ( rest: restSectionQuestions, question: questions, questionResponses: allQuestionResponses, + // TODO; continue from here // risks: allRisks, } = extractProperties(sectionQuestions, [ 'question', @@ -211,18 +210,16 @@ export const flattenOneTrustAssessment = ({ }: { /** the assessment */ assessment: OneTrustAssessmentCodec; - /** the assessment with details */ - assessmentDetails: OneTrustGetAssessmentResponseCodec & { - /** the sections enriched with risk details */ - sections: (OneTrustAssessmentSectionCodec & { - /** the questions enriched with risk details */ - questions: (OneTrustAssessmentQuestionCodec & { - /** the enriched risk details */ - risks: OneTrustEnrichedRiskCodec[] | null; - })[]; - })[]; - }; + /** the assessment with details and enriched with risk */ + assessmentDetails: OneTrustEnrichedAssessmentResponseCodec; }): any => { + /** + * TODO: experiment creating a default assessment with + * const result = createDefaultCodec(OneTrustGetAssessmentResponseCodec); + * Then, flatten it and aggregate it with the actual assessment. This way, every + * assessment will always have the same fields! + */ + // add default values to assessments const transformedAssessmentDetails = { ...assessmentDetails, From bb8dd28b16b606207e58f158eaed43d2aec32812 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 18:35:02 +0000 Subject: [PATCH 26/79] flatten risks --- src/helpers/enrichWithDefault.ts | 3 +- src/helpers/tests/createDefaultCodec.test.ts | 9 ++++ src/oneTrust/codecs.ts | 24 +++++----- src/oneTrust/flattenOneTrustAssessment.ts | 47 +++++++++++++++++--- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts index 2d1596c8..f6d71949 100644 --- a/src/helpers/enrichWithDefault.ts +++ b/src/helpers/enrichWithDefault.ts @@ -5,7 +5,6 @@ import { OneTrustAssessmentQuestionResponseCodec, OneTrustAssessmentQuestionResponsesCodec, OneTrustAssessmentResponsesCodec, - OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustAssessmentSectionSubmittedByCodec, OneTrustEnrichedAssessmentSectionCodec, @@ -60,7 +59,7 @@ const enrichRiskStatisticsWithDefault = ( // TODO: test the shit out of this export const enrichSectionsWithDefault = ( sections: OneTrustEnrichedAssessmentSectionCodec[], -): OneTrustAssessmentSectionCodec[] => +): OneTrustEnrichedAssessmentSectionCodec[] => sections.map((s) => ({ ...s, header: { diff --git a/src/helpers/tests/createDefaultCodec.test.ts b/src/helpers/tests/createDefaultCodec.test.ts index 2e6c0a46..09ba0729 100644 --- a/src/helpers/tests/createDefaultCodec.test.ts +++ b/src/helpers/tests/createDefaultCodec.test.ts @@ -3,6 +3,10 @@ import chai, { expect } from 'chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { createDefaultCodec } from '../createDefaultCodec'; +import { + OneTrustEnrichedRiskCodec, + OneTrustGetRiskResponseCodec, +} from '../../oneTrust/codecs'; chai.use(deepEqualInAnyOrder); @@ -97,4 +101,9 @@ describe('buildDefaultCodec', () => { // should default to the first value if the union does not contains null expect(result).to.deep.equalInAnyOrder({ id: '', name: '', age: null }); }); + + // it.only('test', () => { + // const result = createDefaultCodec(OneTrustEnrichedRiskCodec); + // console.log({ result }); + // }); }); diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index a7dc1661..0a643d46 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -775,6 +775,19 @@ const RiskLevelCodec = t.type({ riskScore: t.union([t.number, t.null]), }); +export const OneTrustRiskCategories = t.array( + t.type({ + /** Identifier for Risk Category. */ + id: t.string, + /** Risk Category Name. */ + name: t.string, + /** Risk Category Name Key value for translation. */ + nameKey: t.string, + }), +); +/** Type override */ +export type OneTrustRiskCategories = t.TypeOf; + // ref: https://developer.onetrust.com/onetrust/reference/getriskusingget export const OneTrustGetRiskResponseCodec = t.type({ /** List of associated inventories to the risk. */ @@ -801,16 +814,7 @@ export const OneTrustGetRiskResponseCodec = t.type({ /** The attribute values associated with the risk */ attributeValues: t.object, /** List of categories. */ - categories: t.array( - t.type({ - /** Identifier for Risk Category. */ - id: t.string, - /** Risk Category Name. */ - name: t.string, - /** Risk Category Name Key value for translation. */ - nameKey: t.string, - }), - ), + categories: OneTrustRiskCategories, /** List of Control Identifiers. */ controlsIdentifier: t.array(t.string), /** Risk created time. */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index c128a9f8..d7dd29ad 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -8,12 +8,14 @@ import { import { OneTrustAssessmentCodec, OneTrustAssessmentNestedQuestionCodec, - OneTrustAssessmentQuestionCodec, OneTrustAssessmentQuestionOptionCodec, OneTrustAssessmentQuestionResponseCodec, - OneTrustAssessmentSectionCodec, OneTrustAssessmentSectionHeaderCodec, + OneTrustEnrichedAssessmentQuestionCodec, OneTrustEnrichedAssessmentResponseCodec, + OneTrustEnrichedAssessmentSectionCodec, + OneTrustEnrichedRiskCodec, + OneTrustRiskCategories, } from './codecs'; // TODO: will have to use something like csv-stringify @@ -43,7 +45,7 @@ const flattenObject = (obj: any, prefix = ''): any => return acc; }, {} as Record); -// TODO: comment +// TODO: move to helpers const aggregateObjects = ({ objs, wrap = false, @@ -125,8 +127,39 @@ const flattenOneTrustQuestionResponses = ( return aggregateObjects({ objs: allQuestionResponsesFlat, wrap: true }); }; +const flattenOneTrustRiskCategories = ( + allCategories: OneTrustRiskCategories[], + prefix: string, +): any => { + const allCategoriesFlat = allCategories.map((categories) => { + const flatCategories = categories.map((c) => flattenObject(c, prefix)); + return aggregateObjects({ objs: flatCategories }); + }); + return aggregateObjects({ objs: allCategoriesFlat, wrap: true }); +}; + +const flattenOneTrustRisks = ( + allRisks: (OneTrustEnrichedRiskCodec[] | null)[], + prefix: string, +): any => { + // TODO: extract categories and other nested properties + const allRisksFlat = allRisks.map((risks) => { + const { categories, rest: restRisks } = extractProperties(risks ?? [], [ + 'categories', + ]); + + const flatRisks = restRisks.map((r) => flattenObject(r, prefix)); + return { + ...aggregateObjects({ objs: flatRisks }), + ...flattenOneTrustRiskCategories(categories, `${prefix}_categories`), + }; + }); + + return aggregateObjects({ objs: allRisksFlat, wrap: true }); +}; + const flattenOneTrustQuestions = ( - allSectionQuestions: OneTrustAssessmentQuestionCodec[][], + allSectionQuestions: OneTrustEnrichedAssessmentQuestionCodec[][], prefix: string, ): any => { const allSectionQuestionsFlat = allSectionQuestions.map( @@ -136,8 +169,7 @@ const flattenOneTrustQuestions = ( rest: restSectionQuestions, question: questions, questionResponses: allQuestionResponses, - // TODO; continue from here - // risks: allRisks, + risks: allRisks, } = extractProperties(sectionQuestions, [ 'question', 'questionResponses', @@ -151,6 +183,7 @@ const flattenOneTrustQuestions = ( return { ...aggregateObjects({ objs: restSectionQuestionsFlat }), ...flattenOneTrustNestedQuestions(questions, prefix), + ...flattenOneTrustRisks(allRisks, `${prefix}_risks`), ...flattenOneTrustQuestionResponses( allQuestionResponses, `${prefix}_questionResponses`, @@ -184,7 +217,7 @@ const flattenOneTrustSectionHeaders = ( }; const flattenOneTrustSections = ( - sections: OneTrustAssessmentSectionCodec[], + sections: OneTrustEnrichedAssessmentSectionCodec[], prefix: string, ): any => { const { From e104337d5a0abd21543869dc1a726635b8d08061 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 19:12:08 +0000 Subject: [PATCH 27/79] flatten approvers, respondents, and primaryEntityDetails --- src/oneTrust/flattenOneTrustAssessment.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index d7dd29ad..f05a9436 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -264,9 +264,9 @@ export const flattenOneTrustAssessment = ({ const { // TODO: handle these - // approvers, - // primaryEntityDetails, - // respondents, + approvers, + primaryEntityDetails, + respondents, // eslint-disable-next-line @typescript-eslint/no-unused-vars respondent, sections, @@ -274,12 +274,23 @@ export const flattenOneTrustAssessment = ({ } = transformedAssessmentDetails; // console.log({ approvers: flattenApprovers(approvers) }); + const flatApprovers = approvers.map((approver) => + flattenObject(approver, 'approvers'), + ); + const flatRespondents = respondents.map((respondent) => + flattenObject(respondent, 'respondents'), + ); + const flatPrimaryEntityDetails = primaryEntityDetails.map( + (primaryEntityDetail) => + flattenObject(primaryEntityDetail, 'primaryEntityDetails'), + ); + return { ...flattenObject(assessment), ...flattenObject(rest), - // ...flattenList(approvers, 'approvers'), - // ...flattenList(primaryEntityDetails, 'primaryEntityDetails'), - // ...flattenList(respondents, 'respondents'), + ...aggregateObjects({ objs: flatApprovers }), + ...aggregateObjects({ objs: flatRespondents }), + ...aggregateObjects({ objs: flatPrimaryEntityDetails }), ...flattenOneTrustSections(sections, 'sections'), }; }; From ee8b072d670587be78e55db55bf6d32bd9b5b0d8 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 19:15:55 +0000 Subject: [PATCH 28/79] update flattenOneTrustAssessment type --- src/oneTrust/codecs.ts | 12 ++++++++++++ src/oneTrust/flattenOneTrustAssessment.ts | 24 +++++++---------------- src/oneTrust/writeOneTrustAssessment.ts | 4 ++-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 0a643d46..1e7ad7bf 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -1075,4 +1075,16 @@ export type OneTrustEnrichedAssessmentResponseCodec = t.TypeOf< typeof OneTrustEnrichedAssessmentResponseCodec >; +// TODO: do not add to privacy-types +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { status, ...OneTrustAssessmentCodecWithoutStatus } = + OneTrustAssessmentCodec.props; +export const CombinedAssessmentCodec = t.intersection([ + t.type(OneTrustAssessmentCodecWithoutStatus), + OneTrustEnrichedAssessmentResponseCodec, +]); + +/** Type override */ +export type CombinedAssessmentCodec = t.TypeOf; + /* eslint-enable max-lines */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index f05a9436..794303ea 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -6,13 +6,12 @@ import { extractProperties, } from '../helpers'; import { - OneTrustAssessmentCodec, + CombinedAssessmentCodec, OneTrustAssessmentNestedQuestionCodec, OneTrustAssessmentQuestionOptionCodec, OneTrustAssessmentQuestionResponseCodec, OneTrustAssessmentSectionHeaderCodec, OneTrustEnrichedAssessmentQuestionCodec, - OneTrustEnrichedAssessmentResponseCodec, OneTrustEnrichedAssessmentSectionCodec, OneTrustEnrichedRiskCodec, OneTrustRiskCategories, @@ -237,15 +236,9 @@ const flattenOneTrustSections = ( return { ...sectionsFlat, ...headersFlat, ...questionsFlat }; }; -export const flattenOneTrustAssessment = ({ - assessment, - assessmentDetails, -}: { - /** the assessment */ - assessment: OneTrustAssessmentCodec; - /** the assessment with details and enriched with risk */ - assessmentDetails: OneTrustEnrichedAssessmentResponseCodec; -}): any => { +export const flattenOneTrustAssessment = ( + combinedAssessment: CombinedAssessmentCodec, +): any => { /** * TODO: experiment creating a default assessment with * const result = createDefaultCodec(OneTrustGetAssessmentResponseCodec); @@ -255,15 +248,14 @@ export const flattenOneTrustAssessment = ({ // add default values to assessments const transformedAssessmentDetails = { - ...assessmentDetails, + ...combinedAssessment, primaryEntityDetails: enrichPrimaryEntityDetailsWithDefault( - assessmentDetails.primaryEntityDetails, + combinedAssessment.primaryEntityDetails, ), - sections: enrichSectionsWithDefault(assessmentDetails.sections), + sections: enrichSectionsWithDefault(combinedAssessment.sections), }; const { - // TODO: handle these approvers, primaryEntityDetails, respondents, @@ -273,7 +265,6 @@ export const flattenOneTrustAssessment = ({ ...rest } = transformedAssessmentDetails; - // console.log({ approvers: flattenApprovers(approvers) }); const flatApprovers = approvers.map((approver) => flattenObject(approver, 'approvers'), ); @@ -286,7 +277,6 @@ export const flattenOneTrustAssessment = ({ ); return { - ...flattenObject(assessment), ...flattenObject(rest), ...aggregateObjects({ objs: flatApprovers }), ...aggregateObjects({ objs: flatRespondents }), diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index 75e5f866..1372a7e2 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -115,8 +115,8 @@ export const writeOneTrustAssessment = ({ } const flattened = flattenOneTrustAssessment({ - assessment, - assessmentDetails: enrichedAssessment, + ...assessment, + ...enrichedAssessment, }); const stringifiedFlattened = JSON.stringify(flattened, null, 2); // TODO: do not forget to ensure we have the same set of keys!!! From d6052b1f7885b67e84693eb6fe6e9fb27d29eb1b Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 19:17:22 +0000 Subject: [PATCH 29/79] create DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT --- src/oneTrust/codecs.ts | 6 ++++-- src/oneTrust/constants.ts | 6 ++++++ src/oneTrust/flattenOneTrustAssessment.ts | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 src/oneTrust/constants.ts diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 1e7ad7bf..1f0d149d 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -1079,12 +1079,14 @@ export type OneTrustEnrichedAssessmentResponseCodec = t.TypeOf< // eslint-disable-next-line @typescript-eslint/no-unused-vars const { status, ...OneTrustAssessmentCodecWithoutStatus } = OneTrustAssessmentCodec.props; -export const CombinedAssessmentCodec = t.intersection([ +export const OneTrustCombinedAssessmentCodec = t.intersection([ t.type(OneTrustAssessmentCodecWithoutStatus), OneTrustEnrichedAssessmentResponseCodec, ]); /** Type override */ -export type CombinedAssessmentCodec = t.TypeOf; +export type OneTrustCombinedAssessmentCodec = t.TypeOf< + typeof OneTrustCombinedAssessmentCodec +>; /* eslint-enable max-lines */ diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts new file mode 100644 index 00000000..dfe7a7b2 --- /dev/null +++ b/src/oneTrust/constants.ts @@ -0,0 +1,6 @@ +import { createDefaultCodec } from '../helpers'; +import { OneTrustCombinedAssessmentCodec } from './codecs'; + +export const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT = createDefaultCodec( + OneTrustCombinedAssessmentCodec, +); diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 794303ea..d2465221 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -6,7 +6,7 @@ import { extractProperties, } from '../helpers'; import { - CombinedAssessmentCodec, + OneTrustCombinedAssessmentCodec, OneTrustAssessmentNestedQuestionCodec, OneTrustAssessmentQuestionOptionCodec, OneTrustAssessmentQuestionResponseCodec, @@ -237,7 +237,7 @@ const flattenOneTrustSections = ( }; export const flattenOneTrustAssessment = ( - combinedAssessment: CombinedAssessmentCodec, + combinedAssessment: OneTrustCombinedAssessmentCodec, ): any => { /** * TODO: experiment creating a default assessment with From b1c5f6b321c10b7adcb90e9ec0733296fc130211 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 19:20:26 +0000 Subject: [PATCH 30/79] add comments --- src/oneTrust/constants.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts index dfe7a7b2..47738c8b 100644 --- a/src/oneTrust/constants.ts +++ b/src/oneTrust/constants.ts @@ -1,6 +1,10 @@ import { createDefaultCodec } from '../helpers'; import { OneTrustCombinedAssessmentCodec } from './codecs'; -export const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT = createDefaultCodec( - OneTrustCombinedAssessmentCodec, -); +/** + * An object with default values of type OneTrustCombinedAssessmentCodec. It's very + * valuable when converting assessments to CSV. When we flatten it, the resulting + * value always contains all keys that eventually we add to the header. + */ +export const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustCombinedAssessmentCodec = + createDefaultCodec(OneTrustCombinedAssessmentCodec); From ae91ee22a213d0ae837b2a12adc760e4b1edea04 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 19:21:50 +0000 Subject: [PATCH 31/79] update --- src/oneTrust/flattenOneTrustAssessment.ts | 56 ++++++++++++----------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index d2465221..9c1e4188 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -247,7 +247,7 @@ export const flattenOneTrustAssessment = ( */ // add default values to assessments - const transformedAssessmentDetails = { + const assessmentWithDefaults = { ...combinedAssessment, primaryEntityDetails: enrichPrimaryEntityDetailsWithDefault( combinedAssessment.primaryEntityDetails, @@ -255,34 +255,38 @@ export const flattenOneTrustAssessment = ( sections: enrichSectionsWithDefault(combinedAssessment.sections), }; - const { - approvers, - primaryEntityDetails, - respondents, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - respondent, - sections, - ...rest - } = transformedAssessmentDetails; + const flatten = (assessment: OneTrustCombinedAssessmentCodec): any => { + const { + approvers, + primaryEntityDetails, + respondents, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + respondent, + sections, + ...rest + } = assessment; - const flatApprovers = approvers.map((approver) => - flattenObject(approver, 'approvers'), - ); - const flatRespondents = respondents.map((respondent) => - flattenObject(respondent, 'respondents'), - ); - const flatPrimaryEntityDetails = primaryEntityDetails.map( - (primaryEntityDetail) => - flattenObject(primaryEntityDetail, 'primaryEntityDetails'), - ); + const flatApprovers = approvers.map((approver) => + flattenObject(approver, 'approvers'), + ); + const flatRespondents = respondents.map((respondent) => + flattenObject(respondent, 'respondents'), + ); + const flatPrimaryEntityDetails = primaryEntityDetails.map( + (primaryEntityDetail) => + flattenObject(primaryEntityDetail, 'primaryEntityDetails'), + ); - return { - ...flattenObject(rest), - ...aggregateObjects({ objs: flatApprovers }), - ...aggregateObjects({ objs: flatRespondents }), - ...aggregateObjects({ objs: flatPrimaryEntityDetails }), - ...flattenOneTrustSections(sections, 'sections'), + return { + ...flattenObject(rest), + ...aggregateObjects({ objs: flatApprovers }), + ...aggregateObjects({ objs: flatRespondents }), + ...aggregateObjects({ objs: flatPrimaryEntityDetails }), + ...flattenOneTrustSections(sections, 'sections'), + }; }; + + return flatten(assessmentWithDefaults); }; /** * From 80c3f723039ec95515bb64c1dc4e8b8adf7c04dd Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 12 Jan 2025 19:44:43 +0000 Subject: [PATCH 32/79] improve createDefatulCodec --- src/helpers/createDefaultCodec.ts | 11 +++++- src/helpers/tests/createDefaultCodec.test.ts | 39 ++++++++++++++------ src/oneTrust/codecs.ts | 2 +- src/oneTrust/flattenOneTrustAssessment.ts | 3 ++ 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/helpers/createDefaultCodec.ts b/src/helpers/createDefaultCodec.ts index 5d0dc0b6..a7520812 100644 --- a/src/helpers/createDefaultCodec.ts +++ b/src/helpers/createDefaultCodec.ts @@ -12,12 +12,21 @@ export const createDefaultCodec = ( codec: C, ): t.TypeOf => { if (codec instanceof t.UnionType) { + // First, look for object types in the union + const arrayType = codec.types.find( + (type: any) => type instanceof t.ArrayType, + ); + if (arrayType) { + return createDefaultCodec(arrayType); + } + // First, look for object types in the union const objectType = codec.types.find( (type: any) => type instanceof t.InterfaceType || type instanceof t.PartialType || - type instanceof t.IntersectionType, + type instanceof t.IntersectionType || + type instanceof t.ArrayType, ); if (objectType) { return createDefaultCodec(objectType); diff --git a/src/helpers/tests/createDefaultCodec.test.ts b/src/helpers/tests/createDefaultCodec.test.ts index 09ba0729..9962e046 100644 --- a/src/helpers/tests/createDefaultCodec.test.ts +++ b/src/helpers/tests/createDefaultCodec.test.ts @@ -3,10 +3,6 @@ import chai, { expect } from 'chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { createDefaultCodec } from '../createDefaultCodec'; -import { - OneTrustEnrichedRiskCodec, - OneTrustGetRiskResponseCodec, -} from '../../oneTrust/codecs'; chai.use(deepEqualInAnyOrder); @@ -42,14 +38,40 @@ describe('buildDefaultCodec', () => { expect(result).to.equal(null); }); - it('should correctly build a default codec for a union with type', () => { + it('should correctly build a default codec for a union with object', () => { const result = createDefaultCodec( t.union([t.string, t.null, t.type({ name: t.string })]), ); - // should default to the type if the union contains a type + // should default to the type if the union contains an object expect(result).to.deep.equal({ name: '' }); }); + it('should correctly build a default codec for a union with array of type', () => { + const result = createDefaultCodec( + t.union([ + t.string, + t.null, + t.type({ name: t.string }), + t.array(t.string), + ]), + ); + // should default to the empty array if the union contains an array of type + expect(result).to.deep.equal([]); + }); + + it('should correctly build a default codec for a union with array of object', () => { + const result = createDefaultCodec( + t.union([ + t.string, + t.null, + t.type({ name: t.string }), + t.array(t.type({ age: t.number })), + ]), + ); + // should default to the array with object if the union contains an array of objects + expect(result).to.deep.equal([{ age: null }]); + }); + it('should correctly build a default codec for a union without null', () => { const result = createDefaultCodec(t.union([t.string, t.number])); // should default to the first value if the union does not contains null @@ -101,9 +123,4 @@ describe('buildDefaultCodec', () => { // should default to the first value if the union does not contains null expect(result).to.deep.equalInAnyOrder({ id: '', name: '', age: null }); }); - - // it.only('test', () => { - // const result = createDefaultCodec(OneTrustEnrichedRiskCodec); - // console.log({ result }); - // }); }); diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 1f0d149d..91fefc8e 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -1048,7 +1048,7 @@ export type OneTrustEnrichedRisksCodec = t.TypeOf< // TODO: do not add to privacy-types export const OneTrustEnrichedAssessmentQuestionCodec = t.type({ ...OneTrustAssessmentQuestionCodec.props, - risks: t.union([t.array(OneTrustEnrichedRiskCodec), t.null]), + risks: OneTrustEnrichedRisksCodec, }); /** Type override */ export type OneTrustEnrichedAssessmentQuestionCodec = t.TypeOf< diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 9c1e4188..3d8ef31f 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -16,6 +16,7 @@ import { OneTrustEnrichedRiskCodec, OneTrustRiskCategories, } from './codecs'; +import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // TODO: will have to use something like csv-stringify @@ -286,6 +287,8 @@ export const flattenOneTrustAssessment = ( }; }; + flatten(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT); + return flatten(assessmentWithDefaults); }; /** From 2c12a0e219192d405f9969a845b911d03d998258 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 02:24:44 +0000 Subject: [PATCH 33/79] add missing fields to codec --- src/helpers/enrichWithDefault.ts | 15 +++- src/oneTrust/codecs.ts | 100 +++++++++++++++++++++- src/oneTrust/flattenOneTrustAssessment.ts | 23 ++--- 3 files changed, 119 insertions(+), 19 deletions(-) diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts index f6d71949..33e90c01 100644 --- a/src/helpers/enrichWithDefault.ts +++ b/src/helpers/enrichWithDefault.ts @@ -7,6 +7,7 @@ import { OneTrustAssessmentResponsesCodec, OneTrustAssessmentSectionHeaderRiskStatisticsCodec, OneTrustAssessmentSectionSubmittedByCodec, + OneTrustCombinedAssessmentCodec, OneTrustEnrichedAssessmentSectionCodec, OneTrustEnrichedRiskCodec, OneTrustEnrichedRisksCodec, @@ -57,7 +58,7 @@ const enrichRiskStatisticsWithDefault = ( : riskStatistics; // TODO: test the shit out of this -export const enrichSectionsWithDefault = ( +const enrichSectionsWithDefault = ( sections: OneTrustEnrichedAssessmentSectionCodec[], ): OneTrustEnrichedAssessmentSectionCodec[] => sections.map((s) => ({ @@ -80,9 +81,19 @@ export const enrichSectionsWithDefault = ( : s.submittedBy, })); -export const enrichPrimaryEntityDetailsWithDefault = ( +const enrichPrimaryEntityDetailsWithDefault = ( primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec, ): OneTrustPrimaryEntityDetailsCodec => primaryEntityDetails.length === 0 ? createDefaultCodec(OneTrustPrimaryEntityDetailsCodec) : primaryEntityDetails; + +export const enrichCombinedAssessmentWithDefaults = ( + combinedAssessment: OneTrustCombinedAssessmentCodec, +): OneTrustCombinedAssessmentCodec => ({ + ...combinedAssessment, + primaryEntityDetails: enrichPrimaryEntityDetailsWithDefault( + combinedAssessment.primaryEntityDetails, + ), + sections: enrichSectionsWithDefault(combinedAssessment.sections), +}); diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 91fefc8e..1e27aa6c 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -105,6 +105,16 @@ export const OneTrustAssessmentQuestionOptionCodec = t.type({ id: t.string, /** Name of the option. */ option: t.string, + /** The key of the option */ + optionKey: t.union([t.string, t.null]), + /** The hint */ + hint: t.union([t.string, t.null]), + /** The hint key */ + hintKey: t.union([t.string, t.null]), + /** The score */ + score: t.union([t.number, t.null]), + /** If the option was pre-selected */ + preSelectedOption: t.boolean, /** Order in which the option appears. */ sequence: t.union([t.number, t.null]), /** Attribute for which the option is available. */ @@ -157,6 +167,20 @@ export type OneTrustAssessmentQuestionRisksCodec = t.TypeOf< export const OneTrustAssessmentResponsesCodec = t.array( t.type({ + /** The controlResponse */ + controlResponse: t.union([t.string, t.null]), + /** The relationshipResponseDetails */ + relationshipResponseDetails: t.array(t.string), + /** The textRedacted */ + textRedacted: t.boolean, + /** The maturityScale */ + maturityScale: t.union([t.string, t.null]), + /** The effectivenessScale */ + effectivenessScale: t.union([t.string, t.null]), + /** The parentAssessmentDetailId */ + parentAssessmentDetailId: t.union([t.string, t.null]), + /** The responseMap */ + responseMap: t.object, /** ID of the response. */ responseId: t.string, /** Content of the response. */ @@ -185,8 +209,6 @@ export const OneTrustAssessmentResponsesCodec = t.array( t.literal('DUPLICATE_INVENTORY'), t.null, ]), - /** This parameter is only applicable for inventory type responses (Example- ASSETS). */ - responseMap: t.object, /** Indicates whether the response is valid. */ valid: t.boolean, /** The data subject */ @@ -218,8 +240,10 @@ export const OneTrustAssessmentResponsesCodec = t.array( t.type({ /** The ID of the data element */ id: t.union([t.string, t.null]), - /** The ID of the data element */ + /** The name of the data element */ name: t.union([t.string, t.null]), + /** The name key of the data element */ + nameKey: t.union([t.string, t.null]), }), t.null, ]), @@ -374,6 +398,56 @@ export const OneTrustAssessmentQuestionCodec = t.type({ totalAttachments: t.number, /** IDs of the attachment(s) added to the question. */ attachmentIds: t.array(t.string), + /** The canReopenWithAllowEditOption */ + canReopenWithAllowEditOption: t.boolean, + /** The riskCreationAllowed */ + riskCreationAllowed: t.boolean, + /** The riskDeletionPopupAllowed */ + riskDeletionPopupAllowed: t.boolean, + /** The allowMaturityScaleOnQuestions */ + allowMaturityScaleOnQuestions: t.boolean, + /** The questionAssociations */ + questionAssociations: t.union([t.string, t.null]), + /** The issues */ + issues: t.union([t.string, t.null]), + /** The responseEditableWhileUnderReview */ + responseEditableWhileUnderReview: t.boolean, + /** The businessKeyReference */ + businessKeyReference: t.union([t.string, t.null]), + /** The topic */ + topic: t.union([t.string, t.null]), + /** The questionLaws */ + questionLaws: t.array(t.string), + /** The attachmentRequired */ + attachmentRequired: t.boolean, + /** The responseFilter */ + responseFilter: t.union([t.string, t.null]), + /** The linkAssessmentToResponseEntity */ + linkAssessmentToResponseEntity: t.boolean, + /** The readOnly */ + readOnly: t.boolean, + /** The schema */ + schema: t.union([t.string, t.null]), + /** The attributeId */ + attributeId: t.string, + /** Whether it is a vendor question */ + vendorQuestion: t.boolean, + /** Whether the question was seeded */ + seeded: t.boolean, + /** Whether the question allows justification */ + allowJustification: t.boolean, + /** Whether it refers to an asset question */ + assetQuestion: t.boolean, + /** Whether it refers to an entity question */ + entityQuestion: t.boolean, + /** Whether it is a paquestion */ + paquestion: t.boolean, + /** The inventoryTypeEnum */ + inventoryTypeEnum: t.union([t.string, t.null]), + /** Whether it is a forceOther */ + forceOther: t.boolean, + /** Whether it is a isParentQuestionMultiSelect */ + isParentQuestionMultiSelect: t.boolean, }); /** Type override */ @@ -421,6 +495,10 @@ export const OneTrustAssessmentSectionHeaderCodec = t.type({ sectionId: t.string, /** Name of the section. */ name: t.string, + /** The status of the section */ + status: t.union([t.string, t.null]), + /** The openNMIQuestionIds */ + openNMIQuestionIds: t.union([t.string, t.null]), /** Description of the section header. */ description: t.union([t.string, t.null]), /** Sequence of the section within the form */ @@ -556,6 +634,8 @@ export const OneTrustApproverCodec = t.type({ email: t.union([t.string, t.null]), /** Whether the user assigned as an approver was deleted. */ deleted: t.boolean, + /** The assignee type */ + assigneeType: t.union([t.string, t.null]), }), /** Assessment approval status. */ approvalState: t.union([ @@ -604,6 +684,10 @@ export const OneTrustPrimaryEntityDetailsCodec = t.array( number: t.number, /** Name and number of the primary record. */ displayName: t.string, + /** The relationshipResponseDetails */ + relationshipResponseDetails: t.union([t.string, t.null]), + /** The entity business key */ + entityBusinessKey: t.union([t.string, t.null]), }), ); /** Type override */ @@ -629,6 +713,8 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ id: t.string, /** The name of the creator */ name: t.string, + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), }), /** Date and time at which the assessment was created. */ createdDT: t.string, @@ -656,6 +742,8 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ id: t.string, /** The name of the organization group */ name: t.string, + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), }), /** The primary record */ primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec, @@ -686,6 +774,8 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ id: t.string, /** The name or email of the respondent */ name: t.string, + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), }), ), /** Result of the assessment. */ @@ -723,6 +813,8 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ id: t.string, /** The name of the template */ name: t.string, + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), }), /** Number of total risks on the assessment. */ totalRiskCount: t.number, @@ -857,6 +949,8 @@ export const OneTrustGetRiskResponseCodec = t.type({ id: t.string, /** Name of an entity. */ name: t.string, + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), }), /** The previous risk state */ previousState: t.union([ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 3d8ef31f..baa041bc 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -1,8 +1,7 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/no-explicit-any */ import { - enrichPrimaryEntityDetailsWithDefault, - enrichSectionsWithDefault, + enrichCombinedAssessmentWithDefaults, extractProperties, } from '../helpers'; import { @@ -16,7 +15,7 @@ import { OneTrustEnrichedRiskCodec, OneTrustRiskCategories, } from './codecs'; -import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; +// import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // TODO: will have to use something like csv-stringify @@ -247,15 +246,6 @@ export const flattenOneTrustAssessment = ( * assessment will always have the same fields! */ - // add default values to assessments - const assessmentWithDefaults = { - ...combinedAssessment, - primaryEntityDetails: enrichPrimaryEntityDetailsWithDefault( - combinedAssessment.primaryEntityDetails, - ), - sections: enrichSectionsWithDefault(combinedAssessment.sections), - }; - const flatten = (assessment: OneTrustCombinedAssessmentCodec): any => { const { approvers, @@ -287,9 +277,14 @@ export const flattenOneTrustAssessment = ( }; }; - flatten(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT); + // add default values to assessments + const combinedAssessmentWithDefaults = + enrichCombinedAssessmentWithDefaults(combinedAssessment); + + const combinedAssessmentFlat = flatten(combinedAssessmentWithDefaults); + // const defaultAssessmentFlat = flatten(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT); - return flatten(assessmentWithDefaults); + return combinedAssessmentFlat; }; /** * From 24841f11a3da01975b9354b34392f9517ed7ca25 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 02:57:06 +0000 Subject: [PATCH 34/79] fix codecs --- src/cli-pull-ot.ts | 5 +- src/oneTrust/codecs.ts | 382 +++++++++++----------- src/oneTrust/flattenOneTrustAssessment.ts | 3 + 3 files changed, 207 insertions(+), 183 deletions(-) diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index 1116a817..aa6abf7b 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -35,8 +35,11 @@ async function main(): Promise { // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); + // TODO: undo + const newAssessments = assessments.slice(3); + // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory - await mapSeries(assessments, async (assessment, index) => { + await mapSeries(newAssessments, async (assessment, index) => { logger.info( `Fetching details about assessment ${index + 1} of ${ assessments.length diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 1e27aa6c..eb2b0757 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -166,88 +166,92 @@ export type OneTrustAssessmentQuestionRisksCodec = t.TypeOf< >; export const OneTrustAssessmentResponsesCodec = t.array( - t.type({ - /** The controlResponse */ - controlResponse: t.union([t.string, t.null]), - /** The relationshipResponseDetails */ - relationshipResponseDetails: t.array(t.string), - /** The textRedacted */ - textRedacted: t.boolean, - /** The maturityScale */ - maturityScale: t.union([t.string, t.null]), - /** The effectivenessScale */ - effectivenessScale: t.union([t.string, t.null]), - /** The parentAssessmentDetailId */ - parentAssessmentDetailId: t.union([t.string, t.null]), - /** The responseMap */ - responseMap: t.object, - /** ID of the response. */ - responseId: t.string, - /** Content of the response. */ - response: t.union([t.string, t.null]), - /** Type of response. */ - type: t.union([ - t.literal('NOT_SURE'), - t.literal('JUSTIFICATION'), - t.literal('NOT_APPLICABLE'), - t.literal('DEFAULT'), - t.literal('OTHERS'), - ]), - /** Source from which the assessment is launched. */ - responseSourceType: t.union([ - t.literal('LAUNCH_FROM_INVENTORY'), - t.literal('FORCE_CREATED_SOURCE'), - t.null, - ]), - /** Error associated with the response. */ - errorCode: t.union([ - t.literal('ATTRIBUTE_DISABLED'), - t.literal('ATTRIBUTE_OPTION_DISABLED'), - t.literal('INVENTORY_NOT_EXISTS'), - t.literal('RELATED_INVENTORY_ATTRIBUTE_DISABLED'), - t.literal('DATA_ELEMENT_NOT_EXISTS'), - t.literal('DUPLICATE_INVENTORY'), - t.null, - ]), - /** Indicates whether the response is valid. */ - valid: t.boolean, - /** The data subject */ - dataSubject: t.union([ - t.type({ - /** The ID of the data subject */ - id: t.union([t.string, t.null]), - /** The ID of the data subject */ - name: t.union([t.string, t.null]), - /** The nameKey of the data category */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - /** The data category */ - dataCategory: t.union([ - t.type({ - /** The ID of the data category */ - id: t.union([t.string, t.null]), - /** The name of the data category */ - name: t.union([t.string, t.null]), - /** The nameKey of the data category */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - /** The data element */ - dataElement: t.union([ - t.type({ - /** The ID of the data element */ - id: t.union([t.string, t.null]), - /** The name of the data element */ - name: t.union([t.string, t.null]), - /** The name key of the data element */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - }), + t.intersection([ + t.partial({ + /** The maturityScale */ + maturityScale: t.union([t.string, t.null]), + /** The effectivenessScale */ + effectivenessScale: t.union([t.string, t.null]), + /** The parentAssessmentDetailId */ + parentAssessmentDetailId: t.union([t.string, t.null]), + }), + t.type({ + /** The controlResponse */ + controlResponse: t.union([t.string, t.null]), + /** The relationshipResponseDetails */ + relationshipResponseDetails: t.array(t.string), + /** The textRedacted */ + textRedacted: t.boolean, + /** The responseMap */ + responseMap: t.object, + /** ID of the response. */ + responseId: t.string, + /** Content of the response. */ + response: t.union([t.string, t.null]), + /** Type of response. */ + type: t.union([ + t.literal('NOT_SURE'), + t.literal('JUSTIFICATION'), + t.literal('NOT_APPLICABLE'), + t.literal('DEFAULT'), + t.literal('OTHERS'), + ]), + /** Source from which the assessment is launched. */ + responseSourceType: t.union([ + t.literal('LAUNCH_FROM_INVENTORY'), + t.literal('FORCE_CREATED_SOURCE'), + t.null, + ]), + /** Error associated with the response. */ + errorCode: t.union([ + t.literal('ATTRIBUTE_DISABLED'), + t.literal('ATTRIBUTE_OPTION_DISABLED'), + t.literal('INVENTORY_NOT_EXISTS'), + t.literal('RELATED_INVENTORY_ATTRIBUTE_DISABLED'), + t.literal('DATA_ELEMENT_NOT_EXISTS'), + t.literal('DUPLICATE_INVENTORY'), + t.null, + ]), + /** Indicates whether the response is valid. */ + valid: t.boolean, + /** The data subject */ + dataSubject: t.union([ + t.type({ + /** The ID of the data subject */ + id: t.union([t.string, t.null]), + /** The ID of the data subject */ + name: t.union([t.string, t.null]), + /** The nameKey of the data category */ + nameKey: t.union([t.string, t.null]), + }), + t.null, + ]), + /** The data category */ + dataCategory: t.union([ + t.type({ + /** The ID of the data category */ + id: t.union([t.string, t.null]), + /** The name of the data category */ + name: t.union([t.string, t.null]), + /** The nameKey of the data category */ + nameKey: t.union([t.string, t.null]), + }), + t.null, + ]), + /** The data element */ + dataElement: t.union([ + t.type({ + /** The ID of the data element */ + id: t.union([t.string, t.null]), + /** The name of the data element */ + name: t.union([t.string, t.null]), + /** The name key of the data element */ + nameKey: t.union([t.string, t.null]), + }), + t.null, + ]), + }), + ]), ); /** Type override */ export type OneTrustAssessmentResponsesCodec = t.TypeOf< @@ -373,82 +377,86 @@ export type OneTrustAssessmentNestedQuestionFlatCodec = t.TypeOf< typeof OneTrustAssessmentNestedQuestionFlatCodec >; -export const OneTrustAssessmentQuestionCodec = t.type({ - /** The question */ - question: OneTrustAssessmentNestedQuestionCodec, - /** Indicates whether the question is hidden on the assessment. */ - hidden: t.boolean, - /** Reason for locking the question in the assessment. */ - lockReason: t.union([ - t.literal('LAUNCH_FROM_INVENTORY'), - t.literal('FORCE_CREATION_LOCK'), - t.null, - ]), - /** The copy errors */ - copyErrors: t.union([t.string, t.null]), - /** Indicates whether navigation rules are enabled for the question. */ - hasNavigationRules: t.boolean, - /** The responses to this question */ - questionResponses: t.array(OneTrustAssessmentQuestionResponseCodec), - /** The risks associated with this question */ - risks: t.union([t.array(OneTrustAssessmentQuestionRiskCodec), t.null]), - /** List of IDs associated with the question root requests. */ - rootRequestInformationIds: t.array(t.string), - /** Number of attachments added to the question. */ - totalAttachments: t.number, - /** IDs of the attachment(s) added to the question. */ - attachmentIds: t.array(t.string), - /** The canReopenWithAllowEditOption */ - canReopenWithAllowEditOption: t.boolean, - /** The riskCreationAllowed */ - riskCreationAllowed: t.boolean, - /** The riskDeletionPopupAllowed */ - riskDeletionPopupAllowed: t.boolean, - /** The allowMaturityScaleOnQuestions */ - allowMaturityScaleOnQuestions: t.boolean, - /** The questionAssociations */ - questionAssociations: t.union([t.string, t.null]), - /** The issues */ - issues: t.union([t.string, t.null]), - /** The responseEditableWhileUnderReview */ - responseEditableWhileUnderReview: t.boolean, - /** The businessKeyReference */ - businessKeyReference: t.union([t.string, t.null]), - /** The topic */ - topic: t.union([t.string, t.null]), - /** The questionLaws */ - questionLaws: t.array(t.string), - /** The attachmentRequired */ - attachmentRequired: t.boolean, - /** The responseFilter */ - responseFilter: t.union([t.string, t.null]), - /** The linkAssessmentToResponseEntity */ - linkAssessmentToResponseEntity: t.boolean, - /** The readOnly */ - readOnly: t.boolean, - /** The schema */ - schema: t.union([t.string, t.null]), - /** The attributeId */ - attributeId: t.string, - /** Whether it is a vendor question */ - vendorQuestion: t.boolean, - /** Whether the question was seeded */ - seeded: t.boolean, - /** Whether the question allows justification */ - allowJustification: t.boolean, - /** Whether it refers to an asset question */ - assetQuestion: t.boolean, - /** Whether it refers to an entity question */ - entityQuestion: t.boolean, - /** Whether it is a paquestion */ - paquestion: t.boolean, - /** The inventoryTypeEnum */ - inventoryTypeEnum: t.union([t.string, t.null]), - /** Whether it is a forceOther */ - forceOther: t.boolean, - /** Whether it is a isParentQuestionMultiSelect */ - isParentQuestionMultiSelect: t.boolean, -}); +export const OneTrustAssessmentQuestionCodec = t.intersection([ + t.type({ + /** The question */ + question: OneTrustAssessmentNestedQuestionCodec, + /** Indicates whether the question is hidden on the assessment. */ + hidden: t.boolean, + /** Reason for locking the question in the assessment. */ + lockReason: t.union([ + t.literal('LAUNCH_FROM_INVENTORY'), + t.literal('FORCE_CREATION_LOCK'), + t.null, + ]), + /** The copy errors */ + copyErrors: t.union([t.string, t.null]), + /** Indicates whether navigation rules are enabled for the question. */ + hasNavigationRules: t.boolean, + /** The responses to this question */ + questionResponses: t.array(OneTrustAssessmentQuestionResponseCodec), + /** The risks associated with this question */ + risks: t.union([t.array(OneTrustAssessmentQuestionRiskCodec), t.null]), + /** List of IDs associated with the question root requests. */ + rootRequestInformationIds: t.array(t.string), + /** Number of attachments added to the question. */ + totalAttachments: t.number, + /** IDs of the attachment(s) added to the question. */ + attachmentIds: t.array(t.string), + /** The canReopenWithAllowEditOption */ + canReopenWithAllowEditOption: t.boolean, + /** The riskCreationAllowed */ + riskCreationAllowed: t.boolean, + /** The riskDeletionPopupAllowed */ + riskDeletionPopupAllowed: t.boolean, + /** The allowMaturityScaleOnQuestions */ + allowMaturityScaleOnQuestions: t.boolean, + /** The questionAssociations */ + questionAssociations: t.union([t.string, t.null]), + /** The issues */ + issues: t.union([t.string, t.null]), + /** The responseEditableWhileUnderReview */ + responseEditableWhileUnderReview: t.boolean, + }), + t.partial({ + /** The businessKeyReference */ + businessKeyReference: t.union([t.string, t.null]), + /** The topic */ + topic: t.union([t.string, t.null]), + /** The questionLaws */ + questionLaws: t.array(t.string), + /** The attachmentRequired */ + attachmentRequired: t.boolean, + /** The responseFilter */ + responseFilter: t.union([t.string, t.null]), + /** The linkAssessmentToResponseEntity */ + linkAssessmentToResponseEntity: t.boolean, + /** The readOnly */ + readOnly: t.boolean, + /** The schema */ + schema: t.union([t.string, t.null]), + /** The attributeId */ + attributeId: t.string, + /** Whether it is a vendor question */ + vendorQuestion: t.boolean, + /** Whether the question was seeded */ + seeded: t.boolean, + /** Whether the question allows justification */ + allowJustification: t.boolean, + /** Whether it refers to an asset question */ + assetQuestion: t.boolean, + /** Whether it refers to an entity question */ + entityQuestion: t.boolean, + /** Whether it is a paquestion */ + paquestion: t.boolean, + /** The inventoryTypeEnum */ + inventoryTypeEnum: t.union([t.string, t.null]), + /** Whether it is a forceOther */ + forceOther: t.boolean, + /** Whether it is a isParentQuestionMultiSelect */ + isParentQuestionMultiSelect: t.boolean, + }), +]); /** Type override */ export type OneTrustAssessmentQuestionCodec = t.TypeOf< @@ -458,14 +466,16 @@ export type OneTrustAssessmentQuestionCodec = t.TypeOf< // TODO: do not add to privacy types // The OneTrustAssessmentQuestionCodec without nested properties export const OneTrustAssessmentQuestionFlatCodec = t.type({ - hidden: OneTrustAssessmentQuestionCodec.props.hidden, - lockReason: OneTrustAssessmentQuestionCodec.props.lockReason, - copyErrors: OneTrustAssessmentQuestionCodec.props.copyErrors, - hasNavigationRules: OneTrustAssessmentQuestionCodec.props.hasNavigationRules, + hidden: OneTrustAssessmentQuestionCodec.types[0].props.hidden, + lockReason: OneTrustAssessmentQuestionCodec.types[0].props.lockReason, + copyErrors: OneTrustAssessmentQuestionCodec.types[0].props.copyErrors, + hasNavigationRules: + OneTrustAssessmentQuestionCodec.types[0].props.hasNavigationRules, rootRequestInformationIds: - OneTrustAssessmentQuestionCodec.props.rootRequestInformationIds, - totalAttachments: OneTrustAssessmentQuestionCodec.props.totalAttachments, - attachmentIds: OneTrustAssessmentQuestionCodec.props.attachmentIds, + OneTrustAssessmentQuestionCodec.types[0].props.rootRequestInformationIds, + totalAttachments: + OneTrustAssessmentQuestionCodec.types[0].props.totalAttachments, + attachmentIds: OneTrustAssessmentQuestionCodec.types[0].props.attachmentIds, }); /** Type override */ @@ -737,14 +747,18 @@ export const OneTrustGetAssessmentResponseCodec = t.type({ /** Number of open risks that have not been addressed. */ openRiskCount: t.number, /** The organization group */ - orgGroup: t.type({ - /** The ID of the organization group */ - id: t.string, - /** The name of the organization group */ - name: t.string, - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), + orgGroup: t.intersection([ + t.type({ + /** The ID of the organization group */ + id: t.string, + /** The name of the organization group */ + name: t.string, + }), + t.partial({ + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), + }), + ]), /** The primary record */ primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec, /** Type of inventory record designated as the primary record. */ @@ -944,14 +958,18 @@ export const OneTrustGetRiskResponseCodec = t.type({ /** Integer risk identifier. */ number: t.number, /** The organization group */ - orgGroup: t.type({ - /** ID of an entity. */ - id: t.string, - /** Name of an entity. */ - name: t.string, - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), + orgGroup: t.intersection([ + t.type({ + /** The ID of the organization group */ + id: t.string, + /** The name of the organization group */ + name: t.string, + }), + t.partial({ + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), + }), + ]), /** The previous risk state */ previousState: t.union([ t.literal('IDENTIFIED'), @@ -1141,7 +1159,7 @@ export type OneTrustEnrichedRisksCodec = t.TypeOf< // TODO: do not add to privacy-types export const OneTrustEnrichedAssessmentQuestionCodec = t.type({ - ...OneTrustAssessmentQuestionCodec.props, + ...OneTrustAssessmentQuestionCodec.types[0].props, risks: OneTrustEnrichedRisksCodec, }); /** Type override */ diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index baa041bc..e644975f 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -284,6 +284,9 @@ export const flattenOneTrustAssessment = ( const combinedAssessmentFlat = flatten(combinedAssessmentWithDefaults); // const defaultAssessmentFlat = flatten(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT); + // console.log({ + // defaultAssessmentFlat, + // }); return combinedAssessmentFlat; }; /** From 8480eaf521bd9bff39d92ce1e4ab737a30fe1b8a Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 02:57:30 +0000 Subject: [PATCH 35/79] undo changes to cli-pull-ot --- src/cli-pull-ot.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index aa6abf7b..1116a817 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -35,11 +35,8 @@ async function main(): Promise { // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); - // TODO: undo - const newAssessments = assessments.slice(3); - // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory - await mapSeries(newAssessments, async (assessment, index) => { + await mapSeries(assessments, async (assessment, index) => { logger.info( `Fetching details about assessment ${index + 1} of ${ assessments.length From bd5842c6a0d3241babad9bbbd977ce2a8c57c5b2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 03:32:12 +0000 Subject: [PATCH 36/79] improve codecs --- src/oneTrust/codecs.ts | 245 +++++++++++++--------- src/oneTrust/flattenOneTrustAssessment.ts | 15 +- 2 files changed, 162 insertions(+), 98 deletions(-) diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index eb2b0757..e04b2c59 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -100,33 +100,40 @@ export type OneTrustGetListOfAssessmentsResponseCodec = t.TypeOf< typeof OneTrustGetListOfAssessmentsResponseCodec >; -export const OneTrustAssessmentQuestionOptionCodec = t.type({ - /** ID of the option. */ - id: t.string, - /** Name of the option. */ - option: t.string, - /** The key of the option */ - optionKey: t.union([t.string, t.null]), - /** The hint */ - hint: t.union([t.string, t.null]), - /** The hint key */ - hintKey: t.union([t.string, t.null]), - /** The score */ - score: t.union([t.number, t.null]), - /** If the option was pre-selected */ - preSelectedOption: t.boolean, - /** Order in which the option appears. */ - sequence: t.union([t.number, t.null]), - /** Attribute for which the option is available. */ - attributes: t.union([t.string, t.null]), - /** Type of option. */ - optionType: t.union([ - t.literal('NOT_SURE'), - t.literal('NOT_APPLICABLE'), - t.literal('OTHERS'), - t.literal('DEFAULT'), - ]), -}); +export const OneTrustAssessmentQuestionOptionCodec = t.intersection([ + t.partial({ + /** The translationIdentifier */ + translationIdentifier: t.string, + }), + t.type({ + /** ID of the option. */ + id: t.string, + /** Name of the option. */ + option: t.string, + /** The key of the option */ + optionKey: t.union([t.string, t.null]), + /** The hint */ + hint: t.union([t.string, t.null]), + /** The hint key */ + hintKey: t.union([t.string, t.null]), + /** The score */ + score: t.union([t.number, t.null]), + /** If the option was pre-selected */ + preSelectedOption: t.boolean, + /** Order in which the option appears. */ + sequence: t.union([t.number, t.null]), + /** Attribute for which the option is available. */ + attributes: t.union([t.string, t.null]), + /** Type of option. */ + optionType: t.union([ + t.literal('NOT_SURE'), + t.literal('NOT_APPLICABLE'), + t.literal('OTHERS'), + t.literal('DEFAULT'), + ]), + }), +]); + /** Type override */ export type OneTrustAssessmentQuestionOptionCodec = t.TypeOf< typeof OneTrustAssessmentQuestionOptionCodec @@ -174,6 +181,18 @@ export const OneTrustAssessmentResponsesCodec = t.array( effectivenessScale: t.union([t.string, t.null]), /** The parentAssessmentDetailId */ parentAssessmentDetailId: t.union([t.string, t.null]), + /** The display label */ + displayLabel: t.string, + /** The type of the parent question */ + parentQuestionType: t.string, + /** The ID of the parent response */ + parentResponseId: t.string, + /** Whether it's local version */ + isLocalVersion: t.string, + /** Whether relationshipDisplayInformation */ + relationshipDisplayInformation: t.union([t.string, t.null]), + /** The lock reason */ + lockReason: t.union([t.string, t.null]), }), t.type({ /** The controlResponse */ @@ -188,6 +207,10 @@ export const OneTrustAssessmentResponsesCodec = t.array( responseId: t.string, /** Content of the response. */ response: t.union([t.string, t.null]), + /** The response key */ + responseKey: t.union([t.string, t.null]), + /** The response key */ + contractResponse: t.union([t.string, t.null]), /** Type of response. */ type: t.union([ t.literal('NOT_SURE'), @@ -431,6 +454,8 @@ export const OneTrustAssessmentQuestionCodec = t.intersection([ responseFilter: t.union([t.string, t.null]), /** The linkAssessmentToResponseEntity */ linkAssessmentToResponseEntity: t.boolean, + /** The translationIdentifier */ + translationIdentifier: t.string, /** The readOnly */ readOnly: t.boolean, /** The schema */ @@ -500,36 +525,43 @@ export type OneTrustAssessmentSectionHeaderRiskStatisticsCodec = t.TypeOf< typeof OneTrustAssessmentSectionHeaderRiskStatisticsCodec >; -export const OneTrustAssessmentSectionHeaderCodec = t.type({ - /** ID of the section in the assessment. */ - sectionId: t.string, - /** Name of the section. */ - name: t.string, - /** The status of the section */ - status: t.union([t.string, t.null]), - /** The openNMIQuestionIds */ - openNMIQuestionIds: t.union([t.string, t.null]), - /** Description of the section header. */ - description: t.union([t.string, t.null]), - /** Sequence of the section within the form */ - sequence: t.number, - /** Indicates whether the section is hidden in the assessment. */ - hidden: t.boolean, - /** IDs of invalid questions in the section. */ - invalidQuestionIds: t.array(t.string), - /** IDs of required but unanswered questions in the section. */ - requiredUnansweredQuestionIds: t.array(t.string), - /** IDs of required questions in the section. */ - requiredQuestionIds: t.array(t.string), - /** IDs of unanswered questions in the section. */ - unansweredQuestionIds: t.array(t.string), - /** IDs of effectiveness questions in the section. */ - effectivenessQuestionIds: t.array(t.string), - /** The risk statistics */ - riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec, - /** Whether the section was submitted */ - submitted: t.boolean, -}); +export const OneTrustAssessmentSectionHeaderCodec = t.intersection([ + t.type({ + /** ID of the section in the assessment. */ + sectionId: t.string, + /** Name of the section. */ + name: t.string, + /** The status of the section */ + status: t.union([t.string, t.null]), + /** The openNMIQuestionIds */ + openNMIQuestionIds: t.union([t.string, t.null]), + /** Description of the section header. */ + description: t.union([t.string, t.null]), + /** Sequence of the section within the form */ + sequence: t.number, + /** Indicates whether the section is hidden in the assessment. */ + hidden: t.boolean, + /** IDs of invalid questions in the section. */ + invalidQuestionIds: t.array(t.string), + /** IDs of required but unanswered questions in the section. */ + requiredUnansweredQuestionIds: t.array(t.string), + /** IDs of required questions in the section. */ + requiredQuestionIds: t.array(t.string), + /** IDs of unanswered questions in the section. */ + unansweredQuestionIds: t.array(t.string), + /** IDs of effectiveness questions in the section. */ + effectivenessQuestionIds: t.array(t.string), + /** The risk statistics */ + riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec, + /** Whether the section was submitted */ + submitted: t.boolean, + }), + t.partial({ + /** The name key of the template */ + nameKey: t.union([t.string, t.null]), + }), +]); + /** Type override */ export type OneTrustAssessmentSectionHeaderCodec = t.TypeOf< typeof OneTrustAssessmentSectionHeaderCodec @@ -538,22 +570,24 @@ export type OneTrustAssessmentSectionHeaderCodec = t.TypeOf< // TODO: do not add to privacy-types /** The OneTrustAssessmentSectionHeaderCodec without nested riskStatistics */ export const OneTrustAssessmentSectionFlatHeaderCodec = t.type({ - sectionId: OneTrustAssessmentSectionHeaderCodec.props.sectionId, - name: OneTrustAssessmentSectionHeaderCodec.props.name, - description: OneTrustAssessmentSectionHeaderCodec.props.description, - sequence: OneTrustAssessmentSectionHeaderCodec.props.sequence, - hidden: OneTrustAssessmentSectionHeaderCodec.props.hidden, + sectionId: OneTrustAssessmentSectionHeaderCodec.types[0].props.sectionId, + name: OneTrustAssessmentSectionHeaderCodec.types[0].props.name, + description: OneTrustAssessmentSectionHeaderCodec.types[0].props.description, + sequence: OneTrustAssessmentSectionHeaderCodec.types[0].props.sequence, + hidden: OneTrustAssessmentSectionHeaderCodec.types[0].props.hidden, invalidQuestionIds: - OneTrustAssessmentSectionHeaderCodec.props.invalidQuestionIds, + OneTrustAssessmentSectionHeaderCodec.types[0].props.invalidQuestionIds, requiredUnansweredQuestionIds: - OneTrustAssessmentSectionHeaderCodec.props.requiredUnansweredQuestionIds, + OneTrustAssessmentSectionHeaderCodec.types[0].props + .requiredUnansweredQuestionIds, requiredQuestionIds: - OneTrustAssessmentSectionHeaderCodec.props.requiredQuestionIds, + OneTrustAssessmentSectionHeaderCodec.types[0].props.requiredQuestionIds, unansweredQuestionIds: - OneTrustAssessmentSectionHeaderCodec.props.unansweredQuestionIds, + OneTrustAssessmentSectionHeaderCodec.types[0].props.unansweredQuestionIds, effectivenessQuestionIds: - OneTrustAssessmentSectionHeaderCodec.props.effectivenessQuestionIds, - submitted: OneTrustAssessmentSectionHeaderCodec.props.submitted, + OneTrustAssessmentSectionHeaderCodec.types[0].props + .effectivenessQuestionIds, + submitted: OneTrustAssessmentSectionHeaderCodec.types[0].props.submitted, }); /** Type override */ export type OneTrustAssessmentSectionFlatHeaderCodec = t.TypeOf< @@ -561,12 +595,18 @@ export type OneTrustAssessmentSectionFlatHeaderCodec = t.TypeOf< >; export const OneTrustAssessmentSectionSubmittedByCodec = t.union([ - t.type({ - /** The ID of the user who submitted the section */ - id: t.string, - /** THe name or email of the user who submitted the section */ - name: t.string, - }), + t.intersection([ + t.type({ + /** The ID of the user who submitted the section */ + id: t.string, + /** THe name or email of the user who submitted the section */ + name: t.string, + }), + t.partial({ + /** The name key */ + nameKey: t.union([t.string, t.null]), + }), + ]), t.null, ]); @@ -882,14 +922,19 @@ const RiskLevelCodec = t.type({ }); export const OneTrustRiskCategories = t.array( - t.type({ - /** Identifier for Risk Category. */ - id: t.string, - /** Risk Category Name. */ - name: t.string, - /** Risk Category Name Key value for translation. */ - nameKey: t.string, - }), + t.intersection([ + t.partial({ + seeded: t.boolean, + }), + t.type({ + /** Identifier for Risk Category. */ + id: t.string, + /** Risk Category Name. */ + name: t.string, + /** Risk Category Name Key value for translation. */ + nameKey: t.string, + }), + ]), ); /** Type override */ export type OneTrustRiskCategories = t.TypeOf; @@ -1041,14 +1086,22 @@ export const OneTrustGetRiskResponseCodec = t.type({ ]), }), /** The risk stage */ - stage: t.type({ - /** ID of an entity. */ - id: t.string, - /** Name of an entity. */ - name: t.string, - /** Name Key of the entity for translation. */ - nameKey: t.string, - }), + stage: t.intersection([ + t.partial({ + /** The currentStageApprovers */ + currentStageApprovers: t.array(t.string), + /** The badgeColor */ + badgeColor: t.union([t.string, t.null]), + }), + t.type({ + /** ID of an entity. */ + id: t.string, + /** Name of an entity. */ + name: t.string, + /** Name Key of the entity for translation. */ + nameKey: t.string, + }), + ]), /** The risk state */ state: t.union([ t.literal('IDENTIFIED'), @@ -1158,10 +1211,14 @@ export type OneTrustEnrichedRisksCodec = t.TypeOf< >; // TODO: do not add to privacy-types -export const OneTrustEnrichedAssessmentQuestionCodec = t.type({ - ...OneTrustAssessmentQuestionCodec.types[0].props, - risks: OneTrustEnrichedRisksCodec, -}); +export const OneTrustEnrichedAssessmentQuestionCodec = t.intersection([ + t.type({ + ...OneTrustAssessmentQuestionCodec.types[0].props, + risks: OneTrustEnrichedRisksCodec, + }), + t.partial({ ...OneTrustAssessmentQuestionCodec.types[1].props }), +]); + /** Type override */ export type OneTrustEnrichedAssessmentQuestionCodec = t.TypeOf< typeof OneTrustEnrichedAssessmentQuestionCodec diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index e644975f..361ece51 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -15,6 +15,7 @@ import { OneTrustEnrichedRiskCodec, OneTrustRiskCategories, } from './codecs'; +import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // TODO: will have to use something like csv-stringify @@ -282,11 +283,17 @@ export const flattenOneTrustAssessment = ( enrichCombinedAssessmentWithDefaults(combinedAssessment); const combinedAssessmentFlat = flatten(combinedAssessmentWithDefaults); - // const defaultAssessmentFlat = flatten(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT); + const defaultAssessmentFlat = flatten(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT); - // console.log({ - // defaultAssessmentFlat, - // }); + // TODO: test that both have the same keys + const keysOne = Object.keys(combinedAssessmentFlat); + const keysTwo = Object.keys(defaultAssessmentFlat); + + const keysInCombinedOnly = keysOne.filter((k) => !keysTwo.includes(k)); + + console.log({ + keysInCombinedOnly, + }); return combinedAssessmentFlat; }; /** From b26cb56f23d7008425f70295c4d8c8afde632d7c Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 03:50:20 +0000 Subject: [PATCH 37/79] done --- src/oneTrust/constants.ts | 7 +- src/oneTrust/flattenOneTrustAssessment.ts | 87 +++++++---------------- src/oneTrust/writeOneTrustAssessment.ts | 20 ++++-- 3 files changed, 49 insertions(+), 65 deletions(-) diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts index 47738c8b..bb34c2d2 100644 --- a/src/oneTrust/constants.ts +++ b/src/oneTrust/constants.ts @@ -1,10 +1,15 @@ import { createDefaultCodec } from '../helpers'; import { OneTrustCombinedAssessmentCodec } from './codecs'; +import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; /** * An object with default values of type OneTrustCombinedAssessmentCodec. It's very * valuable when converting assessments to CSV. When we flatten it, the resulting * value always contains all keys that eventually we add to the header. */ -export const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustCombinedAssessmentCodec = +const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustCombinedAssessmentCodec = createDefaultCodec(OneTrustCombinedAssessmentCodec); + +export const DEFAULT_ONE_TRUST_ASSESSMENT_CSV_KEYS = Object.keys( + flattenOneTrustAssessment(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT), +); diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 361ece51..6fd45cbb 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -15,7 +15,6 @@ import { OneTrustEnrichedRiskCodec, OneTrustRiskCategories, } from './codecs'; -import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // TODO: will have to use something like csv-stringify @@ -240,68 +239,36 @@ const flattenOneTrustSections = ( export const flattenOneTrustAssessment = ( combinedAssessment: OneTrustCombinedAssessmentCodec, ): any => { - /** - * TODO: experiment creating a default assessment with - * const result = createDefaultCodec(OneTrustGetAssessmentResponseCodec); - * Then, flatten it and aggregate it with the actual assessment. This way, every - * assessment will always have the same fields! - */ - - const flatten = (assessment: OneTrustCombinedAssessmentCodec): any => { - const { - approvers, - primaryEntityDetails, - respondents, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - respondent, - sections, - ...rest - } = assessment; - - const flatApprovers = approvers.map((approver) => - flattenObject(approver, 'approvers'), - ); - const flatRespondents = respondents.map((respondent) => - flattenObject(respondent, 'respondents'), - ); - const flatPrimaryEntityDetails = primaryEntityDetails.map( - (primaryEntityDetail) => - flattenObject(primaryEntityDetail, 'primaryEntityDetails'), - ); - - return { - ...flattenObject(rest), - ...aggregateObjects({ objs: flatApprovers }), - ...aggregateObjects({ objs: flatRespondents }), - ...aggregateObjects({ objs: flatPrimaryEntityDetails }), - ...flattenOneTrustSections(sections, 'sections'), - }; - }; - // add default values to assessments const combinedAssessmentWithDefaults = enrichCombinedAssessmentWithDefaults(combinedAssessment); - const combinedAssessmentFlat = flatten(combinedAssessmentWithDefaults); - const defaultAssessmentFlat = flatten(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT); - - // TODO: test that both have the same keys - const keysOne = Object.keys(combinedAssessmentFlat); - const keysTwo = Object.keys(defaultAssessmentFlat); - - const keysInCombinedOnly = keysOne.filter((k) => !keysTwo.includes(k)); + const { + approvers, + primaryEntityDetails, + respondents, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + respondent, + sections, + ...rest + } = combinedAssessmentWithDefaults; + + const flatApprovers = approvers.map((approver) => + flattenObject(approver, 'approvers'), + ); + const flatRespondents = respondents.map((respondent) => + flattenObject(respondent, 'respondents'), + ); + const flatPrimaryEntityDetails = primaryEntityDetails.map( + (primaryEntityDetail) => + flattenObject(primaryEntityDetail, 'primaryEntityDetails'), + ); - console.log({ - keysInCombinedOnly, - }); - return combinedAssessmentFlat; + return { + ...flattenObject(rest), + ...aggregateObjects({ objs: flatApprovers }), + ...aggregateObjects({ objs: flatRespondents }), + ...aggregateObjects({ objs: flatPrimaryEntityDetails }), + ...flattenOneTrustSections(sections, 'sections'), + }; }; -/** - * - * - * TODO: convert to camelCase -> Title Case - * TODO: section -> header is spread - * TODO: section -> questions -> question is spread - * TODO: section -> questions -> question -> questionOptions are aggregated - * TODO: section -> questions -> question -> questionResponses -> responses are spread - */ diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index 1372a7e2..da9f9747 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -9,6 +9,7 @@ import { } from './codecs'; import fs from 'fs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; +import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_KEYS } from './constants'; /** * Write the assessment to disk at the specified file path. @@ -114,21 +115,32 @@ export const writeOneTrustAssessment = ({ fs.writeFileSync('./oneTrust.json', '[\n'); } - const flattened = flattenOneTrustAssessment({ + // flatten the assessment object so it does not have nested properties + const flatAssessment = flattenOneTrustAssessment({ ...assessment, ...enrichedAssessment, }); - const stringifiedFlattened = JSON.stringify(flattened, null, 2); - // TODO: do not forget to ensure we have the same set of keys!!! + + // transform the flat assessment to have all CSV keys in the expected order + const flatAssessmentWithCsvKeys = + DEFAULT_ONE_TRUST_ASSESSMENT_CSV_KEYS.reduce( + (acc, key) => ({ + ...acc, + [key]: flatAssessment[key] ?? '', + }), + {}, + ); + const csvEntry = JSON.stringify(flatAssessmentWithCsvKeys, null, 2); // const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); // Add comma for all items except the last one const comma = index < total - 1 ? ',' : ''; + // TODO: might be better not to convert it to CSV at all! The importOneTrustAssessments does not actually accept CSV. // write to file // fs.appendFileSync(file, stringifiedAssessment + comma); - fs.appendFileSync('./oneTrust.json', stringifiedFlattened + comma); + fs.appendFileSync('./oneTrust.json', csvEntry + comma); // end with closing bracket if (index === total - 1) { From 3ac69be2b4a793327cd949e3150e9c0ef9f29b49 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 04:26:30 +0000 Subject: [PATCH 38/79] update writeOneTrustAssessment to write in csv format --- src/oneTrust/codecs.ts | 2 +- src/oneTrust/constants.ts | 7 +-- src/oneTrust/writeOneTrustAssessment.ts | 63 +++++++++++-------------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index e04b2c59..441ab96f 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import * as t from 'io-ts'; -// TODO: move all to privacy-types +// TODO: move to privacy-types export const OneTrustAssessmentCodec = t.type({ /** ID of the assessment. */ diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts index bb34c2d2..3daa79e9 100644 --- a/src/oneTrust/constants.ts +++ b/src/oneTrust/constants.ts @@ -4,12 +4,13 @@ import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; /** * An object with default values of type OneTrustCombinedAssessmentCodec. It's very - * valuable when converting assessments to CSV. When we flatten it, the resulting - * value always contains all keys that eventually we add to the header. + * valuable when converting assessments to CSV, as it contains all keys that + * make up the CSV header in the expected order */ const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustCombinedAssessmentCodec = createDefaultCodec(OneTrustCombinedAssessmentCodec); -export const DEFAULT_ONE_TRUST_ASSESSMENT_CSV_KEYS = Object.keys( +/** The header of the OneTrust ASsessment CSV file */ +export const DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER = Object.keys( flattenOneTrustAssessment(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT), ); diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index da9f9747..530f3f0c 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -9,7 +9,7 @@ import { } from './codecs'; import fs from 'fs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; -import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_KEYS } from './constants'; +import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; /** * Write the assessment to disk at the specified file path. @@ -96,23 +96,24 @@ export const writeOneTrustAssessment = ({ fs.writeFileSync(file, '[\n'); } - // const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); + const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); - // // Add comma for all items except the last one - // const comma = index < total - 1 ? ',' : ''; + // Add comma for all items except the last one + const comma = index < total - 1 ? ',' : ''; - // // write to file - // fs.appendFileSync(file, stringifiedAssessment + comma); + // write to file + fs.appendFileSync(file, stringifiedAssessment + comma); - // // end with closing bracket - // if (index === total - 1) { - // fs.appendFileSync(file, ']'); - // } + // end with closing bracket + if (index === total - 1) { + fs.appendFileSync(file, ']'); + } } else if (fileFormat === OneTrustFileFormat.Csv) { - // flatten the json object - // start with an opening bracket + const csvRows = []; + + // write csv header at the beginning of the file if (index === 0) { - fs.writeFileSync('./oneTrust.json', '[\n'); + csvRows.push(DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.join(',')); } // flatten the assessment object so it does not have nested properties @@ -122,29 +123,21 @@ export const writeOneTrustAssessment = ({ }); // transform the flat assessment to have all CSV keys in the expected order - const flatAssessmentWithCsvKeys = - DEFAULT_ONE_TRUST_ASSESSMENT_CSV_KEYS.reduce( - (acc, key) => ({ - ...acc, - [key]: flatAssessment[key] ?? '', - }), - {}, - ); - const csvEntry = JSON.stringify(flatAssessmentWithCsvKeys, null, 2); + const assessmentRow = DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map( + (header) => { + const value = flatAssessment[header] ?? ''; + // Escape values containing commas or quotes + return typeof value === 'string' && + (value.includes(',') || value.includes('"')) + ? `"${value.replace(/"/g, '""')}"` + : value; + }, + ); - // const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); + // append the rows to the file + csvRows.push(`${assessmentRow.join(',')}\n`); + fs.appendFileSync('./oneTrust.csv', csvRows.join('\n')); - // Add comma for all items except the last one - const comma = index < total - 1 ? ',' : ''; - - // TODO: might be better not to convert it to CSV at all! The importOneTrustAssessments does not actually accept CSV. - // write to file - // fs.appendFileSync(file, stringifiedAssessment + comma); - fs.appendFileSync('./oneTrust.json', csvEntry + comma); - - // end with closing bracket - if (index === total - 1) { - fs.appendFileSync('./oneTrust.json', ']'); - } + // TODO: consider not to convert it to CSV at all! The importOneTrustAssessments does not actually accept CSV. } }; From f9be7b6e334c0a78d7497d73506005af5877395e Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 20:37:49 +0000 Subject: [PATCH 39/79] import assessment types from privacy types --- .pnp.cjs | 10 +- ...ypes-npm-4.103.0-b8d1864632-4661368b34.zip | Bin 379125 -> 0 bytes package.json | 2 +- src/cli-pull-ot.ts | 15 +- src/helpers/enrichWithDefault.ts | 66 +- src/helpers/tests/createDefaultCodec.test.ts | 5 + src/oneTrust/codecs.ts | 1307 ++--------------- src/oneTrust/constants.ts | 8 +- src/oneTrust/flattenOneTrustAssessment.ts | 41 +- src/oneTrust/getListOfOneTrustAssessments.ts | 14 +- src/oneTrust/getOneTrustAssessment.ts | 6 +- src/oneTrust/getOneTrustRisk.ts | 6 +- src/oneTrust/writeOneTrustAssessment.ts | 42 +- yarn.lock | 10 +- 14 files changed, 234 insertions(+), 1298 deletions(-) delete mode 100644 .yarn/cache/@transcend-io-privacy-types-npm-4.103.0-b8d1864632-4661368b34.zip diff --git a/.pnp.cjs b/.pnp.cjs index 9695ff8c..202d28f6 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -32,7 +32,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/handlebars-utils", "npm:1.1.0"],\ ["@transcend-io/internationalization", "npm:1.6.0"],\ ["@transcend-io/persisted-state", "npm:1.0.4"],\ - ["@transcend-io/privacy-types", "npm:4.103.0"],\ + ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ ["@transcend-io/type-utils", "npm:1.5.0"],\ ["@types/bluebird", "npm:3.5.38"],\ @@ -684,7 +684,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/handlebars-utils", "npm:1.1.0"],\ ["@transcend-io/internationalization", "npm:1.6.0"],\ ["@transcend-io/persisted-state", "npm:1.0.4"],\ - ["@transcend-io/privacy-types", "npm:4.103.0"],\ + ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ ["@transcend-io/type-utils", "npm:1.5.0"],\ ["@types/bluebird", "npm:3.5.38"],\ @@ -785,10 +785,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@transcend-io/privacy-types", [\ - ["npm:4.103.0", {\ - "packageLocation": "./.yarn/cache/@transcend-io-privacy-types-npm-4.103.0-b8d1864632-4661368b34.zip/node_modules/@transcend-io/privacy-types/",\ + ["npm:4.105.0", {\ + "packageLocation": "./.yarn/cache/@transcend-io-privacy-types-npm-4.105.0-41965240e3-fa0f2af076.zip/node_modules/@transcend-io/privacy-types/",\ "packageDependencies": [\ - ["@transcend-io/privacy-types", "npm:4.103.0"],\ + ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/type-utils", "npm:1.0.5"],\ ["fp-ts", "npm:2.16.1"],\ ["io-ts", "virtual:a57afaf9d13087a7202de8c93ac4854c9e2828bad7709250829ec4c7bc9dc95ecc2858c25612aa1774c986aedc232c76957076a1da3156fd2ab63ae5551b086f#npm:2.2.21"]\ diff --git a/.yarn/cache/@transcend-io-privacy-types-npm-4.103.0-b8d1864632-4661368b34.zip b/.yarn/cache/@transcend-io-privacy-types-npm-4.103.0-b8d1864632-4661368b34.zip deleted file mode 100644 index ea860d211a5669f7e5cb8b3906048f37b7a95704..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 379125 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 HoW=5S! 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�A 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

P1}WwW6-W)!_Ky0O}Lp}vnI&p#f){e&U7 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„EkOSgYm``gtMiTt~C?{(4M>snV- z33gwXt~?hO`&Mz{*yZO52&nhxj$sO_si!2vKcw9@9X6GSEuDEP@V$JA+T1nQ%;~F#VH=pJ*T2Mr!?nyIjsJX#}Hw`o4D( z6so@Pq22Qjla-q3`%{om*(I@ED-)h(bg10Ihj(RjlDSGXHiI?x-h2A~X;uxDSNQhs z&R4OXoTg8xw8EILPC7sI!7`6PsI6KKa&b6m!2Z zGaVdFFghc(?e;C;bJXqJ+p7QG8Feg9? zBwhl8bVd}U179q!(xi)Os~lsuqbUAJz%po2mI2Jg9WEF9=--b{Mdb2fa^WyVai~(L z69JLj@%t9=pkbdRJIAcEA{6lG_zCn(BPKx3~u4ceS5m$qKm6M zg0IOeORP^Nf0Y`esh}!6m4!E5cwcwTNq8C@s6mMQt(xdl4OgWx;y@z^lq~m0gRP@B zu?fs7*lrJTCm`ymrRjNTSz@)0?-s$A0*<~;K#)c5FWuKsN^zx``g8wDbw5JKf}3E!=-g6`gF)ZP61*tBsf5 zYP&ok&4}ZSFi-NhIMqn?C`=DtM6L9Rx~1oG5*Ocz#i3c9s$ckO8sLrA^G`Qr2E107?ZZ%?wxw`+6@Yr|~+ZSS;uYP;E|K+QhP@4EI*<=uB!$n3red_FQ) zvy<)c9eSAW)bML{J6~%4X;xH+%3i03KSR&V=49);!?mviwXb8M<;gbhPVa`(+z^_L zq&r4bd^@nfZYN0}*Pf_5x?E(sP-MDZ;GO!;r(368)P(_9U9&mmt!kIlnE%iOz9D#A zcqb(q(>2wFPlRm`nMhCcsdmm@hXVCruuSh%zD;jHrPww>B-YBrp8ih8)8;&F$m5?( z{rWY!vTPIhX+K<@z_;ml>58&VAV=`%ok|4zgC?aJH1R`&55>mB{o>NIrEP*pPPkPL z_{Kee8~4B-T24p9ee(hI%?Dfcc(Q%dr7)xz`{KXwuKxJO%7P25t8L$)C0 za*^K5ovhC~oTtM|*q;n$z+nauCRC@J-7ej<+!_t(NJ)CpAAQgHmU7(b(>dE%GP(F6 z!-b~|cV+yODY)C`{nxMI)RW8jtnbBMnC|@ok|9?HSR~tx)h0~KXnu5p{lp#fei_4l zvGZznI{$-r4(ywuimd>g?DOf63(^n@(om&YK2Fs)JxQrm( zwl=0bHI!1~*4u>M_5;l~-EV&F4YO##Ty!Ue}e<>D>}9_=atja zb9-#W7&izbOEFM{QY|gr76tM6?r@)Q6?B;9z@4k%Fju!^5aBpu+E*hJj7 zAi1LT_UX-6U;Z6ra7&0GOBrAxDz#jyg=77@PO?;CXvKCRfXPvq9M06-QM0G_!8b&K zdU#Nwh$v5q@;s$@_?oBD#ux>PQNSJZQayk6^nN8kC{hFzE%()z|MK+aBkc!mTVodcBuFx}6 z{!|5$fg!m)_bpIdLZl|1z zh9V3!)}GT=k1Pi&W;mw|=ZwKsI<0M3vRWkoxu75yFv3|S&RC@qDPXvy43{{{+9~Pq zc>BzouV8OJwclG!B-0^7MH1ILRnb(KbA}2d%)M3*(~Wws4IQ*#RDe|=)15lxaN!8k3WR-el zPgb6!7pO$SjA{cjU3k`;)hwAghM+PDlc_a*yH|K&l)msw*Qe7Ipir%Z`PBXxem^+{#|`B`AzXDkE~mgCX%F@-*te&zzWh7Op1;s81BD90VU?SJHTjv+&<#28vId{;$LT2fa4(4YXk_H+3B`7`?<2_dLn zv<}gW6U>Pg6^zyaf^hsCD<#-j+$Mu@o3Q>= z5ZGPiSo<|r413=K#xR)aa$)>c*=v@1|gxVxXtyflX0SHVo$88t?N9Q57)o=%VX?q~5SP_7sX zDp+~F`&qEu{cNp+xs;>MIO;Hii3!Zac8Zju!6+IeE0GF?1WiWJgavF*GACM$0lDU7 z-&@-G;0YNzi~+`6E_eTDASLKBf-bBj>z`sIdW@mRHj+>|bt@I>6f`cQaWPG}It9g$ zQ4FDYhTbGJLWB`R7_nhF(MEE^KyF}KS6_vJk}`b47(PLYwfpCd?@f$cx^EfAE%&(s z>+YOlSne3h9o$s^d36FAuhS3UwM%?#z@^NrJ`EAkDL^y;W2kP1G)o3{G%&2rh%WySuwh&>+Fx z-Q8V+I|K-B!JVMN-QDe<_k7>Yzt*`tb2HUlT{Ydc_H_5!`w?6P>IcP{h=1UR{E($(*>Y$3&aYPp`aGELZ%FneLIy}((oN_neN>ihgUDc0!ZTG^2@~ZdaI%W?`mmLa zcg#_9&|yj7(`W(O0A+ivB?=eF6!n6Y6*W?hW_~ zx%C2XTqpSMusCt6--LdyhF_a-g^*oZpWOd3+vxQErp3e5X3(Ghoi0XqrG>(DD+|H6 zQL{#v5Y~%MbSnvgv{BPl&p^46r_q3QTl`mLX2{0shy=s9B&HBZo_sK8tM>`O-V$mMZzhtM`Md=q2T+Uo~0PzRDChy0w{Cl564b2E2sWr3^z;^0OsdCkVD7dIYE6 z5n3M@my~rd7Q{*f*;Ja^(c67_fyM2($P$;S@?w3i9O>IwzRWeN^iEt(2BOX^Aru^m zhRj8VRKuz4;VHMz;bZA;(ArZ(YEx3>MLGv&du=WUh-jv{B(hU_2dZvtV?W*}`da&% z60k?8APj_^uOzPw%yP!RT|fy*ZHPW~9dg*RW{D-mEE_4uGZ!;$ms<$Jw*Di7}%^H|yx8hlwdb}_%@-uLvqoVCozd$u(!>zxwJmJrS-M(si=2< zsppjBO((o*k*`D-AP{MfcWM!%GFIX1bh}iZF-wTmF|T;j!#7;RLj#8CLai)m;wYsw zTmw30qbeasyy$%xAI+My?!olVEj3Tbe2ud90}y7Q{Jtc~ z5~g6^{v%BKcr;Xn&527VEzTF%^id?rbl3VBi_IFiaatyE1E6b%Hjre z?)rC=5g+TJU1`E=nM>2DURAQRtoxNA97# zB-!pz%?>WaLi#BU!Su zI4GK4+0O&HmwJH#aR)Y*y8~zmuf`iq#@_^!SF}0EG5z>5%#rK@Xf)8!KVV01^%Knu zonF)H+iv89eT^Y{#U0sL0K@kog(mnULCJySJhhK+46~0}$f*6Jy<|j7+=R?+xWb_6 ztD|h>nC@o?r!03%e3J0wz#lxDy&hVf!QQiW8e!pkAQPE5!YY+tDxN$_uwlDxgv^Um zt{5k;F7Eyx+w+f_znv)Mriz`|Sm+O*!;P-)-YHYa9N$AkdFjNeseXv!lcXdEit>7g zcrzmlAi$tbjP~~Cpkfs@Y?<=n zA?a$d2%{HQ+IAmrCS%%E#!tCE2m>BdfvK4%1}42q11?j>DVCg)LvdNs|6ND$^D_b6 zaWn*-XW;5to9#1OdIMy#H_3tko%?ei%@uk{9?y$isi|ZHHYl1Zrn3Rym5F18<_@zY zi|5b2$UaL2gW56LTc6!v6m4|%&0|>+Hf%lS{zEVzBtZUaOc#E3=2>&!S9>;=9gFW5 zl8`v~Bv#3Rfn2+e=n;8kAdhnJ@vAY((#jspz2bIkEDHxplZsEpOj%w&NZD(J7qW7di6U%eYrx3`(`@jld4`{p7Z zY)K%J>FQ$ykIze!zUv6QP^tIQ(NI&Q2g3D$Os?d!5+z(hC(Mo6SRVGp<*+iBdUwI? zk5ZdARD_bfX*hNPENWPd zXfrk*GAU-?7S9jORr0^6%G26PS%wO8%mQN6!CyfxSx>c>kxB*vGStw)ATQg^L9zV< zQ?^X!n~~W^9Jc%*xG~VwHKz3E?5$*1Zz(41*qyk8nNK8pAM`i2!@a;=sZskePY(sy zuwmqdUADi<_yBEXJhI#S*NXXa8r0O$UKRYMwH_$pA$fihdhoxUtu`}M4sSAbdzK9! zk7<`c*f)_(4E)*v{cVm0V~j5HpFIgva^Nf*X`XdT(0c?A;bFEkqu2JtBRXZ0L%l!> zwR51bymwU#uJ`r|Pt4@I#Dc1UmZ{35N@S4=yFd;#^ni7ksR`Gyd|5*jC7ywfsmLUY z50-D}{x%0bo>J52Me0cfvp3Xmu?-tbrNvq3B7eG1;-M%fKAr|UiY6I<$#p>%GIFKo zN9m2^^rwf0BCOP3fzLU_a!S|lVZ(nxCI*ULL%^CZ3EQUNF&Cyckd3}zt$v)WDEir;}=GEWO9WgM0lxa8jTAcW_8cy$~aTan8d&-5Q8?39Ky2V17}6(5wbtLLp!C%}#e;*WL`!b~mu>p5Sin%qIq$0c&+ z{S=xXV}!0f5VT%ed|5Q>etyK<47xJpdjb;pXdjaaHHjA#@8W|Yw(@bFHk_*J$$+G9 zJl|EqV9j)n)_dT8Z^hLH@;Ad?+zHI~xtPXnz&@v9ThicjJsbGuF<0Kc#2P=FTk7bgD$UR96<6Ex?L}rX~0z z0TneBq3R;VlK;e*N$SugU*n#5Uwa=!33WE-dX!f1)dU;NU@_!FHRM2D56$w(yQ~WJ z0RDCX{*Hx~4Y{2?S4|@6v`smS5V!GJ4z_kYXbL?=w#-)djK+6Kxk=^Ac{K`Vi z8)NF>mZke4UuAdvGOHDSBFV(+ApR%m$q=?{We6B+-)zxYJ%J z5blG@kF8YG?`VUaaYG&7r$a7SS@-U@%Ru`|5#=YWl%oi7~2l+P4 zmy>IcTOjB31u<%*VmN?UZl({QBt4D!00E$wkrlCj2^s;o$s)oV$ZaJlIi~hB_;X{4 z#Lx|9h{W745Ma0rmXG5}8Fms?Xa9b!FcFPAQi+*Si76UXHVdTum!}yC{K>QxT%H<* zQ`hjW5Aqi|N8R!+zbY={hPrKCcw^$|`9geu^sgfP$|FK5Qj{YBnC5d>#r!ELX)_8H zK?cZ$zd9|UnyEUR20W}S_z|wP=8Z|1^oXCkN4$f}+IoM@e;ZBCBVRRlb&z_s%~hyp!INW~I-)7^xh73#iD`(MRL;b~2ewIMw) z)ib94y*)JceZDxJ1Xs|ZM_8*V;`sK<2M+ad`n(uPh5QA&u3;zz<$|VI_YWVpZ=T<; z9~iz_^r(d;?|`#>cN=uVU7j@qf0F}3eEe5hMF`!m?))rdtJU}~#6s&N8C^7Ny2N!J zFe!6nR8FA7)THy?$IBkcug?lSca$&m(fq+~@u5m!T^-P601;Cq5zoQ)v0{PtaJGYX z#!w6+b^=V}Tx2B{t?Y7o%Tra1A+TU2aEbz4}$T#7&2f zQ23WK&$AUoWW{bKa;S;xI{-=zDSpm>*lUN6N~g!~ASZgx^BL=C=I@^jC722u_NnNu zRWsO4CnV*|*07HiwJ;c==c;K&*nXhg73aPOk5`}}{~q6pzn6LZSwld9EW*~;!m`J1 z5JXrrwBrdZ1IZ&@kckkAH7c?yD)POAWCLL9APVxIL80u^9re9??ZZ@9FMC}+I&sUF zCtEU~bir!qTPBNcnBA8Vb3v=$0}Ad}wu;23sfRw6_1@e|VIbN7GGACIVV<&0Ud@~1 z>jOgz5`WE=@_^1~$VEa+O3uICFpTF30B6g#t3@Rm-{}p_@lb>h9-^-5pyxTpQCdzk ztFWMc!x;eJequ6Zn z*ce&~q+v;P8Kq`a}ZUf9XKLe?fd1l zy==$!S$3n=o>1RLRKEp$o0Cr7R1DEM=-e;o1NXJkvo++Vf+aAJ>y^AtXukhl{oMcs zs6?t)^RiCe2-8iNv1thi;!~*C^WtqF48^8JfV|HzX?O81kj<4!^}BF2Ey#`l>x=u9+uC$(X&tl{d(vDWgN@D~zq@W(^jh5bIYvenpB{ z2sZ{&zG~!+nUjp}_F9!uFH_%W5Sg|=q}|jMlj*{OO(vGj^)9-8m0E?5sDphSR(ssC zU)(qeMimNn;2D0oC+$xt$%=AgmDA!}{ZM1*pyf~E)GerlXJ}dT?XzD<0v05}M>m%} zr0$96gLlg}$L}m7&M6=P3Pm&iN92Io>O<`uoq`@8nb+-56A;LO54? zjC`SIPm1|;dR?Og$`UWeYb9c$HA17{a!d{H$uf~7E5+-vUTBq=aYHY36K&9|!#3nv zR7!!~{^YzeI2DwASx4ejZOU|NtU*IF>w{CFI(w8j6=Z#8NAIC=^WQhR_p~ZxLMtLd zWtDN0mzF}7nJJTc9&pn1mBd-Wv@}>fG4%hOsnZ(FK3$h&J99pU= zWgPUTE694qHa9)S>5XJM_nqm#)Gt4Vqc+!GZ8<9agx{-q>rA#x!y64_|@*qOp zOSMx&D?yb)-VFgHe7oI**+#jaa#yFiJ{H-7mRXUB?;nUT<##3o+lpDvkzFaG0Ls?U zZWxT=b=+>D4KKUi>LryNT_yw1!mZ)f<2&1317+DxFr09?EH@t0{&*>J<6UP4GM52% zroCVFbd;e5O#wp9-f9yHTIZ4nr@GfD7V=I-!qDOIqB=<;@i0XT^K#8*{=T!~{qSXX z7k$_zEoPPxe@%C1MK})TC?`?VjIb9gkTUqH9$Fzi^O)@)i4R!iG)6n~nqI;C?kzKu zCLa7uVZXJ@$Z{5IY!X`|244q*T|DW-VBv$n zE%BbQ1;rS`M?)@D8Uexy3xarg+(=Jxi82W|q8=nL#bfCXldJ&9O(=DS_=D~8cTnCZ z(320r#Go%Xi&>#R1QlvV79tpN>}3{7c23kvSo)Pzcyptx12@Jm&P~{-!}lD9edTU^ z_9A?`buDlH#t30z9tM8XzGF3ARwXaakHr6Eq44|5;W1!jw+wZWw1iWK6*3pn^DV&t zXKte`$^Dr}7!)z=6W$bOg+3z~9|hSRdb*tKiIt2If)@}6>4Uta8l8z#9Yk^N_XIoQ zTPh^D@x;rCVobR3w@T(}e)@Q3z!Fyc_J}GdP`C^l)flhY?UFjnFpBIIxA~UY@laXk zT=RS0gV3AB+DccpXQ0eG@Cd?Si`%$?Ei#~BI70(! z$QS(JyddObT8x<#L#jWme(Krw6x>ey=}JLHs{(b`Wnay8Qd~TVdi0NQt6e)w_0Tq; z+Op!FvH9aJxkium66wE})hxeKZGB8e&2k(*wL*xm%B(X2jDo&6LhlkL-6E3%M5qkd zc2x5PGLyKbQIS-N>Clt;>bERrtP!cqFVDCOA214bL=Md$1Ov5OTAZ@xmMRsKywXwp zEe_sq`@-*_V@bXG7d8#RHj{zw>xKq1%tHlozjs?B%0Wl|Ggur7aLt($=j-+bVN(4% z*xm8lE)CSK#*2wpwiCt_#^^%v9Gvr-&TXQIC!5BziCOmg=JL9Kdx`ZN85Fi~#tOBh z4){cyU%!FPO#g9p^stg6Wk?@1X+W4-WLFA)3N&JpHG`dBH-xqCwL&pdQGx;j>B%}n zYA0Rr;Hgd15Xm}&YM)Q8k7<SkNpnis;BQxT0wjLHm!tHjaJ5)f z8U@()(51~;vS5yt(F?6=#ps6rTP%u@W&O~F3QWl71NQ&ufzQ5W$UQXvTrGS_Y z%8B|rg}!uQ)Mpel} z@`Lj}IHNRKWC2r-arER9+qZXh@eGlO`{if{j!97}TakzVov9$xH*f!MP=Wk;#Tf9n z56uf)?*!EK%3yr)xwS5{)v>-jMmVD~HX6-Y1M*xKi!vvgKQ^a2)hSeE&{J_a0wpe+ zXJw%0o8rNyc?%k(3O41jmk)OInfjNINH0G6nIv=TJR36Eg=DFWO0q8&J~%x4o|R!N z#>?4w#4Woq#3_by3g(>|kQDpZtoUX+H$1sL%2w=P|=8jwg}yWZfCTECa?)K@`B zU|q1_5F;MU@BJ`fn_AX>5n(w$-oM>YC~n4;Dn-69mA)_-hd{ny?DB`c34z*0Q^tBF zsyY!iL&Co2RuOE}gS{X3QV2PlP}rSN7@dW!3)>2^_JL6v_a=M=g}U^8%eWOgecU1g zDvBhrCI`3`dwoPA1C$w2K2sE$f*Egz&-of#!tay_#t4IDGDE1;kOYGMKLMMCQ5r8% zPfS6eyNZjxLE+YeAHb|PjVF_he16OcOn>SVAFxgD6SfDwuDiD5wMNN}c4x?@X=}1X z<9!Y|R;}abqIiqa$qqF>9H)xkrD?Z^>z%<4yG&-fLyf=!M2|S^nTP+xi@^t&6cslj zn$PNoIMWBP46D)!kTK9TD@&psaM`S~haa*3rw)ndIK2-t8Ge$$mIQuYU z`z(je8(0%&@`C#zij8LBbRy|?eOe>p2LwkzYwzFn9$}?;E@1(NFEVGx-3SK$9DKg; zXW9f*7Moms$G3-(h|A;vHstUWQ=Smjmr<$rhYo*X4a|GK*@o6nXMaU@II>Ecvhi{- zW=f_vo3PCMK65>SJZ7q(pXEEI2UTm!yCc`kpNgjZMIbS=^p_~dTRjc!TEQ{TNT?<=4(7dI$)GJyG(tRWm$9(6v;|73)!G}r5LTZ=AVDxf>8o7QvhY=<;2V@0~*DHt^|- zdtdN#5X|5@S;!i-WSd>m2|%9-(qk0T=mQ3l00>jBID^?X^4qx@giYPH3vl;W4MNY2 z_=P|rlTxQU%P2APT|J0e0ne? z6nPTZiHWF{WIba)XIiglvcWYCgsGwInz5d1x5_REZq@pOE2Gvb5nhPoEWKL8%LOHP zw1G_5`{fR^3(m6qZWZ%RtVhZlybkFo&XM#F+2CbBP+kV}u!~@L8SlZcb13MCNv=fr z-FkWHKCVCj9~$Ic1?4g|o(KnOxcG0pu&X4ejF-qF-7DE#3?H>&z@l&h*}u3Wj<(S7 zr8UZ{``P)exUh@O#F=*%Mb6mCP4fRkwqVBzi^Juuy}_=6g`#r;yVf^WXJzWj@{ub~ z7Ne$3s>}z0nVErE%p#vwO6;p*VE;&xp843c6IHm?WACcSklh@o9MVBn#Dmc;z+O;L zFKUJtFgrGVLEzP=U9C^>oh1lQhv$7!YG_BiLq`AJXW(l20+qJf>^`re5#v)a*_r08Q< zx_dB%M=T&y#qriQJyrUO2L-^*rA_~WP@RN(Mr<*>MPZpV)S%70QbhRR7r0UC#4`z9 zdS8~jO~WITDv4Uvv@v5Gnik{(FX{&HU0CW4_3xleno*h@SIQIP1- z*w)J}(T=#ScYst~q+WDGdm{5GhH`ept~9AoX0wZI10#}Z?5en-c3lt#7HBK3 zlkXr?Ju=8jN^3$gB+D(z0!HT{1KTMK9Ly)PTdrZ?T^f47Ba|wBhiXDhVZSPJWxH-U zPt$5MM2I-5419)O&7N0C5%{Y`)Z1f7V4MqED$;^2)%0kwAxnFt5cp%?cUOPo-NJCm!;e zo`)$&iA^?2L)rvnW!rG`hM7x0b*vC5nuNVZC+~`Zb|O3=(}zVl6FonX;_S5$r0Ptn z!&P>5cxs7%qxmF`>G8T%<}OU>9P{2&Ari${)~ji!Wy>la$1cJ&i^-1rAESnmIZeQ0 z?<1cAiwA0IA8REz6&b9-yyd#3H{BAmm4n%fN%PCfG3NYsdsxJ{4HgVu5Ucr3d!{!=2X3ItoV$Au7rk@{Z;=4N?m z9B7_xKobMk^xw~T{?b$Sflv&8&c*9AZ1F;51G;V8-FK+_wEnm)GlU+3bj&UvGz!O> zkfo$=RjCZv5@)A^`&&T>!sO}6I=Y2uW8c!osoqiy4oIEppqAu4c=fr@1W~EB^fHI^ zGGV_`vccA-UKzE0gE7k#vPqa6V_cGdvTJ|ef^p6SS+?u+Uyj$*M6S$>kM$0*$>)() znoygK+{|m?R(Id{rVVDYIl2y(F=A%vaG||Lq)u+#)D~`Yv)~1ee-$uoSs0XeUOUcA2)3p4 zOR-hXcHS=8qd=I}sYOw&#{z?5WIg`S*CeU5nciS(rRCIH!2KIF1=J?&g>cGShyiL7 z_Z(a9Eb?LkNoyMStXg~$6s7$d$1K?QB96Vq4ns+68_yg|L+MASwN@D8wBZ*R3Qzk{ zwRAn|PxMHEN+>+XW{AKfU4x_O4j|45v^#o@Op^}Ba63CgBLA_tyQ z`c1rUB_8?9^?qZEHddkT@1hu%#OQu$92c{@htx*N2tuOrSy{`?R#`>`>oh}iEFqou zfZz?NOg(`e`W~3uJdTMI=!px7C>CNhQ$JECOjC1&J^HKCiI!y4s%_7oH$b*BM11>O ze6lBQ&o*=?QA9oPpCqcpYWGnxAX|L~tY989r3dBhC=bJg1TQ zBM^v05Iu3Rhb3{elC(~MHhM*$URA=cInE)@crS#AhkY<`ZDfUI4fUx1MKu(=K0ZVb zt1@xN;M+L*_3PI{BuiDHYGbcH zsyJrZOm|w1c|Be=_3({{!2nzejKV&rVMHU8K^$adX&ktIAPGB|-}@(5tRT z&%!LN(f)r9Nj-?j0D}0jdp^hdUh6irMzyGVUdQ@6>gEh4LkD_G(T#jl&+sYIl%)M0 zQ-j@&wQw!ue#u@(*Ae+?nE0%q^j&s3T}@ThXrcC3^tPaHc)0$}TG%WE7!YiSYCraU z4~^;#MK|)e-vQBO!@c!0z%h4TOxRF>+l}CMhgTJJx1JdkFq-#_6-aspCK_+`!#i~7 zS;OM4fc9Dqd7aicXBVyILD5OZZu^cC);3naGX!89Z_a}G&uOnkSIigt|DCDgkeY94 z|M{DCUgryY}lC%Yu?l8JDfLLY(Sry_|)nhHo}k%BwR#oJE$YOs^Rko6kRdbL(3(D{C{ zj9($PGPd_HcECXVeP?q*sIm6VGy%IFQ!B zgQ8b5XrLC8#Ax^%cu&w2XVbr+f#q%L*m)0ZLK$bzWHE}E*krlori~1wdAZJQqFBkB z!=ZhfVGIJNmeiwSl`TZzOzQ6xh;#BPiY-xXPC34AUZ5vJ6w9zW;snP=QQUKeL+mD* z8SG5pAw6BKAQR80dR)o^c;ashYW{rKxc29if;@tdmKx0GIAYAYOchzvA z29p>CfB?ccZYLu#FR1h58n&<{`Lz|}x!0546(ay17^p0IpyWU3jeFS0-{+9fy|5tv zVpZd9hC~Sxgh7ZSvV=g0ZYi1zD(>v!S7QHBk^HdLly1n4Z!kCF{Ux6CSAvb=FF~m2 z#rMCre}m0tx!|{=D(A7)${6^+>Oi7f^(bjeb^&a4G8}>U0WT4&q|5GKU z?^~}oue-lK$ZozgZCU&2iRR+dBp0ilc%7*SA5tz`=8LKL`t$O%*Q)QQ3?dM$AsArt zGZn;bGppFXzvBR+$S-h}pWN&(S`*DtNMGB9Ty?n^sNA(nK9b>r0@uUnw(AZPvw%q> z(dK<3E=O!<8rN$_Ov2?N5Wk__Y|upDg3jy2vD@ZDv=Qb*^OEU@@d8dn%8($drLi(~ z*ib9fmybn*XxU_Gm&5pc5od(A)FvC8Sq&4z4bMc#Dr1S$?_^$vy_k-%PUzgjSgIK# zBu^GfjhtHhVTUS8rn{QqgU8fc!dQs}z?Bthomv%A zNrzr=mbOu2Eg9i?1{rp&6~Yq-&0GupRS!H|CI$=LN-WckdRE92mt8O&2kK)2izI!S(yoih1)+awtLyg&M;hidrqUd+KBupyTJ663qa{?J7h@#{k> z(mwH}8TF+c^(7Mb&ma54y?7 z62(u3Q9A0|lum}F@=Ux5{KOE10s;AQD9vAgv&YU%aUi;!O%icyHQ@3tR&f{3%bN)+ z(o(uxNiSp)ky|=pRSjML8P;g}L#4yOkz<=^6kDMclc*K*G4{@I2p#G}`NxIwj}_$~ zqqfF8aJ9_g$mCzj*09;BIP9%XWBB^acAMzHkK*DA1PUhd0^q;N41r?Dz4@CkFDLGi z4t4=M5eBx0qm=#um=rAB<<)}ygz=6hA;-79AErP?d7fjKkC?l=4uXY(p; z{-LBKxU?!#Tj=|`bXUwmSs)K#EVBA#cI_yWM&xK-srQh+MKPN3%+F7tuV5S=cEq3L zteZt?DLXt!32){HUcnDMyevu>JyP{kbA##>tyPf&Lq2E&4rqh@;h)9j#bMxm|oWBEV6?_C9sSerdt$SE~r z)6G*kIJfE3IyK>bN{RjhoXHUj_zyB*%thEGxbY^>2W?b@e>F#s8Cv!h;F(3 z2OtcVq+Fnx@X2CWmbF_244RFlhrbNr%|Z;3v5=o?$yDy1*cUg3Ig)FHDl0x8sYlzU#%h%zN9X1iwcvkTO)k zGYsja83+~=2xt$YEIBv6^c5*%<*<&{`OviLPIC5m=CdU4 zNeox9+piWR#1ankojaos9;fCO#D*E0RNXi(Z(o{xp$8Tht^YQgRBG-StOzn|bb~fu zL;4EBh0U>}D38+cdxju=8l2=9_|e7s1Uk);SsGnS=SCKbI|}4ccMW~;92m|ua5_l0 zC3ts%6eDU-E_e=?ei?b5%@AbLkS2c1>!`WEwe|$zsQ=6M-}%O>f1Df&qmyINLJ5R? zzSwOwr}p|Ku1XJIuC0G!^X#wdk8<#o0cgd?SXO=_Gh?nOw3VRMr|f78`x8#&x^To< zj?COcUaW&9Saj!_-SXo#u%OBy+UBQh)y2%m9s zovlfii|mpeFiO%>h!8O z3S}7j&mhDQ55^mfpEp;74&S+D3@QXt!vz@8>@?wWuE_2mE$l zMQK$EX+;W`-3JnU3BgVpl!#X|xT~6cI0$ZmFRU<6QkIYN73V=^myv%0phX%fD1r*-rS1^7)8m3~+Gi+|H! zk|%E`#|!0Ub=KcO(B}@g>P0ljj@LDZ+_xj^#6xg!|*C%-(j+*nlTW z^a5_A%Pn=J_ajq+ivRsBslKJt^=a$ySB(zj!qrgo#IZeX;ZJ|6^_c?XSfKhzo!>Qd z-*wMy8Nm0q`AW;PLz2BC*oC7;Ji_?R_oeSMnqzi!hrc_Meskrl>2WA7j!4_O%swbS z9)>8_RjwraLD5miS&s$=GX_GmXTcOKBe$yLb_tdCNQ}{z(;Et?MU;53<+SP13cd$d zr;HFF!l)3z@_hw!+8yo~0Y;fncVjGaG&w}Jp9fKzBw&f2RJg0?l57T(Y?;Asm>7)g zs#y2V=h=)*(!${%#j#~^oELeij>vzyVNM?7bq=NoadjQkPaPNRrt^>4H~9&ZY;6=+ zORz>vX@?>UkZO-063}+yHA;M@9~TTnRO?uy_|)=$bABYYszpW=%Hn5*i1x6FMF

1zb16!2MEOtRPLURo?N5R_>tjN+=rWyAz6tgZw#;ift*gn*+iG1vq+SDu! zCX9c8rHBUWv{8 zv4y(O(Ng4N3Xt8UH|eU*!F1B~`Rq-0tjdU z2;vras`)x+aRht3K;qEwvPUSo|9xdBKbM<>19GC~4kc;siVVfw5O**M_&z?fGYu5p zASd$+f6mWmf4(6lQn$MVI}u*tNGp;^JajDh;XVW)e%vyLOa;li!(}-A!~)te3v@ja zb9?3r_>)|0kW90cU}9Oy$bJYPy2KT~>ID8D5BSPZvM(m4QmUxo< z7R(kUAF6sVwiO9(0T&Yji%ujFGH0}Go41t5@|ze40vBrw%3@fzuFG-fxdCE3iK2I8 znNm%@Phq0dpr5Ks{C5F)jU@b~0w z;SCTF5ZfRK2qo~hy#wf*v7H0R_4_wxW)T-BQ+sFgZ}uQ2YX@dWCu=uTb5AB0Pe*W{ znXC185VN(jgSdmevx}*{i}QaQ?5tc|Ol&|9(8U>OXX2*7^OhyJ}g-ZaD1PiWw^Ero8naBc8PpewErNqsbX{QP03 z($i)U!;JRXjyJ}gx6butEqz8Xg{AG9?4GoJ(x;~i|@H5rf3I|?b^ z%AbqLwbI3L4jamKv?*Kre*s_H|Ml}$EJPen%Kg}&T!Cu5`Tt)WKx*>7ItC;JOEMgU z40!bao5itlu71~dHXlu;M(lp9TTVtTt~3{;izFPiCu|BPghX5Fg&id`UL^{E+Zz0{ z5#tMqDd0ANrR8kTRfB&ztKfc^lB@MCwOXE)bwx9)dU2sb>lmgam%ix(v(lLDLWGuG z<4I^N*G89~J&5Gmw>!PA#pl_sX1YTBYPm@BH_1f4XENj&Z2xmB+5nDg9`&P@1!5m{twrvAF6AlZ{z){ z?KzJh${T!Y?3cIx*VBGin;vWk8W{%`^ytYVUJTcgN}nRs8~(S!{tv;V9_v>0K_<7p zmquPqBS^&y*}y%Q<5Hwl4~LWw28!99L?e@r67G$EqacE+jsT4`7$G|IDT-l|P6Fu29?d67NzI&7;9U()Kx#WnMT#swx#(-Jyss zJr8#6vz?OjQ^^(XLl*VRfrGyVFvYl}Na$L*h^OQQ^-%)B}1$P_pox{~_F?D%MaHX*uN>zUQZiyYo$Os>(L% zjlWBFMh|nR2vr4c*LCxEK|z)d;f(Q9=J9@x4G&qa%!(E{4J>L$h|doHgX2%%I<;xv zyw45c&nNxc9&3*I;LnEb*Ub+eEQZ_NEclj~B4vULiRzvokK95ZcRimU?#nP?hCGAz zA?gjko5Fx>M?2&$+8%<5loVKtS5&aZd?BQR?AWVFr4t3?;WOPcUS4@HHm_wH3SR5y zpWIpxq(3Gy;!rc`w;7|%YjpcNQbbLZo%&^zdhuvh|5&n(a`t;-O-r;j6kG4nA0}8V zIf~rI3%|LiuKnHZKr7;gc=;mU1DTX1;Cp5NF>V|&`Uj`SPxQ)l0Mbt|J0h0z4nJ53 zdM34(f82hE8V5=LO*MS$Gz|JJ@oAXUFVm`S&lSh4snPOUKSYw7$WOBI>TPJZL#M07 zvdu#h&t(8|V94g1&`#>|zv$RfoZS7&RN&rnD^Y@mWc}c}u=uiI)H?KX{G}G51q6Ke z2I%THOyBfD^e}3qV7KzMFJ)Gy4lpTn9G9cwDqH30R&oo%xDIT4GVf$H5P!)v&X^Z#zAR)4gKm%gqFLH5uvwe@-?3{R{02 zM4|9I?XMww&Wy#3H0ZN5WGz0UW@%E@J9M!n^Q_gNeAHU_Hfvr8;rI(udt$6^byIqC zJ(MkeJ;TOcl1}BmR|tb~1H5pDc;S*K@#Sa!^9hno-Q6VzJU?OyWa4v#MmSr?%1h#s zOX8Z#$d*Ny7X>Osq?7ii`_BhV{X6?m#jihM2KR4=#zbj4pb>>du9StdkZg00)pO6j z=AK2X1b5JW>zVxNIPWBL^<;`I{63;*_k#N!q&QCgHXp9TOhH^CTmnTv0r^GJBm0P! z<<~H&>Kl50{H3ASriF8gHRRE_hTWN9f4%rNKj7nfV2)(7>^eVwN7(qt)8>MVBp-28 zy8lr!CwXILV>h0GILJW{lhw~Pu z)XZ&F)b%67D*HR!PXu=ppThnH+cDZad6^jiKvGHt1UQ_fXbxj$eZmO7jPx-7Vr+5s z4OSRS;syRAqmNYGBXew-;4@@9U*#XwN zFd#6L%R!TfNixY^co{PVAF_T{3CrQr?6r^JHN%v&SjXd-ErSM`VA(=jh>^(4cdS4; z5T!CQxIVJay|C{!`WuHcq@`9QqhGZd`s(;z#L7N;{rI>@RV0-Qv-BcIN7w+^%5h@o zuIJ4&#cciz@(q1}RMh972JMb2xLaa|p+T#G$t~EG(9Vv-tQ}_&!~MSzKW-$@tY`-}gp+6rRW_vFVduIKrD8%#Yt zabuRF0f-lN|6R&+R`r2vD#Z7tm3i%7>)q9uN8^0;-VKo?pESe)di2j^+IBmA|5^i7 zC~|g#dm;O~3zl*5_>=ccQ2JU4k-OI+u54a6aDGdrF}~tB+6bsJdWau!Qsd4$MdU>~ z;&`@vryvS3K7GWO?TMEW!oIxVDXRE~i-S?Kf+W0gn5&@MiGD z^xW7#FTp<;FI~Qj;hi#2Y37IW3nB>Xbo23tp1h+n8!m2$ygXcNL%HsMLtE zMX^haGa%7)q0YLl_#-@rW4vI+?oT+|Jt0xpSv3du+nEiPZ~y_(-#p1{dbw);_b9Sp zeqrCtuE}4QHP|4Dg1(a_ypE-GXYb%^gtqueH4n=<9501}VH3JE#iqXx47P~|XjWJ) z%#E^Xt5C)mCe1z?a|Y%dLMM#L&bE=p=!lM~ML0tZb21NzA6Wsx5#_($sq-W;ZNKzT z&upp9dAeoWegEMAfAM9$SMUD=w?Ih0Os+T_fRA%{BhdUD#1`>fh5@^EGd={*6L7E* z*xCLx*5er3LKg@$*q`q!Ig|1B7iOM}rJ62mIf{e>?9&e>FJdSW26&_vJWTWm*g}^H zG&)X8r8d=-*}Ag?KPKSEaQ8S0Y;jKrF4(BQjA9W7TI|*mIVIp=tNu#aYT`w%Qp0|H z-*ULOnv2uF-kRCisUh-wONQx2bU2NH|KvC)t|ZPz3}vI=cAS zzv8?B}iybv_;Bd^+0sX!X<<5Zg2KDFb?S^1uJD z;rxI9Ut=aE=z=;I%nCZ_7Jg^@OEZP7l=Vk+nUCnAlR@nxw1KU{%gkY+MVGL zen3|%1GHKhJpP1iry}eTo7+tb%2$xlpbg6A6^%S!p~&;qE^-L$gOgX4*AyQ}|CRW{ z9{GkM-yq}%2W+~KdxchVud<1-hi+5oHin)#_?lM(I-UbGp3x-JD_m<`e|5n;6WeuoObLxjIt+Q&9@d4g6aeztmRRwt=FdYi(xq3BoQ z3x|>Iy{7Q5;oHv4dXDm`*rC)_>=4rU)Xn?I%|HG7?CB@gGvJt7>lj(YeV{?>gIlM_YwV(Z7GvwC*__Z*rr_lZ|;5A+Ry+(__*Lv}{64qjQ zU1L^cS`N`oJ~(?LdfjG$o@qHmfBgA_1NbGIxnHAq2Bzz4BY9i=YpU$mNZGGNJbV24 z%0W8U4TcYHC|=8TU>^lvH!dmmNF7$K zH*{QY(74{10}U~;La$bG)i#XvKDHcgMw<&MVybS1SB;WTddh3bcq-@7TH&`1->zal zB^}im=?M6UYW(=4W#rA2Nk_R%ZCU)$kg%8*@I3e~n*?epJU_lmmcovvO5O?Y8^Dz{ zlQ6@pEwUKjwW(GCL!L3@*%R$bIE&@IH9>(9yx@gDBq%Zh{=#4XCb7mZG5nHi1_20P zX7FVWpU6A=5k6)F^pSrzn{KS(PZ&IXqB0!YBD z7`-dr6O0$pN?59Bow!B^}UlgP575VfLzd#4Ps+SZ;kGmJGWFu$kc=6;Tq2P~7s- zJp=H}Xpc%TnWcJ0c~I|}(H@mi;2SCjwP6N(bc}8>+=*Z=!dr)~Gow8MM%xq*%1toL zXpc@2y3+I1Y$5E&HP5Uz|2ewGFj3D4rwQPh&E~;OtHVqYaOt!FJTusQ9>FJz2*1O` z=9*{bng`~3zL23%Gn~Mf;4_oW7g4^?jTQ}`3Bg>RXEvKJq5hl8eIi{b`}r!*tTvBQ z`KX=v-Ha>Gj5m)G`Yf^_ba2OcX1Vzjlr1P$&&*fIw3zAUPf_26MxQPg+I8CdwgR)> z!sqAB0yE!29uWkqC{)jw02qS;GvPu3-S7z2(oH`L z%!CWTgnOHn3=Q zUSHgc$uk3tXqW*P%jgwjaJMjz+m*c!Eiwx(9-|jrVyPnuhdHdsY`A!W#vq!DD0Gkx zv)|$=%Hlf-9rj8}%y>({c!w&Eo>_8%=&Hmlw{(QMo@UQV9uY9pE#+bQ=c$fSM7*%q zoD#F$QUTQ(xt(ym1fHS9jJH&T=*dFR;1-Q8Gv87P!H09F^>m3@ZK;gWwb7;yR#jp~ zTSB|o&rVyw>6e(zmf$9Kx?CuiX}!dZwuE-DHT12U%Pcd)Eu$UmO|X!ueZ){^W?M$P z*O~r>+mgD>thS6!pv-zgq)riAnOSYQfZm{L)udA{LT6T6239*>h-DT(5c-TjpE-KoL*ynwZn_!% zA#jTTx7^o({}jBDtKxk4A6vk=OqL`vNW4}Qy_qGcoXJ4lF1@C_m$p*ch=0Zo_|NbGf0RuF>CG~)7iEhm!y|qMSn`YomRe1wVn(jf zAn*bM2T@o}nk0nQ&_xDaWYD<=u{H!>V(=vj-;d&%HGY}lmnr@rlMWwn%m|JtfuGs; z|AfKA3aj?rK4rM4$n|8Yo=8Qa;}C0|BuD&b@QB}KNRODkq8mTMK%`P+xt-)m@Qyf` zwI0Endkblg%uE#>>uW`E(?*h`qspt0;>6#ddcWU-?@@SEh?Fd7KC$lK0`F7sKD=sm zFP!>*L*Z`_{DYniDZw!yz?BO=wxF40uBSpZVI1Eo#HSo&M&Gv}jzE*Jk0>I74khSd zf~AtH8CRq7AxUk()X21el#8o~rzVL?_AQ4Tp_qqM zn@#}!;DX@~F3??+EMB?bwI^8w08(VZ-S&O(%!m{biBCy~6#8fs`g)8fiUq;Yg%2*+ z1YueZcc-$+^9F~EVQ^8W94IwLHe*>&K8^f8AsAMeQyQYIFhjk9ieehdAafWF*hFMn z4jH0QIShhNr#T-@bH1MDA_wvKZ6+Sm0uJZ2&kmGBAnEs**ETIESM^G`+OpqgA-8Ek z9P>;$g~L7#hkXF|HQ$u?1wU z-#7$d*A+Il*QmbVwU{r2gXpg-V+ub;@XtCp;2mz-Y-~--;S*C8M-F;$3a)*~J7ke9{?`;=j$>*FGxJL83!ReGg0(LYbfK;0h*a6dY?56 zA2H)Z53@#DBCUa$WgfxnX@SX=V6B3A zW|-(fmLDjGfy^_bM31rjRm75UL2PD~=p~jPy-~vZgCvatr9Jr{T9YvIL=UodB-Y5x zGSO2k-z10kZgg1TFs#fz(OWD%Et@1B$s)DZ$viVs^c1TllBL5emuE(rhj&S{m4iw$ z8%0mBu1!f&%PFYEe0dA{@=D5 zC`yo`m;L&0-itUEZ><@Q8N)FujIi+hg;+hl3miV9ovY%Vp@LGb z$D`S8X0BZcZ;QKEN9rI7yoRrZ5DMS2=>zB< zgYLms9-&mZUPellA%ZdM`xx|nEKL-vZO0n@f}vlaW1Z9*dO)6D4bao8M&MOhhM>VA za*ZIP!u8Q~Atp5F0pKkH4hyAcZ!?vMsWo_)fU~mW54OO40^Ensf=N^ia0di;fNEM~ zu{G|H;11DgdKx9$uWgZig6z9iO-A5j0zQU2Lh9C1Fd4zz}4&p*{lV~wFW`1HPHJE5!-jVenz0{XDExF zNrIe4-aZp^U`x{Q2}#384?ym=U+-9qgNbZ8nghbo9H5)VYWtP7&h*&8;!T2Bgp2K; zTX($IWav#;HcObtfU!{a-DlG8!J=PU@Xa_%lE|CsA;_!;nOtMqH9SXSi$7$eFsz_i z+Q9~fRfb%J$YT5T#M*X;%yx$jd<3eat!!#9l{SNKqm(QnBcT9-9x><<7E5R^^3gfL z9-V`I52bZcn!^OiF8?5kKBMSkie_vc^BS!;RoShl!Vz;kBj9*OQ8?TFb!hMUM{E#A zBZRK~!pa_-Ifjw{yCme`7kn0j_%#|+id2Vxq4kIO%*J3OCF8levmRcb1q}WWT`v9e zNUuAxz*E5e)dO;vo)+66|%~(Lng2oq_J@Of|y|W=Y+)OuK^u{}jrM%OVrY%Xc zMo5}qPT0%GL62I@ELv@JH6%9K92`x9q`lZRfOm&?(i=zjp)Gp5Pte;SbZLiTo>>oX zm&pe1$QuK86HJedY_A6M&(7m$EnNn;$LtF(m&C>hC$tt%kBJAA$@{Dqne>>+M+tpq zNzrFhL%#uY3Zx77iJ{LtX@9^tocw5?If?!d0A8EoYmB4YU&T@N4Th| z&veMg+u5Ekg0+}Mu{FUJBLKHB{5uh(~SDqXmAoJk&)3AkBElM>b%yvQVuGQ5vtu`dx| z56^^N#P4n9bCO>lVKbqljE24?C zMLru5pF80=t*^E-SbYDp?4vA8n|?wiKT-Qz}!n0v2+InyE{PJ6BEIV z6JYn5aXz!~79?#Qf@fmsUorSAX%FwS-aoS*bLhR#1W%V@GPZUAyH@&N}&ZPA_$!IPC&813{jhwuQ>LypkIVE=u=z6buz-D=K2wl4-(6wuH zK(&difp5AD`KF8RvBuE@3AF$^$#J%802eoPj3~V;5iD)-$&vUCJ`ztw7<#o>N^AV_ zfJw)+fMexA&g?{AtK=&1w#>c(T>YEKIE5j4t#XK6tr6^M4P6IUso77fMS;oOWlhp! zAJD7f5_u^}?1;eo47^{(JLBe3dvdR)OKXNMW9UMLHzwoEcW1I<60;pn$mTOPxDCG+ zofjvgxsFI#5phV+WdvP9(2Z7NrL6Ip^jjAM-%b4OF~M&G z&1|RSi915>FwN{B%@m2){-vvGb{dSLK`92>DQgdB&(UNYXeUJb1e`eyLNncg+aUKU z$gGk33>jrs)~kR8*{FozJ_Gle)NP^h{>r`q?60^OQkNN0_X1T>5dnEBgRt=eZ{Gkz zHo3CLW<%NQBW#uH&!QFHT!rvV;^dX;oFutL*nQ@F`yE`KBG*xCYaQ>i0fhmRLBE)2 z>$FxFpA9O0SQRd_*t+jNlMD|0ejaphO&6p`9ym6c? zqC0DUJ7$7`&ta2AChjb|eMX+L?Hj;*2{Q9CbKOhhcYM2${%SCra_-Dumx+_$`(DSbv+a8_(*nv4)2dDtU2#>VWX7D*5D-uj_T!QYni6c6tsg3WyXLq zxSFR!rXn9n3V;aMkmg}|l4X=J4+gNAbV{(iiVEzJj|uWIO2(r*&6Xkn`h-BA&=Ivo zJ|)N?^=w~}x6lNI}BwUFZ7=dt(iuQY4jXZ#S~1m0w#ZvxRbWw`y>T3TZ>={JG&n}LXNeiI1I zWP*wnHe-=Zw!aJ=H1hm{;Gcs9?Rkb@J|N&O1IC+W4-U%SXG00EImv9g*#23HFPK93 zD+Z6QIjPyb2pj}HVCeAVec}f0Gk@&k8aT_;YYOe}?Fm|p0Nh3Njdyvo5Lx0tFkl4m zmR1m@i_Bhg{2PXT1G|5}d9Z~ZGtrNm=;1@=G8c~7TsTIxb<|YhOn?abg87#V;9o9C z>;Z5u>jd|*j#e_b)G`ESlhCD)fCG|720`DjA-n-Yc#~OAJvYo~Zh+C;Y=6m0iX39M z*oAiH<7BBmo#|J;G3PDlm(n>wN-U2ZtVY8O}3) z=n3GPbpqc6`rX|A^&(flH35sYXu35*(5<0dfYZbpxXFN<2zZ{66s`!n$FSkkWmCvy z?ttzyZSi~Xj>TRVb0%NaBFgN?*1u#tmxyOA9KPX_+3RH$x-YGg(wk*oReuY4?uclv zMug@Hxb>?jX7|4U^feoW>mID{5b1SNlr8a#NjH-;O{ej;FKz18od#7k&xSk2!F`% zQSLuI&j@eh4R@J{rjY~9h|!E-p3;h5mrO;t{o0;k%oxTvwKua3fL&!ufYbU!lkL~m z1B?^A>>I#@_BK*G8{}MVzX`pbOylUz)OQl0FzM<{Dl;YK*7`eUBY}_oV{s>@IN=^b zCkulvTNvE!(i|q1z3=64G5dYPp-VE8HH-Fohv4{G<-P$wtCY6TxOnis;c!o{v-#sJEFl*$pGRL1Y>{a{QcDg)p?hRNIoF>yWpq3?IZM zt!yG4t*+Hz_zl#5clYRPXIrxlo6)0DH<~YuMgsN?Ju>o|W+7`|}S)-D@^ZXd-$A_|r3 zFq2E%eSC>queT!?T_qRF`uIYbHfD28JPKU>YM+fn|1~TGZi^9uxZ<`qZ+WLDvYaE`AkgkxNsqUBPZ+F#%Gq{_aT7@ zU2V^2I_1B{pWsR`X5rcR93Ae7@OI|{twl7xV)$2R)vfMb_2@PGfY;yyUaN3F30-XF zS?p?duUX*q`UX)w7$QfCYo<}xee}*Ube`~Tn7D4vVd607IeU<+kUg(`i&KS}+5o_l zC(Zj7w2!UKPN%TOAJOiQQ1^Gc%hurIE(0HTVfn@FSNa2RN=`&j9Wa6_N=;CWg6-F9 zoIDJ}UsHIL+dbOQO*DXBtFX@3P~D@N2xj(rQ^SQXKehnyx0y^``tDPBl>A&IA{1mF z4Z>eg_zMK@pp>-+?Ro>|ZBOhKriN4Ed~88m4Abp5*5HE?10R6R``Y%Kc*f2e5&T7; z!C&;zz1wi6muf2!+l#(lBMkN0Gv%Rb;!dR^FuUN@XdXFeY4d_PH`9W)8WcS^gYcIW z{u08km4gwqF4_d%w1D-X3aq={CAW$9E$CcP_x@JJ)&xCD(5t}I>hqZhWN44yVDqAB zImDN#HFUqi?5vN@T>Bzc+n-xcS$&`BWFNg$-4ze|%>j_9mt*aVh8IjirUfL!O|uor znS|h9QSE$%wDVP_!Wlc<1@NOb^U|gT)DzNP+DwqP_zMLg1Gb*6~a9HrHI+ zksmLII0ONgG{Cq-0mh}s?)1=Dqqpk}y^XG9s_8=5s_rO<{fGHEs+D=0Nir>r4xwuV z8Wr;uamvo40dkWdH}Q_Yn2Llv>H**_0^UNKv$32hYk_nLGP;VPR@-kvTi_u99>O%` zsevNFKLGd73A%rdUC()xkSlu!=oh)q@$1H211J)xtiekRyo4-22()hxNC|R?UuO7aR8dKWZ!*d52O;=l27e4yXPPR{ z-`6`-$qD?1`L1H)_T8FAxQOGFahxJ&wi5I0FXhG-{pfRot{Z@YCf+y;?RDsg zSr~dh+=_yEw*5M^2G29_Jg%0tvX=P~Gh6b6cz2`83~eo&*po2pMURQiT%s3@`ghj8 z=ZG0FdQV)@(fLXx_MK0+tb@8Nmm&Ydi{RCj>H%!Jxqc);sarR(O&|@OQcSn^&-^qdNCCQql zBTq$YTVebm>pCtbjjP>aMH==QbRXp&h*MKWkMhq5$C(O9L1n_wWei>9m>g`6yn(vJZ=g)ydupD^z@hKVF~JwOhM^l}8gcs< zI&6v(Z~NnTu7Bz>9#pe@!uXpic4o@xW#P<4i`gs-A9oqDOEuMi4wgA)u9w_%kHs6| zFhFA_uCdSIcgASGrmv<^K%O3hueoMR>ub2Q?#egIC?Wp%8i3~+ILb34S27`rg|G_@ z8!p)6kR@tD*hPk2MCa?PXu8l-xI^Dd48MeC?>7#}Wrhr*{f>+`p|l;&8@2?!flE+5 zaWBO8y_OEa!|m7J>hVt!GEdwbGUXXl{&Q4d;OR>_iynU@ljZjByk#tI?RkzU5lWmT zL;3Vow8rq?b?&zq@{}PDv%*AK=!9RO_`nc4ftZo>0Sv!L@rx*-Q@}O`7=DT3ql8ge z8g9SxOz9XWk}@ShtKi2!3AD~GofwWO1K7su>0!{HQH=tr*Tio6bV7;1R#tzrnq;xt zmz+|9Q)1@1_4eA;w*sX@n0E=_g*!dJ^smUv{tw>_NSfvEwVf%%QLdS^Ez?1 z3aI|>N+xI z%27shZ>$wMbmcgv9KczQw_i<2!54y0l?x@T3?Agu|Lkrqd1@}GUa?-ril<)%&&Vl{ zIQWO;;oP1^>x~luwGLRlcqgjdbD~lkQkq+}aN?l0kuOn>M6PA*#6qnj53FMp zEjLpUx(x}njXbgqZQ|S^0{kLVTU2|vV-jMlMeJ-H#UoC0gl;9u3HgTN5kk*X^gR2D zjdv;HjT1|OvY^AIXc_Uj2@=q+03WgaeiXGVuB2G9FUXhsx;Cb%FN=@iKR$cP~~a8O4~TWmneJ*p9naimnk}MOJjK#xJu`k z5*%Y?)ocDx?CQ12l><|WDQpStN0}@r?p9Gc;v_WG{mScZzrJIu&e9R0$x|9&8s}mX zM4_DS#MV~{NsG+53Y4or1?NOiqy$A&1b3x^jSD`N74qou_ABjcx9=Cb5Z!tFsaGr^ zuee_ac;Aw;uR_|K=bwdCKt1PvF##QSqZ#(u|1XQo!8t3D8 zNGA#|DHQ&!RVwJ>0~1iqDioicQwFh~zP4XGaFyA&l+hlk9%=7#lSGd{VwWRs5XBMY zKu$%hgYDOkKU$idhH6Z0vJAf_PpP0+Sp@8Wn-Oq%Av2M|LdRPxYKWMlNIAgpkDtDj z6C*GuiV~#&&gALu9{+FKui~AG`P;7^#G1M;$VR`Xh887{esK@vVBU`(o7#io2W9y3r4E0)m3 z$ypTg2_7?)DFX=YFE>wLxeDo+5`fVDCX8-ZE+c)+)b$u>>mV?tlAPz;<0DRjw#a27 zpV@*xK0*w6%8;ku;Y3iN1jyyadWE>#cz5C`QjQ`#4Uw@UL5UI|hx@15>Q2T^<3NW2 zjKYmMi*$Cd$8>dYj8+Goc)#7S{rc%w>_hU1g?|b7l#7Locjql;F`}Nn(qr#LMwJV# z5-t;&axaAmsAi#6!cE`?f5NTutYIQhHj1i3*gc0C!2(DkLwR8AFjW6w$-dwH}S<*SMIbL}`Fm z8Qk$;7sHn+JeXkzNouZ(J|FTN@YEZmOy|AFNgSYC3>v*N z6e|~X>0J`+-X(g>K3xWTb*ue zD%@oveIo*Up3#u-jKC|(k>SwGuUT%WNgixDd^YH$2u~oBdo|CsqQJZ&u>KT$iFw}u zE-{<8G~vt|ywAYlOLwQS_Qwo+OtEv7a3O-!mSW6?VBA90eoRf5aBlkr=On@Yz5%`W zg0oru902jSNsCQM;pkS z+04BB#LGblUu5t_wDq!1@@`>GaLfpf89_H& z+KcCe!Jjbr9?soDO3HJmQwgM)b-G2=ce;qrshtHs*yDhC5UVg`;bzYon$h4$sR zoD@?nX5{GV`%uOvgQJ53KVtTMbb|6cN%MSvs4Y;qm>{$9qf?mv%BXGP^;9e-_$4eE z@H{hh_zF;H8oYhY;F*!a{Keh)Bz)T1K5Z#qtBl*@B6G@0oqky z_FniLZFg(W)e{^1GKeuSlP?@SlcvrX%U~M0xZwh``U1B4?^Xg13xNVNeRN5yrY6E< zYSGG-u&5MJFoQ>Tw7#W?oG6&7qdQvPQaIo7UK-?^&5bkXUOpVO(5S`BNrGlTtCbgQAU<>2)xL^foBEun%Y>OFTE~w~`L03sfUjahcxIbu31123(ptlhm|>!& zd;L}hZ|sMX875k`pQ~FEx+7#}mT0l=Z{MfNdVa_=yTt4B?O)oPe4g1QT$?Y|(gB*8 zBwUcInewW7>Zwd7+n*)Y9LzG&qP!A@Vq)KYW|(=Hj8+CKduV2uXwhBME6LO>j6-RS z&ny!yx_jIAv+aBPzBAiIt8DYUR?J0+U#3EMW}je@{aNpOi3-GLw$RK#;lf%ZQ(-^7 z6_|yh74;w!@jS4{W+sZ((t}u7Pdx=@plI!^msC};5wnFgIx|hQXl@6Ao`0;NnQ5ZM za@E||FcGa+iR112Sj}#&d6=D|6|-ho$#~}Ayb8=r(Sljae=K6_DWky56fKv>Dx5`` zJvOsZv|1i39mcJ+omz)21!kvcwXDT5$mGlxyU6Smt(H3rY4%m^Ew#uj6|I>4C|i1! zMJ%lKrO50Rt(nK_PCVH6oEa;6B-T?7B4MVA9)?x6e;Imp`NjdBStxoH))&HljxI9e zM6bbi9oMT0^A<@fjkT&5nU$g!VNJb8oVl!Ti_A`oz)nXpFoiztVvI%&m#W$Q9A1rp z50)FXL)}Be!~IE#d)z9pITgwgw&&U{rKTY17sdyS0=@1bc)AFWFT? zvQ}uHVEgdBT@;FEGtNT>=AB?F_)kyBb|pmg2aOYx%|uW7_Fu)^mfv6%B^sdnz)Sj6#)m&XPP1~X;|m>;I9ezYqYD7Mt9;1>nZM< zKwqOID0}emsAosq-f-h@bdg)3)rj7QZdgGjNbtGIPm}2 z0`{R4aq$p4PqFi`N_Z03gCEn5qpSgqU~LbLUk!Y00k;gqT_S?a8vc~_{1j$^32udh zTux6Y`Uyghm)1QeiMT(ufa745t%P;Y^G8hVd32!A+WvI=)0tdJc98&b6zLbBW!L%k zKgnSG(?z;+U^u1>$BQlI zzW{~8-R&>uB6JAji^VdK^6KVi3 zFHjcDl|43<`3aJFmZWQGJ)$Mn@zUosigb{9iB7O3G{N@tBy#J7f5fZ`=1=YmhfuOa z7YZ=X@D4j?Lir@KR-6*ESCngb9|^liCx1fWQH7rEe~PT3OKc)EEr*->sk}2D%^IHR ze5p)*rHf$7wD)E7_DQIjy+u)bEuFxZTk0-R<_qlO?J_gkGM{Z-y=D1`x#Kdd@S>N1 zw<70@rNbsd`RIgloIs8sO zo)g`GK0YGD=^%5GO!i|7Oul756CTq53MP3q;L@q@rxYG02C;W7$9Xn~ALCb-dz8>M6Q(-apB&b!Cj|-)%NUqkf!5d*8X#Y);8uh)QVPfpod%(MW}s8wPw^;6pQ(_=1odw6O^r>DO^{ORY+lupr0(@X8k z9k2@&8>JD~YhTB$)YH$8DFc#y6gv#;DLubAJz`2}k9|t9Pk&!*x}kK6Hk2;6KUGVa zGuyzn8|BShrHe1=>|4N96tA5uwO)$&@xumZv}`m14NeUEvE`5^xqo~Y+JlcN_!xn2 zo<2PNxh;CNLcpsPD4(kM_20aYJrQ^n{fia3q>6}IjHrc)GL0`bvHd7i`#z)S`{-qK z`1rx|vwN|5V@pvZ1U2{xuO8nml*0$qI|TmQM4P|$nBJ?4w_*`EX=G;UD*;enGQ9v4w*ar6DRpJZy3W3WH^8P@L-SLW(wLq)X&d$ z&S>@c?t70P_GA2+CCfQuIR`A~B7FKV@%+aRp{BB>=u`1bUp^hN5%EzQeSD zTPAO`TS^w5zYv?HJqz>P9pJcMDI+9HWIFYbndjo|(@!#MciLn9?)4#hPlX%%zW4fP z6#k6iKYe)PG{8OPi+W>p{QLCh@zV!AC)mOd$4q}r3n-KIA@+Rnk3al(j~`~X9Db`w zI84joCdSi;#pAnMd-M@SM=90!${)XV8U?@2G|99a_zO*-eNy1+F#HSVSWOG~l>Nk< z*nY)TrB3io3sTst$9FS5|LZ{JT+FP8e3YwTaqSww{oq7Qh}n$}u}J_hf&swZbMHT4 zAC>KY{ajpzI4>CI1)lLf(%RxTs|3FZrV1S~C5v2D#7m;UfS^|)1U($VR_+r)Vj6(% zvmx=pkoXH_jN|tl6Q8{H^Wkg1KutxM%1R*2e<6dPyQ~3{Dr13j_(V)4xwP!(YCVA8 zW%yl$zmg`CD#8EOwm!x0Gkj=>GZAD{$^hU;3?5dq5Ob1p8Djek8x^!j(kxaY`Mx)d zRXA05VrJe7S#w=7E>I9>`)>a;3m;x>99 z@Qv31zRlp{GQ>&uauwg1Dv9h+^8kJ2A_I)a2nKQ6VIZF%j{BP z?!Pud=`}Nx{CgyE3S}}u*4Qji^YJjqAkd@y?XMi>`r4RLjS*F<=gM$nKhVt2)qtO? zWip%!2Qf8X6Z8gHM>oDm%`x$eJr)POZ!$;MtiV^IYHII@n#>V3Ybe!Z;tfTx5_A>- zdfH@DSQAWPO%=P%zfEQZO<)DhSljT-#mJgWahew}b%BoI#I1;3w5LTFP^H04vKxE2 z`z_Y})&)dQsNPq=BB0Ht$aWQGbdrf)as*Pu);{!{5S*jM=JtK)AR0D}p$rUxR5KT) z?6TRn3ufPECPo-lb=kbq9pLH}iY1WnbE7uEXFAipK(9hb;sG48v1uLVrUY*g@nT)=#W9L=2ukPVM z#0;R6Jc3nGZpGxFj=vI2BAWq4RpVP-YKj_9E-kqqyG~znm(do1KzJunooz~ zEQ&>-2jn-@%gFpF^E$gfk&0AW%ad9BfUm`wW+QYqg$_n2SD^+=q=U^52|CKeVoJht zWkB9=$Xv>>_o6xIySeLA5;g!sFakqUf?+LZvCCR^$o4>nmq<+8uM_W5B-5yoB}=Vk zEg3s3K1VDf8?{|$kV9~`WbmC=&EKuGKZR$;?^n>3OmjW>nP46$l+WgFn5ssuxnG3S zh~H%J=rl&oqqUPc_)Js%0m?WSyMfcO^-&~$mpv>am)$6~1RTS=nX#C=EfvOWWXE7+ z$1)~mT1YTFK4a)-EX7eR&K^uorLhcUtlmh6Mb(&%^LT(Rga&dJIV?$-GrEL#6SqHm z#%H6o{w1^F%L*h2#4HMEEQmDqlFi+hHOR1=b;56_?^L9=?(UNL(#tm5ybg4nP>IFF z#Vl?J15Aiytm46C5qHA|3Z=%6KlbY4 z-ufe&Y;5ti)N1nh<3M;V^)QL13tN&FBWVGW*5kWangdNkV#`3n-!Tq<<7j0rFW7>U zYnQu*!?HHAE+^TEY>nK13=P=5FciowEzM1kfjvcm zQ4}b}Fk0O1zQ~%S$ViHm#24$x8o$KwQEnfhLYxx-LYEnI8D@b>MXt5|DNY>*@sAlk zN*yGVrFwz*PZ-AuxPDbK%@l;i4y_zu|EI5@xhK!QIL@S}z3YXd@_{R(%73>kyjMS1PLO)^XCu%Q$nv zW~K^@-U$^*YO4UfM!?ZkD~%K5J2GeIhsX_rjH)r1OEGb1i`*y3DBpe*rwa$@5rGEF z-BFa1QmY8~iU5OL=2y}|8ogWxZ~K32K?yWtod4#mcL+Z0w+XmuLGe*|CzUPwc+?^2 zrUg`~$g-s^_H|`QuuaS1{(`u(LmshsVuV-V3J>HcR~c0RcAjCQ{N*aaHE8soZ`IEL zdV!&%3V=bNa2ZP4`67cy^^1Z#ndD|BF=v*_+|a}?;go&Ouu=njCB}zxgBzM8x1)6; zDKnBX`>39Xb=0wPvH`wh-1BRE9_YWuS} z`+z|WNQguoBN%m5vWax*{1G#16m?XxnOo19RintGvX+}T3Z3woU8C@$x~sdTO)txh z7TdoLolNeCnK%kYY9d&RS;Um6KNtZ5X47anzB?ojT#Vp|SuT zFh_w!7btIiQ^Yfu3A(^MQsIbwfToDT&?(F)FsFpl2{vV%IDs=)gh~|eo+`|oTt$KT zq5>?%^F9_CE|W_?fcYYn4$<6(G!}QM0--`DizqO!gen!c!x^a zX3DSt4ngt0c*8TJ3&me#wu~wm@7<9*2eap5k$gd}k_M(;NIY4{Qta~szvuRXQq8G&zwGg383{YZ*jVfZiL_Ex*OXws~ zZGX1rQ9YVMOU$B6r|^tr=CTW2W_FBfRt!|)w8dR!R*cg154eFr7g1(LT+YMNu4a3? zCUgXuIDG;$V^kAhm~9M8-F0zWX0{B=J#6Cb`*8c-?4N%-M>_>gWoFN1zE%GntIK3m zX0}}}Kl@>WSV^;+=*Y$l9TktMYK<5z=z$Ql^fI&b@(C84I|H-zGAgz3+>S9+v}0!N z$Dgx_&lk^-K!0`2tR7WH*u(#>5Crou2XG8aB$x@%Wr96smVb<{5r}o9+=hbL{Be;F zg)<4W^ka0NPQ=tVwvlf!%enOPqnGa3`;JQJ|tuVKcGs?c4p zWuW+x94274jVc|~W%fnR-^B_}zWsz*I4XGON2`_7gnq(I8)e}ii`ePl?1b6#Ngjj$ z?iCp3VTO%r1z(ETyE;k3uvprc{N`>^Syb zAr3n9C{Al;8xczH58lKg-2R1L3(7xa9|VO)M;}FPCL_W2eHJQufdp+=oYJuB^cWWN z-9#z97I8g3BruU8CipIf{H^;nYZ=eA#q5_G&t2L&nx#!y+K6TQe+{HNNsn>#&Y1n}y}mWUD@O3j zeL^{eH{m44|1Qw{t<6^L8_nT#Z4cV z{U~0UT2j{dgBtsg0ZI@%m`=5v9Qtq8Kp6kK)zPP07#}Y#?B=@5U+#D>z>nDvoNJdq{lv5fxJcfzv{VXheA6(GDa#V->hx3etG zLSajCJtidAW5nkMy|b9Ax^7=2#yKCF(>Fo zfi?UIfj{B!&utnbic>-Xwr8%QKxVmqDpz*k)m)_tXQYoU;AzE5%okowrJL`2RS`~N zHJi)bL(*C-)Lg8w>(hAs_6X6}DLtyrGLi92$}m|ltFGq!gB)Z#v3?j2n~i*o+%4e@P0G-ORZW&EobSG-F6J^^h?Nsv zgV}*;LEAE+n|k2%`5#+=z`h8mSz&@Rs`jW%pV{s=s{V}2h1OebGS_ZefSk;X70z57 zV6}NgDXyR;$xvio^PUe0=4p=@PkV&w&crLNVXk`B=`oT{59Jn_F83tQO}dz=Pe-QT zfCdvfdboaz613pZ&lhPF?o6^pa++{1s?E5DY#b)81~cd~ia{4S&>)T6JoJzf47+4T zxYLfq82!nA;I$`W}mHeM|@xpjTF{gAnO;^3x45>} z?NAbw$He&Tl?X%OzDTa&QXU^$K)t3FOCnD{rbi{#gA!ZK+}0qq9@Xg{tSuI=b~h=X z6&6D*xDJ~Jk+BhLMpFN^pZgd9aHB)ciILV35pfxmu*5YV0 zbBk(fSCrujF+}0L(@apWu&Aky3YO}HbD#re$Iz@^WglT$zed>U2q2M@!1O z3@18bb~cTAV?=32s(c*dw6xUhgZ+SnUv1kpY;J9d2~Kok6ib(>La!aGc8E9;>~Vc{M2Os z52Z_hRKK7yyMQvAImZ5V8j#mfKwi&cy$mf;jg$X*l}+p%3N`9%XTh`_^2}5?$>RCqcdg3^*##xLARna- zbRx_>Dfdt7vPr zI%n$unqB5zOv@ohAd-1>?;MylsW)ySZ`=%`Y`WVIaW?tph;8^Z(UmDJG#TEj>B5QQ ziVn;b8kqY{C}&P5J}qkUEoAa7;kp&yqMM~Hv{~8`t2^hRXjLvL$0g!e%h)mQYt`89 zlxYDJX0zWZbZs@5Ycwr~3^JB4+JJ4f>4>+{h^xTF$c|eTExKdaLOX^nz53?S1h({6 zkIJhDuV1gs&4QKsc<10O3mnfATl8S8g$~AAdhwFzC9)F*J-2ENp*6;7AYBixS|c{2 zm=>gH>B|(2hI}m)^0iD6TQ_OZIIo4`JWZe#+07{112*QS1ztW%Oc3c$pE-shOjnfY zieGcOGAF%GJU&w~(}H;1_xReEY@er%>La#8?T@FyX66*@bsCJsw1BEeVY2^{-<*O3 ziFDf2@6b)f4%$@gI9^lfwAjFymP2^`F^Tz_>`woymAUP>cjyI-4!VHR(P4fd+3p3Q z7&A9xT7Jj(jJ0oeq`RWxyF%g%qkGqD3*8D+fNrG^v*JyKv)Am>d#YV@PqnL(-Bq@o zPU}@)pu+;k6-Kw(qKqx{S*wXmox<2|jfKjl<&Z(Dsq-0Sx6VAZX#pH^=A>U;>TbHo z-F*8E?h4SjqC{7S=w2m`kxaKu6}gQRS%uRm2;IM=eMxC95sjV(T!Wb|-R|$A?fz~Q z>PYrQmrA-9%%7Q-L#E7e((F<1(nH>5pPSFsN_us6ZQHaQlBn5CM>LZ-%H|6Am??O*Ay z3fwoWd-OoRhYs|6QMkLi?7UCbqw9zEeweQ%;*>BP`vn{%cG zFr=ybJ?uVRYxLVAUTw^4$I|75sZZ~Z^}8LW4LU{_&mZ?-6{mi>-|bVbmnt2-OV&qs z$+XhNo9}zQ-K&%RqYTG~NqwJch2LXe_mYP@{V`j4m=;VVR(HSSFqlwLUol3$LYlWp zLrv(7MKFn+UN`jV%D0bJzTa8Z3%-i_^fC46#tkKE=04oBf^CcN4vG?A!qyW2dX+Xf@FZJ@=s zfAQk2)#NTNilOg!yTP5T{bO%;bqs4Ms2EVZKP{zx>)JKdFjUYXh0QVFoqR( zb#(1G;f(0s*9h%>jk46$Vtjh9(nt3yeGPvv-OfCGx&Zdk0@ydM@Xym zO#qlIysDl&NkJ)2HKtT!Oy#ns^63iHM=MZ2d*iwS_0Q-BoS_epZyYy?{3f$T(?Ul5 z_qZ11w^+zzS`PUZ@{RLVO~2Kr93YVK(?9ZB@|jn2gk(fXM(9K0$o=My-(g|DX#rmn znTNWu(<;;N(h=#x^mc*k<5{2HZS&FHHa~C;Uwpc^;iJ8cy%N80Z^Nf2Dn2?<@y!DY z=dC-Rz5w^p3vfRQ)WY$I+aL5OMGxjwcizA7>Fp37-459eiwtMED9rh8cMp8J)$OCL zZa<4-x3g`Z#^FAS!?htUrTLDrYc>vlOl@V1Y$a9kGjBAo2GeaNA8jk`hGoC42{TmwySnNSbxfH^d0Yn_;kO@AJY)*CHqzW6+0d@ zEr&$+BILJ$;F8F{qHgjExygHz%gt^4uc*_%Vov{F#7^PYnBLAEquaS-vp8`aiZNYv zjnS%Wyi11v@x@dum&!?IV;Z!KQP478Xh*5l*@^?>&ptM_e%RwXpOPB$5z1zogja#qi`tAy?@2ky9S)L-m8SOSmL)obwH431bIeK7i%@M#xF4Z0>f`g z9q37Gf+8a*GJ^AH5n98S7<`Gr_t?4-!Iv3)nZXZ58mp-_!_nu2;plU=s}_e|QzM!) zLHVrG9?&&%6}+I^HGquS%Y}F#*}xhA{G5TG1MpiB+e0@m2z2uT)wgj#Zubas8@Sc> zO2#r(w%DBt!R{Px%zW1Qo>*#Eq&~9+?=Wx>1a#JN6?q-=h|;?EJtmnRkW4R4Oz%am zm+C;%K}KZ$-!&Y_Xe6eK?}=PjP8Z8XwoC_UX0=Vqm(6?l*sAu*2@=NdA*u!=cCBX^cqq%o?p$ zSxCad?jcR*g3xq8z_|yPUMIU1K_K7SlDujVl2<^FUdgv_U-Bid#*AdlzeJPCI2D0f z+R~8yiCqJuc`M>KYU%khHWw12$Wm~<9F9iJ&q31u@HWE}K<^N^OMtuReXTji8JU;7 zw&*>A-s8E)o$&hvpT5)4!dcpaUl8yM=98Sdenrr)+#JRMa-Zl*AL>f9B<0ik0Jp(_ zQTQY`0k+tF4q=}$Y*h9pdh>>1R{?g1VRxJ)g0O1@x&~7=izP{H4J1+{ck~@jF@0Y@UNSXbh#ET*pEUE-!dRAf36-R6&^E0OAXDHobq@^-7+h`8T zel}tZVC(&iZKOf#Z!*y|(L!EC8CjTNaHcSL2T-evsY9IE`&{N0(QcZaHpyu!0%l?X zyMWI^f7_K$!I_KcR8iQKP3VAj0XXxF9Y@!Q6tTnR&kmSBJKOiM)|Ze@Mo2V$M$m_% z31=d9(Dn{9)y@FI>;5N+aOl3z@O>owBvD%3=Tzf?vtj-n1-oKm9~pI-!gni9$Ver- zOl!I*yVCZ35M?U|jqkES?GDfiz$K>Z5oENPE>>(D0WJ%=dNqVR)krSAW-b7|$M2cK z>>_kf@*dN{9!drx$lf>{${rQCo5WG{!aNIFXgz4xrDr2ESKAJ0|Ayh;yo3Oa>ySl{ zLl8X?Uu?*3Ri0JYNpqM`v4)0p{y@c`ZC%;uSk1G{GE z*8_C*VPzk}yvmWZTDt~N@-~bn+i!5J@haCogs(DqlsUVqqGhz(Hk1wwK4b71gP+Cr z_%#9#-$l=rmX4>_si_K$)5O8*E5O3OqUMv0p65QJc1Tb^Cj@nHfLXs)lOS3|_V8_j z-sVzjhe;NYG7msP9i~YzG*jF6y#6`-f-d`nZW|@zOCxqh1ktZp@9_qDAanf(k=Wvo za%Ef{e%Ek-zkO%Q_j%hN#}TF&9THRseg%06kw$@9Zol?=Vku`)xcypqOQF2+BHn(r zRJNQ|!db=XWaEEAX-iWhG^pm%CVe4EosfW#X#1l(sT_u;L7>s(wvc*35jgbwjDUk( zk@kL|``fP*WsTn?_)RYSMi6Yjnprcn2m^ZSmdaRezj9@069(WoD(UVUY?03i@;S== z5kz;|uZAkISDg;Q2OAid+izm2BCn$6V*7Pu%g`kZD67==o7A14M+ndnQoQ{-we&t? zc4|1rr-nf)yh|C2JMjPs#)kw|Mo_&(XHvJ$1hpO^sG-xNg<6Rqm7xgdku}3B#_$R< zyv@`qBP(D4-|z{116B>1W-3hW(aHAUm~0OU9Zm>)^cF*J0rXZT7nyVve*1oHMzc1G$`MsdB&`HVF0*CrjGC53Qj`3>xf#Hv(@M zNAp-F)`R(~M(|&OFyvLHrVFxyMA%GGUfm#UnLao>f)SHED)uvydS$f80-4#u`%E8w z^juj?wXNQ5-@nbIwXgJJ*l$3I;tXW@^Tbnx~`8ii^q?zUf9Tkj3ipBtb*H(Gyeea;oTa(RWXT#7d$ zB_n%vxb>q!x(vOGPTm8t65bEIs=5{P?KcVG@c~bd@$}#>%4!l3@IC+XOYZYn;||&ccmP&8 z1LdF{uPzAks|z^uh3v=;DmGI+lp=)O`2x})4(p0Itkr}aLx4VCGnaBrTuQ71_uQ3G zA*Q%KB&b~wf*KHcE!$+F%Jsy9h%m3@N-u3s-)~^ zRmKxFmAP7EU|Gs>MCCIoQo&TObtcho9$V%1kM=))rY&cWDMAkwaoCG4_wfsDIfgyL zF+|B_O&X}kBxuU+bbv_Sqav0uV;LisRK!uvS9eYtN6K{X8pwUaZ}rYmXcz2rP`<}K z)mSXEG|PRw+Wi~-up(^rbGXduz5#49>F6;`72d&W^$+23Le_l)SjR^}mddF;dY7Sh z(M7y~oG?M?9)m_(9MMW_#5}X__>iHacOwytNFnryL5~o0tk=u4=| zjH-+-K;Fe_9jcuym$7>Km3;9h9y8u!u%fz^MwqctnTZOb7c0#PBRv5zuU^GPJ4#I_ zUMSsDMtF*@Fzn=-$nf?>WH<}OzTr@7R2;2Ux~II0X0cemjFh>Uqa(D8&>HjfmH9|* zhSAGpdc<7KQ69dio+Z04OkVnd%+VYbkPn@Sm#O9vb1p}~xeSt9xf?h$f4x*&n2$LE zKIZ99)kN+*_A-2-qd8)Z<_I{N%WU}@drx9@D_1Y0WIpBy_!$2#P^lrDiC`ttmwqMl zFh?gSd`(}fEzGYRo#KZ#`>80(g(mgM`i0&s&m2ttb5vdM@n5S|_OkEGGp~|Af(wnA zw~t=z@W(lvu5#aSxOk6s_#c@>oDhC|2#?DL?;G&lL2GEd1HNxSk_l(>AAk5|B}}Xt zaJs#H1KP2kSpzevklYEgTm!#{hv01n4tB%)>h2CFI)u;z20bAC9%RzK=YtCZKLA_5 zPRKqRNgw9Lw;o8J4W!?Ixc1|CL3Y~r4QQubgd6NtE&z6oVT1K;b@%uznI_~#BqV4s zf(9k9#_lj|^x}8>)ilX0igWUX+-# z7QGIRtVgwZ&1kL(jV*GkN|0M%*Q+INXWPHcRnfMCW}%bPjfU&o%x-t}o^r?KpG(LllJ3(w3>igazNiMk`~u zD?K+pq%xl4-AVL zwZ-nU8M2ScrJ6C;Ym$e=dUhvq;02BYx8FF;D}Cl9`lv4MX8Uz4=bOd$s|V}u51D%6 ztC;q4`;fWOA#kI^?fa>M(`m?%g8aldznjrSAw4PZY3UkcQJybtj%@W%{)40H0OcxkKu!ui!9dUHU~ zo4_43BR!v_B8XV`yN3klY^?Dcnbkx&$mX04_IVY7-3`-H4mk9!Ue8$0a=zPq+LMy6RxKLa%iYrWku@8YeOo4Ecr$DXDO_I&pzd0iqvU8%k!K%L7oxg89nF!MoPFiC7eQO-|*bm_%aZ5aPapq)$NR* zm)mm+42don(Z#bw7b&@~M0mPPpK%VS)t%dP)MN1YGIWqF*|_(BH^*6pKK;X%=m46@ zzjwi)pOro8exDh89~gT-OZI+nu1TJ?cNB;8ncnutzl-WQlZRAHa{Y_nL1jBOL#9+i zpj1O~m$5+^4gokDlwp;CKRXzZV#p|lPW?Yb9x=B*LOEUBoUG5J4xe*sM}998^{h>x z0L^FOhqpAdl_?VO{0P8rX)>1PGvD&Op5o789?I`>BHPi%&tE_NWFRYR@IC|YKO>^& zXH+O579qfgu)pDXcEAw}ixA+R&u`S~cYF;}Eaqq7=Fg{!MWX}wIn2l6=e-|4=UH0> zA9KxCWBAhXe6wE;ScmBioW*DNbCgFq6xn=%N4){C8w?!Xun7ddV59$h4`BBhb|1Z9 zT@xK@Gl@|@MD11rH>sunb?=nT~)=_HLaL($0W2tmo48DcnGh9uDV6y@3cF$r~az5%QxBbC?-hIJfec+H%&rN@S|*FqOoh0AdE*euZlvqUcn#eCs1 zQS{hE(Ho#8>L#)u%KQkG@GB!GmJtxkC`zW$Y88d{o%fks;L%~A<_>}x zR|xhPEP}@74rwf0{Ne?HN5xkH7d^jXUhxVR$kxcuxJA(MD`rPmEmF1E&6ciM=yHWB z$7r|c6uVr#X6UdQ@)s^sMwdiJT@)F$L|w%AqE8zjw<-j>1=O+iUw$re0T~2+-XqZG z!0BGgI7=2H5F2IPcZT0XQt8QfiC?KA=rI!uv{D&(m9<*ig5MDA8`SS{BHj?+bpYKb zd7bwSAYe0ssy+DfN#k7hLCTe7m>qQ%w)2#rWi2__>3Y8AKy*2 zB01CejK)XxITM@&10h%02UJHW57OfCU2F@D6F9V40tb_He=cHcnbgJ%evIH3B26DZ zBrXhf_5pS90rkgsGp#>6brqV$Yl*cC)b0m$v3h*BSLM3Otm(Uq9xV%Huu!(h_z6v$ zRJe(RHSRgXJxAFqZ$7uCYi_C4%Pg(M?4dP64lv&6fwh*;Ia8=}pit+= z)|SRBtl>Kh9v*J5R@qJ^J0e-E9zR&yb%$vWJ|m8`&r8M9T1XwH_MI+Vmy>%u2)fI9 zj!y$;kMEY&ibK}reY`FgOC3+l?xno5#qTlQfh##-l;`nXvPffV>GYW3;c=s!Yl?u~ zj6m>g_J_+w5vse)S|xkTH}r-Gd$!m(&-A?;Msb7gZ9hb@{UqFHlJBFG)Q|7xM)LN4 zVZeOa0Qj`QOlj4$g(k;|eSDmlg|}j24Smg^uMzYtn=a@k9e^LPxoQOFs?p=STalz% z<3n3~pE+ZAF z{D5`eFPI~^0FK}yO?8~+=m#!X*a_3D83r?pI9TfyGpVa8-fHm7Jx6jPaDdOM>$FLA z9ldxACoJfH1<*$f9WIC_j~@bCXmXMD6}-r*J6;t}KZ(h?BZ99ocvNEd@!dq(_q@rV zo9I^Qe4!(cnJsvoiKSja^-UK~AC}h8Z3Yc%v+7uwq=<&#uNXMU(_ddae(+v7=wXXV z1#TUgs;@XV1H$ewY!U!ZMDY0TAAk6zH9iwNyuGO6rNfZ+Sl{uDwN1jNJcND0urHv2 z_Oetv#?W46`mFPPSn@|N#BaqLYj7rYm_On1Ln!X;d;W^`{1x_HTGe$N_V{6A@4xU< zv3k5eLO0RR&)1TX6 zk8+>G94`9?U;$=h_VEJ*9|7@V;_xSEXm{PDV1O-L_5ha2VoT%U(t%Mx!C>Qp0C9CN^obvsBNM-7S;r1Mu1Yu z>6!RJhi@(ebj<4%xe*L%qPqDTi;Ui|- zC;?pK=_hZ*!r?=Xm~o>_Z~o(l-Pn3-bAK~<`kqPO)5uO=4%Iek< zE3+GB56&zb<#MYncDq$xPw4qbhmd=TEf?wI2eXM~KlseNQ9ieB6g$d`**41F_W1wS zj(uef&CD8QY3oFbxt=$kK5XP-C#mRuBV(_kWv9o;nv&T&%FzIY-Zmm=i0S+u$Fe7nQ|VOax02g(Zk%O-^`Tr1+*WuaES@> z%#!m(w4R>{x}rzm%wqGvVqZnp29;;Fng_P}N<6Gt0F1DivF4A_*;(M`W%A4}^C!P! zuJjj?o|FT7cbR8an*UwX@_7_KeQ=1o3e08;pQFXkB#IyGp#2=SSo3p`LgyE6vh81K z^tp(W_4Yq({|XH6@?Jg{{SZ-nwPqzQTo?}Em2Hnrx zzB8umDlqbvY~KPn64xQAo}Cj0(}L0oYM=M!R%r7Ou`5@XeN7b|n^%MFSIKhw&0I`2 z^ZE9#>}dv+W&mk^5zeyhH;K#WR|ic-GH5cnIerH&^6;?*)yK<1O|ss8t=0JjMpLg; z37=^J2LOTNz|?Wk;g2ndVE-+SEa%Mj>!=88{dW7ecVZpQU2U^5=n)>%a=1sZxfMAt zntMI948$S|QfI>JGe&rQh6#UTsL5KY)RyP$yiRyb%i-?Op2KaB&d$4(qYF6#S8Y3^ z-sTK>o3mj1jjQuK>rzRfyE)xP05_g3_R(+i_;L z)qi87ZcNvd=^9G$@6v1{mP=O+Z&jGjF)d(s?@jpK7rkH(YBiW&FfAZWYA_dXWDq1y z9E+_}8k*<}&+)@vnM~GY_*|&gi`P)#eMH(i~JeQQ`lJIPQ zrH$2bk=D9oZo7pN(zVtic+o7^!ujbwwg4U(i@7w5TBkU%O(Xa=jNpHih?h8oPKDWx zX+e2#W_oQU+*c%>a~52imO~;v5hczGh0b8eNCx2cRcHInK7wZ}w|1N$(T+kmdmOi@r`EmPXXaBYoqb4&* z({gx`QV)ps$$J;$wnZ~)UQ?#)-@&wT;c2lA71MIaBR9*%_M5;S9SKQW6Cprj>o8(3EEuw>THNi z3*Y&@b&9S1`XyxngXC}jR!=kgL2~^b8m9RuO!H-s>Pbz9hK_3IH?AqqHR9Qr`T6&V zKm9W{U7Hr57&_w60q^#GAm(cOSKjuWo4&VLcx_sKH(e{zLVa_q<5Q=P>`>MYVqJhhQuJ-C zYNv|sR^|JY)wgBU^NZPyw&onOu*^qKApHI8VwC~2dA6mQY9vkS-I)uCDqHz zriJ`2qq(pq8`JgD7_FDa+pmIUC|-E(F>bLhGcAW}z<;j;O}V*sT+oi`%502QW@Dq@ z?(4tH2HW*EE$}FKx2s{k$3+`18zagyV&~azihW5}nU`plc^TckPy@#7KF}E62O285 zib7l=u{{Rh1qOb2@c-Gan6@|jUM12K)b~C8+mFAIpV`ut7#+MEx6*<0ObOeMDdY)y zA&;JPX&@Cwxk`{mUS+1{^84PmKlHc1oTVy`0&DIf`_JzfYLV zPte6RHB}CBIbj_?LD!n}Y$6@}Nu^1^Dh z`R@XPPbvD00%kj!Moc{4d81F!hYTI%HcF=2R=uH`$}n;vdBsQ$pJ;y8{a)p5rUHQ@ zlJC6Jr}!g=4<8Fl>dC&FXFf$AGxRYMQL0=9_mW|QExXEEul5y{{4wY|dFwo8MSxk~ zkAc*y%(&odWEs!Jigi7Y;M)wojWSAU(YRPul^JdoU9QsL8I}9D@!_ZN%xJ59SYJr4 zpK&~JU`0!Gj2+?|aqgfz*8ZWVQ1NOO07_t%4?a6Pi$K{&s5p6u-*w zt2nNTCL&3g%9g<_Kr)Swai(#RNR}iWTLeiHHzsM~l%$RtO))j*FKS&FU!_`ylEkQ$ zLo`uiUIXS!&q5hFc)S|(7&u|NldrBZcTofGq86JS4i|H-F>e72kw=LPAE@Ra)o0!U zW&kv$4wTcVGXGG!A@C0(jvV^mU~1C4FsMADrZgf4f8o2UgZAISe$YO;*>L`!5*YT^m*1}@I5G-l}liL#_aYC z-7AfwL`RwOvYzR5-wUFvA+RmGYPEJ9~4jnqL@4|HU|-HH1Yo=K;PF2ic+By>*@ z4sDwaZ5s@2dvz!I!VyWQ&AetCc+Iw+Zn;}S(|LzUrvrU{lyc2MPOZxvPq&7$@K2{M z^HrCrWw(LBrGrE0GKbJ@qdnMl6o)Qa(`8DJp0{+PnR1Z}n+dyPwA+{}>gJI%=w2{% zaE{*1rc>b{mL7{=dsSFP;6ZrO(<>LjV+ACk9#hL+4Q=&KWlT5mphxa8&FnQ$^2{Vr zPSbgh>1MBuvQGvgbTO+Q3q(k2&Xl`*2+uShR&vm-PC-bIxyv59tEtyMB3Bd7g(6T( znSu4jh=f)m`()er6ZAe?Bci-keQ_5B%3*=kXX5V<&>he^<6&$!v5y|5v3 zX+z-BhBBFQ#X&3cA&c&ZO#~ljJiIR>)Kis9ATne|H5?(WiC0c4$sFZy48@*C%1QFf+YB$DPU;1@4i_%siE=vl7_#mC z;UyB$+G&$>$o3V6H%NQZ=XOOv-$%@+jVkDt{91-i)9r})v{4P+DA$Yf$b0_W5-uY~ z%=wHOlxBZ-!R1?+3qtS7Mq-{NE-~APc_3V^_L+T61kTo=qaLa2b)-Y z^wdMF`F=YZfH6~kSQtu&YtNir0tFf~pFD1$JYz9GH%9T`n0XzTc0PXQ+!Yupgd+)HTGdTewm~$q3oDx{P+?M z>slo){{Dh3fiA!j=whav!thIGPv}kar3Pcy;h;ahWIJD%RdjJRp6Io0u9hV#$_*xA6_QQJ0@J~?|l`vZ4inRzEr>fpJ;EJ-5k&eCE95IuH z*{XNKX$0y)Oe8K`3ptqOq8!yd5$zJqaut}TZ{Kjh@sB_Jca_`V3Fo5y_BGh2=skwsgH#)>@ zNqhay8obZI;c2TjD3U<>5PZl2wIK}DTG4VT)NK3L5Pb;H^9&vM`%0vzEM*N{V9>zV zx9ce=!&(Oayg!dD6)a+x8QQ7-zGmPuM1Y)Uhz-9)C0NdsJzGa z|B(oX@ndF)YJ0SFujlr?W_E~bdGyIW9RyrpHi$}hTxnsr_`CwM!om@p%~OYPvcL?n z01Z)lCTq1WFgq*&I~+KQgqb2rX30OR`z}V%tE$bq)%HrpELU)*Dip(gBFj}>D+C^rOD8-4H z&J%})L6O;H5!hrWTZvU_9^#p@(*W3chMh-`epfQJMlLX9pn}y*_ZdqYViy^95n(6F z9=XJjQF5SQE;rE+dS=aj?;x2n<0zv9M3c-K_?Q77!>R`=N$dr4!mv@arpMB&ij6h+ zDFZ)+tGy_&R&czJv2OrYf2+jI9-A2^+R5nXXmBOx)&f6bmI>>f%2-Vo_8n)oiFPwO zN;v4?5i?D=nV|ypMp;8M+eBL#9i{!?MmWfuStZ)c=%_^5_nVm|tY@l{Y-KNPW|e3w zqodNyzSGPk(N;$1UIfw>InOK-?PGL>TBUCCGmAt!8hV;c#6($7lX+&4Xjh{rWBZ|G_K5a0 zdTLIV*$|r9BihsGnY5DjGjN_+B-qsWte3?ioPQ5ypF-dTW|RfExu{p#6Xn%ZYCXjl zm|>#LjlNcK`|&I=(=5Q1R5-I9&H}Sbw6)O}akR8XW>$$dHu@W}QubC^U{;CtH8i#< z1(4aJGqXe+8~u&6H=Y8sOSG%uo1nx&N0?!vZ4F$nh!3caRE7S?)MWVVWSJTBEj*$1>mW~gYt<4Ui5>{W&tD%$KYclEqs zw2Uk#{$>u|1K<4|>~^F}oPO-v>I|O%*9dS8CDz*ha$}3zWIb-8E^GBz%BANo1i6(8 zDF%!Jyw?~c3rF~Kf_{!JJq04jRVHF)3*IN-eK?11tcR0{1WUvgbxcsl$k_(?PBg;3 zCb*#bz;&`&<~rM71a{ahT=sqUb6_!HD#ga@NfCOrMfCW`SV>xd<#R@Zwo+of29gVF zZ2AdDFzv}K3OuvKx5m#id@u(M7h$yWhBCD8eu2Rk&;xZT7M^zI5|?^_1Vu(rgajM! zQVNOdmO=ay!$%pTqbw03wZ<+p>@s@cn(FC!={C5>jNll)LS1m@Xyy{o=FWH$ zX`SpWR@urHpNRxYWOZloYdWa0#%Gc#K8L>u9kiYasKgokyG&-b_{?vXIsSNIKLE^Y z9us(LhtC}53B1lY6C$uhW_}XwNsDOVbt2o*Z0BI|i`~z`6}Vs`<^>WC0o#WR zUBb{s`Se2R3O^+9LlogeGE`r~y{lPv>h3lax#QnJk_zyuLZJ3xqe0?y;cyxeg%AGs0({ zhiYx|wb(X#C9Z$qKvrO6^r>i5M)6#FRbldo%9xB1W-Btb;_qN(MP4{(L%=<;!Y*5LOf<%$Io(|sd*Yp>F`k}jOgUKhq(Hq zHj#_IpE80m;~?DUKNhTrq_6`^aAUCk*?9N=U5c zo5*1hP8r21m5?I^nLhBNxskCj7cDwsHh}!;l84lbv2s zhVz}i%&l39jHSq2?n_uojHSfB_yH1J9TJ>X z2*DZ7oN*@N`^R_T_6e0!Jp$#I2VhU=37AooeM(H zxnN0e9)HO5>L7|9qv!#OUN(7r7mKAQOq!I*Oe<|bF^;GMvI%i*`YU6~EKXcBA^ zeAsh?IL4F%wSYgipiAlV`C_BL$a#2%xG?R<7L>X^h|B@68;jpyET#oa-$At;ts}i-_97HKvz2i zy4pc6Lzmm1Mb^+=2Hk~~X4O&)=L?~1@o@>&eFNIcTiuz*)wbxZ0YPsK&`Zcn+%52= zg}^(k>mAVbj_&%3ow6cyJgx5=K+QFcJooxyl`e!e!;moy8G{xNyK00O1{Z{300PB9 zHrxKSi|P!S{D&=cioX48vjPjO;eDq0eixQw5NrFv#kutN4Jens2t%=4Y~QE01UT#T zz5(1-7iO!#zWbNVmEp9|j_9u|1pPJe#jk~$hhh?C_5=I6!SF%C@7LS+%gA2iUo*MC z26BIGB(q%XEX$g}X9PYJPNHo}OUF|9RlV$i_`8O~SxPLI+dqQ~x^5L<`wZJhw`#Pf z$@L?AbdTV1D${+#;Z!9zvDuoj25&L&7QB`jOyn}xlJQCg))Z|<0d%Ljeg9SnTj+Bp z8no-Mm%N`@3;w*z(7W(Lc$(W$*uwW&=W&U_iAcBqakG7I2VWz{t@{RWRVtBQAiR;- zz_Yk-E5&|FkY@yWScE<`g(|GU3k)3PnO;OHk@(yOf|nRL*buJEFqHTi3&bun>@u!& zY_1yM#2pa+n86>T$K{hKR`vsW!m!cHVKt42DFJAFD!Fe!d&_TPwf$La-SZ=+>*$E@ zQpxNK?<|U~g?_{|{RnD$DC{-8&Fr*IQtJc{GRD|#fSqU9pyQ2Xy1)MmCH*hPj7^95wFL$^x|8a*-zM7RJqLCC&zz%Dav^vEQe&t(u;V;?hYbfZp(rNX}3 zCk*-oLhD}3a3bTmHNhz(I3?wb>}5_IOB*_tkJv7KKVsGiJ&TxS4|d2M{9tbPbNGH$ z#-TTiqqPcg1@&$RP*4tZFE|RK6)to0Nv}Rd&olHq_5gyc;W2uFp~J@+L6(vnni!t- z9XWK36N|uE`gtb({1JYHup!;g18mxVmI1@j*`C>ABh0$~|#pxml0~>=POotSP_#G_)rvp$#lgc^^xp5)*5`#x6 zk+L8VDoxcovu9ugC_$lk+1yoP`cr~ovHpOm2qGq>5+GnYR6>O_^z0S8fHUaLHmZTZp@is?{-|V4^xkqS67K@k`mg1H%a+084?3wQ!cv36s$YwCY(F zP)#}k=uA8(dAMO`ZbAqbf>VMIUja>a@GC8TXEB{JF`XWvYu0J_oF1J5d?u(N#<*M){<-P%&t&&565diKn;2z8rtZad647k=Kk3Y@RF6s564Bu!#BsfLSzJZXK zS60FrpNYN6Ua)#iIm7u;AbyA8cNl&LXGeqRLxv7cZJXM*?xMgNygMM^-2plheq$~3 z9+P>m3A0bm)x=sUd#uyF0X}|6WE_*!!4UtN;a|hPZ`g}C1m15G@O~TB{8>$`MLuF8 z9|4i?($Hx&m)7X4-=j7|ugN6&gCNr3r?(cd(u*-qCHn+;Q{D83w$whO_S-0hOc+^y zl#k!c?|u%(IU#9GyZ#|?l>nohV^O*gIrDF6i(Vt>AXi#f#%f{>TqnSFbeyJXAtxmfj$FySEGg5?n%5>B$uCA;x`FCu&GyBx={8#ZxQ6f^X1Q8nViMC z<9+*npA+8KA>H$Wcc`RA@&$x=NUFy z+y=b62mW{Y>DyHS80Sx znL+e2LoZ|WUpe&rm|>%bfpL~BRj9(H1HlO+K*e_gxlH57e;YdBpE7(@XJH!t)n zys;->_KM3#1}09rjqKr>xx%UoA`2r|Uv|Vy7FJ%+3=YuDVqxh8@en+HK3Bp?!kA#U3$>g=H9Iu(F?~^2}aQ&c03{)Sd2qkY%Pg8X3@I~Ml+ohpPuDWkx*3J`j=03Ewgii>WU+AY#3g35=$ge?24-n! zt&b&UwIy72#lc)l%v{k0i;Lw-#x6o(R$4+8S5j%eK~Z8BT0*r}#BOWF8k-qtsf;ec z2TQSbbMPf*q@`m>pr<|eUBwbJ)zS$evENoKF;gv_BJ4!WmBT{6%EGNf>T_Zhs8pHk(LEY4Me2breQmZUi%B+U`JVk31hxiU45Vq1dl1tI7H$J@=m zkmiv!c`XCrdkq5LYruM`iM{+sbpkyCr}iT`6XD!i?4usT?jh`zvhVgK>-J?Grxcjk z+v*iFrz>DiS1L#!?2s>WpOdrceam6MF%KdgD0yMD7Pyi__Y#rhDG55IRu9WqWXnLh z6BQ^?0TSJ1=?hqjlm(qyFT=+V$xCQTl%|AY!RYn{BU7dvWyFzW;rs3bVvyQ06qjPV~k}7lMz@3MhJURf^JZtx1p%#x$z^giN zUB2KusKKMN@Tv}P;>Qm!m{^Xe#iJAPY8*`;e;m4tjM_Xp7_Vk=8tRbNnI=ySA03j{ zL>#K+OOz~6tsk9|*JQF5>IF8Fr}mGI%WIE6PTVC%jUOGF*VH6n9p9o zLCqeWpw~q<7wbC}xbslkM+fQkXd+(_gXO91!<6<}I1?`jDe~0v(Ybm(izhGm5^DG8 ze7zCQv)G-aK&>7fvp@ddw`VwDUpq7sg-`nQ;>;F%^z$gpU^3g$jdl!MfAQ~%B+ec^e z?eOtOnb9=i%tNgo9nGJMEWDM${6zyqeE>S9H%xa*PDJnmmsX^n0G-01yG_M z0No?#X0wMEa8UC{_X&Dp_L5EVGBtj5t)M3!5)p{$3wWsYqdXtI$9KU?Tv?f#KT7tY zS>o&kd!=P+{$-TC=kdpJVp26Y(@^6t!FxoXGY7T)V{|kB@y8Ex`2yoSrXJw<1m2*Zyhz8W3phsi z^j~radqU0s1YXy_dr4?{LTw*qWEy2J+Ri$mc8?M>jk5XUyIc1!p;mu_YSU)nOvf

UBcx{sh?l=;_0)dA;IJL+$>ggv)feQ&5{fDPxN1Jk~+~3z(?opB$5qc}Z}2LM{K~ z1k%hhcMfXzColx>d&wMjN*%x{asX*;p02pF zP~$&6Mpuag_XFQkYWSzX@W*<+x0WwB#yF+6e~J>BXG{0}v7^tK-J2HhLPU>y_!3eMjhe-=$tBy<}7Ih~DozLihVF z<@BXTK1cM5-x0dvr21FwbldMn1uxhpt2ZwRiD`M}eu*u?d811ROba_yv_|h# z8G5Js++m_M!NrIXT!8EN=T9Hv70V_I;Jd@u1m3iuLy9}SEV70lHEsyJX`#z)Yxp+n zy=ejPALiCwzijjgwrSyh$r``M2KExY4b!vVO3B!=``4^q+cmD&Cc^pR@!bQiwBEf2 z@D~go7ecRQPk$b2A7qPP9})ce2swo~#0Az7baPCgo8T2zQ;Q|FMLuUTr{(AM%xp@x zSC)%k|INQ9$@>velMywaCu%N4Jh30;^ETsY6P`Dw9KH2WoHH#t$69ory>USAoDuZS z8AR_mq4%l;y;p_kJrTbZ6KixP`Cc1UL3{d1to78P^XClyoOS-z8v7N)euWA@#1Gk9 zTj+k5K=-@obm(31^ugMA`>fmjS18+~*U#QQzI*y}V^ya7aAM2jGrjSvFn_ytK-SRA zY5Krv`dMr(n=$M281(t+&xBUSNfu<*GPz=o^9no8tH*c0{+ss>p82ZI2*3k4jXrz) zUlqpT@T7zMhHA8-FmZ~@@qGihRxb5wVU-(Xa_04;EcE`M(aLH3REgjZttpugKo9Y{ zV*6(s%_Q~6ER`w6=C#Bs7B6FF-T=MMtBd6#5c_X!%yM87Q^+O z#(he6(`IxxXsz&ItcyvlbckPV^a#FbdDihcv5Y9o2&UybS!KKCUBKry1jF~Y+U>#0G_9E4S?umLeG z&&+Zz9IXc9Xf;qBartJt5K)G6Fnk(={T4}^lC%+t+HuhDI7Sp>N-;(hIvQMxTt%f` zl~i3N6048c$J9{;lzOb93}?e2cG#er)j*on&{2m`EJZ)!1aSE5BO0idN<(|#C{wY^ zh&22jrRX6F9he5U0vkQR$1R|#+VHz5w{BF~VEJflH6bSY=;CPQz7 zy|Xhl&(z#OvCdvm3B5uRdYh?*r`^0~?wc$ermE%zb1J5VMT8DBdh_Cp5}ZMTc&gqw zh^kdNCj_PiRC&+=<*l4J40fwBpbP`JvCz^c_~2lGtwD`33~HF+*@ZE*i1rzCR;K0f z<)U8VIZd6zUj{yncVw|I^ca5c=X@=VQKrjPScg;9(u z=*TY?93N@#JSePP5s2V|jAYM&97_M1>SEc^U+ zhf;J1MMrzUmCInasV=pV}*)W99Oiu;GFro}2D6uG;i7-qZ7MkPAm~xB} zM=G4qZz%c=LVpl0@}cf(jNH|4PTn16yK$Q>$4tv1g9_)`J{%&DahnRLjRd3~^di}1 z0Qzhv&$O^R3Y8>L&up|ILG|fS^r0Gs=1lOV=V;d#Y~gQO4%n{E>0p$-Q7(bPO@&F$ zw16TJ^C-68_^y%c?fV9>a`kFa5Mbv~1Ko8t&H%w#6sBt>mBR-# zD+Ip@_Yb_L2D8OJZxHPB21?Qu&s69&zKBxwHnZmFvi^5r|KoL(rjadpkAe4KmH1$y z^EJPAmuMY@(eSlI?hch?P)Y+)Q$4zlGUD#_(ok^yCfl^OT9{*Tdt98#C%zvSSmWB>5 z<=_8_E%q7n0cXGmoM}syfB$7_-Sac%2hM;WI18fK3BAesj)MB;;~&qpGCnBreeXgq zdPORQm&#bBQLLAXkALimERDiwrGNDY7voCv53RewKC%fuvKi=Rd!;4!%XSS1*sbk< z45v;KY%>YAfdtzkh}I5$?=}c{7p#lAGFn@kTc1g#52Vsp$(>d#`+k$#CA)^hBadmQ z{{By_dp=-JXaJnhAYMr8UXPfxM-6n&@t{KMPLG(l(Y3(Q_CNmpms4RcZl8HKw1D{g ze-{O_$3G68wA^R?M>hg}ZA5b!+Y8=jfwA95nN$qC@U&8BO7<@q{3ROtRK5|RcNu8~uw@uC2Zw7}#$xIaCytrP zkph;|O>HljVqE~mx(MeBYsoj*OS1-iX;ukmvAUIOMHHt2@be5GR(gu9aSIH$0I#OA z!z%<`WYDOfla@yidBZ5UwI?Vs0`!P1o2~7oU1rGW&6rG8=+Nt9hK*{sXuq$6gn3ME z86f-JnY}4F&-J6Vvc^AU_$W_%6o{vZ3KY#Kn*2nOUO3XhWHX zw+76R<6>;*QoX_ zpxSScVcTrw#t6Mj(Yp{`h4Vn%>3;}G7m$qljAVokTH-Kr_yU~P>|+b4BpcgfH>v}I zZCVbuDJM~AFO>$_@!7Y4!h7;wref*+AH545r)%A}r#dGWQui(3JfB@iCFos>j`HG( zAX5(Toi>B-pgVq5wN#P zD-DKUL0gkTI`q8F)TfKCq;}QB1-nDBI|zHJx$JZR!e)$>60ra`xbW?iT(`P{L+;k9jo?^qYvtpJkBYX4$MF(rC zx;bL^I^s@|-3Ulgq!g&Wq*#lYh^NXPzeMp-S-I#TT06+6OuEMnJTZZmJzuz2PsY|1=#s`{8!@XtEIK-@c)>YFK%g z*j-1LQ-_`xC^)pM;PFS&rSC-w4{Yj2udLSg7L}(Kl}8rU5%ENYF7Rav4~w)u{)ipH zLiA&b4)eh+5yt4;CrQpbDWg@M;S(%ztnaqRQA+W=2h@YqUXd;Z) zPOFqMHK{V32xD>g_#?BBaQJ{CWdLT?jFMEX?R`R-T2vWX)YISTiO_xrrc4d0j11~r z&NH)?b`Z`n#fQt$wb{e)lsD31GQPo{=!6oX>VZ+dI<$Q;nKwbj;Vni ze-6uYia7H6CS!my>vU_L6RJfg@E)+#tI#-F*~8~4d>-9p5Za5n4Dt!pp%Zu?I7?+< zk6onL$U}<b9O{ zX7*vk3H8q>$Uk>Pd>7d-D^93`K7n_GRU!jno*&qcFzZLYtO@-)?OJ6Rs_%Q>{=q-A?MI$;1I0)tZO1Yq7iIu< zfnX!+P)m^{*2qPIjH;M#-_J?LKLA`Jz$LWEz9SEz0B)J!meGaL)g;QMsePx93HULZ z?ULQv7HrDmjD3cx)jxTi4VZYW4R4v?#z0l_vc;LX-bEG@;+W6#Zc@VS{U=0cGx zn>~P?XV~zm**vlaE->H%T7=H;#l{-C$e@b|I@B8C0A6C?C73x)h3P`X8)c1OX86D$ zuSGZ~H+>QGF@px?SlP&pi0{SBS~5pW4UgcAG+ij`o*ywSMA4F->_|2|gv`_s1xgy( zq0dYUVSMBw45ooca4N9JWtxZLqLElG*c&Z`%~TIXL;gb8clv|@qoWtG@rE0vW08A1 z{J!=^Slas*lvIESypOPtDK>om8Ee6K8A%ib2~H`&DSH1IrfOpiUOZyp#UqqH;}`2> zzWs%L=Zgi34{Wm)t!MVoWeN>+{bHTiBhw)*9-}0uzgXKt)4?pFdW>ebVJ@xFOLR0# zD2mwraxoEtt?3{E9nKOM&h5Whh1*}uqxIw0_6*ciOC`jx-u}W^)Lhu>e2ETk30dre zSTkP+(dh`6PEn?#aONhUG9Bac5jx9^!^qyI%5;3o1p@Ba`ywTPhdLTNr}cC-k@iYj zrYck}F^)=*I%rUtYEbzYZK?)y2L&oq1uCDSaD5{}JzvcudjTC&6*@j547u&U3FZ#- z$}v@>;{w{(b~9nt|0DEvS%}cZ%Z=DlYy_8Lt!TFJ-ozUEm_egu z*(@g7h>%Yh@(HOqEK^fkdHZLxg_@9E9>jCXc+gR-&>k(dg(jPkBT^$;&D4r*%OG@S zu4u_RRPhEf155@czRK{dVCkSF*18bE8Tzkf#)K8`rz_~W9GfbIB!#`=LH-djRAC? zLBoBt;DOBg2)e+a(J8IcGmSNJks-sRP4QCy!x9Q0_!5IJLHMU%%E=^q{K}r7%m~Wp znpC_B?4getG+GCkR0YZ!`h-CPuYESp90Y#KkWX>KP{mSm0C3`|$Iw%oA_Fei%rKFk zZf3Jvdt_#p$UirqeyQxGeZ=e%x##BNUx?cv;=XX{ zG&4qYjx`jKy%#AmV=TfugsVv$Eu}R$vqf}5HdHd*{;4%Iv&AB?#a9k)wa82now~i! zize&HtjH{}2rTiH2AbJJGgB;GWd`39fxY`HGMhw)hP28p0%j&z zJT%GAcJ-0G^DNNIzV9WU*-|jmgr|~wifCaSyq1`KmOf|jU2(4gr`A?lVpa-HK0gX+ zD6-&tiB}aHTQQZGy_V30UlE2R@g0I@77GqmKlZ)nJ^0Y^*2=!;%yiL#uz#=QTl-!!<1GQ>9gFXIuO~`= zVLhhIZcA{}KK?>UYojeQqb+}q%chyy)Yjn4T+t!+YcYTP3wu2*Gb=3vD}62Rmkukq zGBZn|b^B)|Q>z*G9?yQ!=in>-oe zZ$9}CZTob_z8P1~64SS~$ju=^ZVq93ym023+Mu@Zq+;p?shCPmLjmxxLV$-Aw8&cB z1#-2ka%rt+LniZK7uQSmx>2U=;jb9{70jNaLYa71Dp<&sHOGiae>6bp@n*3y^-Mo% zr!~olk$^0xzM#N3Ku~Zd#B8DN?xJ9kSrZf) z0SFAvWT0k?%<&USi~)oPEwRu9;Yy?nWleC*2#(n}j29{p?nEby=mb`?S!63Q%L41M zIARKg4jX$}AeN3pam2I<-U!)AswYISh!V#SVM+zBg6w$u$~eGHGrvF zo_d*9wieFrGS8e0I=0%&>t#Hrj0auz&*j)W9YzVWy^bU?;svho%;$` z=U%8-%pRBxV}daw7(;?o-LdZX57Fxlf?jW+ba1l=hFt^L1BMMNXx}?PcbL>WBbbOI zV)cfgP&z(C_Yr!Mh4ak5@7*>*@3zs6hd}rJUOK=teedF;9MQ@_GCgLlJz%dr^J4W@ z*>}CyWav%!4tZ{>qT5?iuf^b72;TI5?%)YXQHxyz3|3i&IgEbA(7{r@cQ3*Rm#*J1 z@EZXBpjHzHo#_+oK8X@!96GxAew9&x8aMsrLZrf+L^*r{^OF4rsvtme*Fn+unKyvN z!&9x4iGxM;nKS5jpk$&{ETh!H5%-y^>;qTXPuz6A&n&8cfn_5d-0+ZjgCXz+!-WW< z)x@FiBi8p3==&&J-BC{l^pOeOCjqg$N%<_K#2a0KS~)1qn28>hi5V|8QQ%-uH!SM7 z0a3?I#9i3|_Ul}wiAQzc0uC24kxaF4G$z^eeSJt$t@R0sX#w&4t?+7^uUDvW0UEV>qH5vf2JrW+B9dGuf}utAqnbrBVH z9ss;fYDVr`z?1LTgo(E3O>$VVZvhV{WE{)b9{z$2f@uNpA{4ioNUaH4?K&YaEr)x{ zCfAd_bXc{>*R)!sO6)bX*I<@lT2MYI7ya+GYYe@OmM&_#h{SaJRcI_=Fx5WEo@&5U z)U<#D$5;lDwZrIFt_iwnK^a^`ObQDkbh1gcZvjsTdoWJ4JrBiH zMdp_}uIL70Z5`rWUohuwS`L%EWmA!A*KG@bQ(^F?<#5fQ9a1W%wqtV54&ATeq5FBX zAlXH(0dk%p!#7_ua$13q3k(^(_%cPUQ}O^CffpHg5x(bP=M@OL#Gp&C)}xG-=gZZ^ zoPk<%lo>}EUHe(Eg5L=Gm_Z+-=_?SKH`G4bf#8G@zXC?~sR_ocs+C-0-iNciCdJ%VTKbS|%LeYJN zD{VX*vGDYuCUTjjdfDQ2GGV-Ywz|vgM}pZddP9v!yJC{roAD7dWAvsP(G9aS7PtDx zc8oVUJT;r2AG#(b67q2L<`B3_fUEG>gq5BFz)b?&ge3>8VcP_@jZ(ZU#A?E-NC50E z!G=ZM?=rHu2Ee^xk3gH2-xIt~p!={K`dt=S%X&zV(cYDqL|$D6<~Fe{{3`(cE$}p4G$ODEvfNA#deIkf_&5*BQvSAq{)}qEY zUH1)#ONm%}J^tnq;n$hG@l{mvlocU2m`yf-O*SS`xUp7}2D8cru*$~FdLSFjA{)RW z8%vei_n28@16X4t%<$D#gnU*b$Y(X=`m$T$ok{Jpb+BI(cPiEY9m_aYX%ug4Ih)Ls zn&`ZqUExB=?H)mHgT;P(Wj(UzZGw9ac0bP5+`)}^m@;h73;UU}%VtV+F4z@wd?f)P z_Ze~@ZET8Uw-s#-&ddegk5zN~`DV~0$OEvuIhf<@1`wEOG+aK0_LBlxJT~#-F>&ZI z3mXO?Y#3~;JB**N?;Ftj@>B}z*=EEPW7LLEzGZB!(Ek6ww72VVEV;79-tVtCC$C(oe9GaOp--1`B^+9voqg<zCY!Wc7!JYh_1JP)q2pXsvY^5o^beh#h+~%i;IX$s^rH z1Lz^s%CHQR$dEb#2>OD3^#%Cq3uV6&Ub1h##K)SitgTX$?G2i6Z%{EA5>1v1ISD-T zRsjEmCZO>djsF8QB+wO+5VyWb+0DWs?Qu&K|nSpqsernyKrGpT$6U z)+d!7f;U;NLmVoN&}(d1YhYMw>m&$qlo7(N4+wUBfRmDlnRTz6u};T6V5AaG%kHO>T4{^pHUh5p*Kv zBD}Un@3Ievk89H4MtFWOS2xm{VZbILSww}kH2pJztQ+=`w_hmFgae+D`}=){_E8zT z%8w18>QPEo()K>`>B|g(HZ8!EM?&H^&lo*R(Me2*J#>yj!%EFj;?+{U+9fhw+mcjR zzgIxNSJGH)e~BvCO_T?|Ym%oTN>u~#v z$QHa_Bj9yl)9XPv3u2dVud{E*Yn_T@G!Lvl9zS^AHGmh%t4SKqZL#sw&s_s}UaI2R z(W!B&aX}@>&|nM=#?VlqOYir_grPSEyDzB74AFWJPu1QY)<2weL8l8x*o7}?8IIu2})1kvsqzkEbAUrX2ZV`;e-^X zg5X&Oo<(n42PP$axfX7nu$Z5^Hoeo+y?5I9MmIR5&y9 zW@rldh`E9R7a4L9{{)vQSzzOevT_gO?U%aqLoxUM($oL&o9%CQ z{}^~bI{Mg}k_|Y@K|PGMe$9BikWpmDLo*vm3(wrrm_OUTjhCq%cb~*_^ie#A-X|_L zVkM&Kf|!r~K7N+qXBocN7wg4zAsu;ggeQjr{SP6?6M{Sn_m6MgzzYN%C;6JGwkd>c9mgQ0d{pEZuLSsmy{v5&roN5Q%4Ve zwLuX}2lO+BepZHSkyw&&eBAXd29KgQTOy7`=+OD(ey&gM=hkvRp|Sqch5VRM=JzSv zBSP68p_)*uiHsaZqrNtf(zHf`|nmRTaE#n@PjfcP;;F)7&f0_uO5t~G%!d|UDvjlz( zy@yMzjSQ>D-o^FU%kma)SqA_(%Yd^m2#}OpfY3PxjcTt)0ePtkq4NwnkD#Ug>cASi zz_3v@Um=sV^!ArP(?FI$#93sVu)weVZz(ZcAlcKDU6mo^V}^`MvImiztCgCi*6=3` z9=uwu-CBP(ezMxdW!Rfn>8&*|6DT_R=>$=L-v&cyCQ#t8Xfg{6MrP;Hx)%!e?6c{Uy1csF=Kp0`QI2DSa{Z=ID(Yc7svvz6A)AiZh zGA$t0Lb%4+2O;>d&A^9kk~Y*ud_59|wQB+I(*$`<39xg-##oe4Gd8* z(@?Be!n)&!ttJC+HF1Di#C9YA{=Cj6jcM6`bZ<)eM)Sme;+&I1gsr41FTeCI|I07$-i$h^1>P^?o0rEw2m8CJSbMV| z^o9y&sq2>k!E?rd>Obq{VYPkh^|UDrY#FkIAd5nEr{VUUEpUzi=U`S+5*Ueq^8`4L zOF+oiDUL%$*adP5GwjV_Rk% zeYI=Y_tNLv-%Ns|({yev+1gPVuf88z_AT;C+^L8ROAWwxD-6Di9xX=NVlJ$~`y&S4 zM~l^U8gIW?iK#XIpvLeAH539HyiQ4R7zlqsyZ!=o{YFgF=|b7USITDu-n4+ji7T-Z z@l@F3w<$i@ht8>Lfa z4?m{xV+1d>9&UnA+K)=J!X{M{y=7_IBcF}B1lhFg`@3nh{UWjjZ_~-tMw6*6ytdRf zJP|W{h7M)uAcoaMY_y;QYl0>vKrfoxBq=+DrbfCA_eg0Zr`iyO(&V!51WBx}RVW{G z8P5XGi5chG{?G!PjW|fG<8%OUBWAiIRou5hqVy5PZMP zj8q>T7WG*!V~E{k%bRK0$Chbq|J_u~eI(|78d-m~Ple_qq4|#yoN?k{EQV!frH3e= z@K8)6>y8quee6`blGc-PM2+(Z8RyYdY{<3#K6?GA(uWr+A6n2xX*GfOc>Ov8 z?@;g#EhT7+UOOESbknl$ONe#ib%R^)G}cy4M%FZqI-_aSF^#G0(~$E8tzHj#aNX|@ zEg%U9etZa_eG2WLlb-T}Yq_)~sMA?gN3*D&hOca)8#qP6hZe9W4z0W8?3{hIX#r0@ zZ%NK;1l~Di-`znGm>n5L0jqxo@$obM4=t#m1$kVK!8;TjW>uY$Y}XKcK)-wd^T9@` zwTQZvD#11_AZRX;dT07ws++1@C$XF+-jDh!PCQcvWIfq@Y)#)}Gs?7p`?p)K8Pn%6 zkfGma4E;W@-DeH&pE7v=6dhKkb8Sj*WEfh{Eg#>1e`vw?HBQFJZ?MU0T6irsXN!G) z%0}C?;9qjU0(4G)&pG-%hUT!J(`VW-EsVzwy>av$pHM$G{ID*0oxE=g1Ad)Iy>sZF0%JfyZgQ$ZYvdwBF7lZ= zmgaT8{ScHGM~U~3E5|Y8IOZ;&E5`}rIKhdjm5B!N`avY4HOV!Ocrr|R=x*rSVy_(; z=|!EKydf5*ITQj{6*msZOmeVTgPKm)%e9EfQ)o!Q1cw6QOd^-Lec$I9GzwKs6BVhI zO4lFkohS7;r0YA zY~|S$%cCeuxe~FuwjQZ6*-h?RK&C`;Zt}ez{ysX6vH8$~YLu=;^mq$QGb(+5e*OuQ zPnrBDnMhJ4#?xvuo>m*>rzN#~zP~nnAHIFc;M*u?Z#W@ar|&Il--quG8GLt$u9yv)8<`$4>$?=1YEFwh|wk~tIwjNo||5a4L=4=vz{!&1&7w%&Z_4*dxO9pn)j zWbKwsopewsvr#ZDY)9eHN2Jj6hZa=mSgJ|%*M zbFOi!Gh(tU)Ff44Lc(u<2VXexuNYDq6rOKXF-xCP1|%6%k})E=k#s1Kv@12XJXcWq z?n*!|4X|md^yr-Jb;xewu_SOzm(a{UOSRl1ET)C9^kP#tUW86OKIQTGuX)lTgEV7G zGk%KZNe>edHJRS4s9bGTM%rj4-@Gqza51Z6IzVGSK+~z5xeif{4p9va(Kj1@-)c5*pJ2u1U-uhAz3thBaS|2!m&Yj%OW}E$jCdwPu43!L(%lEgt6u-;P z?w(#qGKj^eEIwsev0g*hx;m|0sTVnSZJ5mVN!v3@fxsFrrDEV>OX z#ogL~QVlrO)4hhg{VrW8yJ)5S_J+dM`|6HrjAvA1shew4dF66$)*X#0$(WIh<=vFz zw5ATOeFXCx3&&9NWoV~A8d8J|pL+H2JZ1qu< zfNuj=Ts=v@J)%4#l*}ni9h@(}M~56mfBPz4t2y^mf%D+gV_zQ~`|9scMC7&7nVwyV z!(!`GkK0Ecx85ztxy=?Ohp1?$q3fLQQ@LH+4XGf9NRUGjhve-OUN(m1bBceC@Neaj z`5%V~c250;bDYady9wTpz6myY{Ktm{3+QJ z2NZ~^!;WxG3#4LIV8~p!^x5iYTAuhZzMQ0UexJMdV_GEJ>8=4>G=*xqkZY21?AwUt z@8Q=^34Z+)Ww;6FGC9&Ti4zIVqqA!O2S%Zu$rJLH380^~2>KaHoOCB<-kDgf(#QJl zEwNs#WW4`Uu6*|2wm!D~f>xh=L936dvTr7VNg=pgBx$r<;4EK=rq9Nze+nO|T#1P- zv|lCAeidfzpDk>Whin3))HfdrbcUO6!*zlf!Ih`~PLVl-R7b0Y#osfEcJ&dL?exOLcUuR@yUgp(!$&B1)_|2|_3IV}^bV(Nh<2 z5~0_I5qb}Aw!fa;xuCNl&cMXn5C4;S_~-QbY=AScK>D4!mBG{npN(t=jBL;K1BnXAe=V#gAqQVD#S50Nv$Y#OFKK!4Kh9S$wHCsgJxAOjH ziCV^Y8z&MrzS(1xdMaMYhyU5wpOv^vo%$-r#u*ifIF~`Vbk$jojdd=Iys7Cz%_6BB z4XJ;0I>d2DyA}{Rl`O(w5x@*ceSaUnN@53gEg*fUjJ;auX?F@|^FbVSoHFP`3&`Po zCv2e`r(FVVTEK&}NlbRP0Q`)kB-yp>KS>MXbgjMel|6o!;-mNYCf3{w&iMF_=L-}0B2G}-bJcCC0l~-DI@5jbZlKQ2~&IU`XvLeUy>7V z&t;Cje95N2Y1u!Q6ya+bZmi)4r);DLC}HM6Z&X4D@eW>4_!kiVl|8oK9ujQR0uB$t zS2D2$AJR!PM3ZKiPK5QG8Mc^ZFf9N()Sil^T^O<#Q$w6#{6?E(d~`Yl&{+nZMT-f! zZ9%{}28?O|&{rD>InR*u1UcIN(jL9Q(9zQj6KWJHPqRc*`#vu+iXys(lq>d{0b!RI zHh6xu{anZ7m#W56wi0o>a9>T4j4^ zCdv%lHpHd?^c-i=hwx0A=tY%*w;st1lP210niO^--o9H|qcdruS5tnFY`>b2`!)p5 zM2WVsWBHhPM|mF%fHetIXQqTmLg5Vpho8U%iZ;t*xeiiWMxhp*u={1TEKIuxgd8D2)sbSQ60X9NYp}20_%^@JZJbB^!B`E9$3tAC07VfKR<&$ zzolYj-8I<^n=)B=co&4hGEhr<_&kNrqg;_97NjUAhCin8#~6OP(6)}G5Wy%iD#pvkV(ZcKbI$u$rXP?N`?DIR>ADscBd2mD*~^E`?GqHQ`ly?jq?HHYWq(BAP0PMI*J)(GZoDj$ z${f1}SeZlIe*NV;e7gFwypP{%5d2mH)`4g(#BJ=|3Qx=yUUeasi~IMnJzd8qbRCrG zVprM1RJpLlAF%F1^(f1+HxQf6{hP=Z-eugi>OURx9t} zA4KTTP3Nqe&da#^hgdoc@+Cums`!Wa?_(jm4mMREBqwy9StT1OX3v0JzS8 z>u3(mgFpPt8hXH>Q3jA0hl(L!))y!PU^-1>{mr5yR$kynNE6Kg_U^`y6kXEUaP5>%)gv5u^8;Md)MJCu7hjxx7|LIbCY8j_<%yTqGKO@Vi&rk>hBg>;wTMxhgbGQnOtAJwBuf^=}0V^`9BL7*3F_joo2~LgGK|OcONYLrD z{vkIa`T-t5a>7VXP)I_UPDJcXl3@lRgI~6!k+^XuVU_{iM4Bi`dH>Vk02*c$(52+V z&(oEd$t!mjW)5&%+)BlV^cXV(=#uhO9Wd@p`x&VHtKj~phs-V}dvs5E8Hz+s4*4xi z@+e-eW5z@NkZA!VO!6oUW*{EkIukItqZ`+O$$}**A3uqZxs~;Nk zqgDs7Fr}j?sD5z&SBG#goug2wek|`a+(EyENgc&QedsZD)R9T7_s2izv_M9t{p`sT zjCP9)B*YI*S+nwmgFt;~BI7PrO|0qIC~r^J&-lL%?S0q8P%1$Z(F5T>~n3uvnUh z5$0CqkXdnBW0TLc?ESRUAe^gM%?|rzgA2wrxBy)7=Fl!=lkjweZUQw=A`g>prAC-4 zHB=Qfm@a}3Kj{Fj1|@6kvpv&n;YLj4VO>~hj40U%{j_xX=mKp#ar0Q7DB5hESTYbJ zCB%F;dg>qAl~txO6yQ`9^Wac<;T-6@hW$&DXr8VY!J%`pNo`^i*~DhNOp}A=Msv)% z&$R3l-H6C|69+K0aQ?~$*QIn_K$phz16Q(EgAP`s3=2Y-(JZ8!Ujtl0--@;W%o(;;ODKD6u+b;R71I6sVvIrp6! zuBou`YT+R>ufrtU>A|DynYuY}j(4bTJ4m-3wO$@*#5=un%5;u?)p~hj|1(C+D0D#5 z-DjQD1P~nbGrCknU6gjI8$`jMx995C*-z_&puA^Yb#wUczl(E}e`wj~l6Q;X&tMy~ zxz`0D4(6AswVcSqtZc8t$a)~?>sjxxwW&8?Cc?DrvuLP^_B;>dYE!N@;tCH`ZokH6 zU>}u_=m+u2d2Q-<*~~L7dq1WhM0deq0`3o*jA_t>7YMtgdyc=yXFjWG+5bHUo7Dj| z-~(j92ioyEl!^K&1K_7T8*Iegfi~7>L^vt<;!jq-{awwOSf+`*vftSF zNDk$pzqLcTI*4l};sc2WlxTp6VsYq9@tG0yYb;dCu=Lh@=V~BLwYg%||FZpHQfJBs z-qbUHrNRR(i{GceyI+G(nwQB@KS+YutO3@)XGqln5==ky6LAxp^rRnsYs0L6&zSz6 zF<8ueJ&vzh;Hl)*Gh^E6(Plv;J*bUSWl-?U|ZhpXE4AUEX%RjsrQF3r9Gu))B1C7OqoraFU z#r0^mO5^0v)8tW`^{8pt`%$Ctqs_s$0pmK`jhmJ|uCWT|>fkemahq8U)3Q$%ikVy; zxPKV8=?`n;AExF9?u*7fwo;El*&R(4t0*||aPXqcwzC(gB+7-!|EiAu{9Ww@^+qm` zH*z6jwGPE$bMm4#W?Z!~;0kWyV6$WT=I0&2dRb;SR+r!*`m;+h5u2rOUZ&4k!2LN4 zxUa}WkJKx#CFj~@g`;&2D6)(qOG_$SW9Jxlj#mt{2G28a5Lx%3u8=i$fngVTg)b{` zzf1;o*8uZX$j$wmufO~ogzuLDewN|GN)pSZSa^C}?W9W)$Jz)itch}rC=ym$>FnYC%oE=wb8#Do6GjMvCC~2P+>vxH2pd0# z*foHJ@mH}6Hd8iw1))3Y)ad%1eZORwveEMgjejO?Us+=_d4cx`&$Pbo-^X@S^OQts z?HXLt)P4J|^!wNihTTA6OUh)swFe(EaFkm?T+2jTwqX#*GEwoy`%yzhQjlAB#8)2? zzWNAeADfAADkA&N!f(cR4W!n+Ep(?vpgS$N=*Ua1;e$_*fC+RR4{Nta*k^2*VnB47*ZEY72eNpwDYg-q1dFg*hx0?65@Zz!T}G zUSCEqUE8;n!@tjvWei!uaH?b&;2g1#Ajb%DgrJrf25bC0!$;0Z>S<$P4P9W+1@h?w zwGj@VUS#-16!0tquPyG#n|O#{V)!M3?+a=2WtO$htVxa;$uWBMEK+YQLQ*6d;-4^l zlq@p{y|RpLv57}gfgXv$?yV;F-JfCNg*jrydMal2*i5#_AE}38Dy=O-h6xtE!K}xR z<%!JP?}&x{=QGiwCz#*k=*MCmO6$3kVFE@EG26nc%6V%48<=|0`^#E152g0W2l6Jc zpPo#=`0Ztsc&F*7iM_NkOuFdV^2R9Bk3EOuV>m;G`dtGkU?<^3CNX0OBsZ=kAxzsC5{C9OFh`j+0B zqRS|{kir~(xRdyd#7A$FqlfnscaC$$0Ydiby06?R@Uwtj13pjt`pdu39P*VlLyIxA z*pFzX@hpACPIVB+CF8gxr!IFiKQh8^l?i?eg$RrF+NAxp25&HMn(H}!`22w!Y9a!r zht?B%a2|se6GV#(!kMIdN=UjWyKv-n@85^!EW3Oci`Uj9Jx0=dG8;BOKt&`2MlwK$ zoQv2KyPsoCz>G)t{OPWmYy`W?05k(O>;oSksA}+c@gtG`o0YJ(RDP2X`At|dKGIgo z4!cH@R__}2u@g02E>yT$d*SvU@zkvWjtb+bphRf@XwH8oDoKLKjpwvVcuuSEM(-dJ z_FKSD8C_p%b10nP*-rq!z~BC67MOVQP;dU+engG107qo|f0*HzZ-0}x)4U)wAbIK8 z_BYXT`e$p2cY+@g_z~%Vm&myO;MwSyso~MEY36=NLQ-^YP3L zlr?;w!RJXZj@#E47(9#harpWM^UNC1Gb^vB+F6q1@U*dy-e?o_Mw=bqHG(i8SC)VP z2XF5h_RsHPX+pP6)zwNGEi6fzN6(22b}jq8bTv~q4{uGfG)&P%6j@3EE1ZXkjHc2a zK1bnm&<&W`L+2?pOclA1(Um=Tfr1xc9!hl+$*DuP7b!lvUQ{>1)E>J;vC-8cDHo2R zk0~^|P>h2+b>q}6Clvn#;y?Uel1m8$Pp%Z3xDfDAKD>32QiiG(*PRTn9KM{&6x}IS zf_SOX^*?%by&js}iNVUAi)t2KG8!(Ysh^>`g>@(mys`(Ux<$8&_y4=bdU&gD94uOf zDi>WXY6d+B`Y%&C`Qfe8Fi;tz8%9HfbgahWhUW0c_*B>E&as+a zhk?BqvQ*3Hrg3Mpg?A&?W=82Tb{GVzYLvrOGwDUn$$V$2u2ITXO<;b>K+NXKVJXc~ zdFP%J68We)dj_iST!u3A1C#02VX??jW#>>RP^{K^UO8B)9948Khcds5(Czc7qH{pe zjaY9lU6zO(m2|Fv;IEC#bktIJE?!xV>N$ z^Pi`}&6gm$bTP|$s@^i%7MpUjOW|sc4HxwCU3A1iDCNTRcGz>aHH%>Lt*=Yf4nO zrRO-K=0w@g&JtB_DT9jC&TiDqzUxa=w&HlOdw43`600cYqTLeJZt0l7ua|-M zSS-t7(^jGyE+Gx8I~hB8n#WYZ$IoFYW#bF_YU1Kj9a9}2BORwM&{V|7DC$8hL;JP& zm`eBWN7puA47P8>Rj%J&$^w>L8nP3#X7j;VT& zfqHxKN{tewzLsAf-|!@_yOe#d-5wR(b{qRAVF zRqKR`^&|_clxl07#EHYr#0iz`39RZly9q*`4J7whd<5 zr_Z<$3~(cFqC^ zKOys+$u)zjbsKbt&dJcIstk&%(XLQOn#r|7_vr7T&Q`K0(QgSMvU+*5EYcDuY6<{J<%UE<;1EGAQKA*Sk80bE*st zy2_xSD?d~>L3HI9{E?y2Rv8p+wIey?t}jF5t}-a@>Kmd!8;t16VfU4xAy^p{g5@V7 zcA2a+{3?UOul$=Jk=|Jv%PV8SAAWxSQ(s%Ag>uNBFBCjsklI8i4W1J2N(t4uaWDBS!28g`XIVOI}7Uztt!V+7e*5I(#muV*0>RW=I2dSpr> zWrWukOMA)DNURKs#CrI-c4rcY>%0sN#>!-&q!ad}1cFmRqxh?#3Rlu$x>7l#*sEbY zF+QdJx6`Pr42rrM#=#_VL8nqi5m&?b;rA|n2931Jph&A>8b{JRm2mics$&#tH8!tF zTsnt}7)4rXbh$JMa~yvH)h~*-8q1l(CV_@oWl)&aSYA8ac4lapRR)DwjpgIHx;;9T zEQ+-ntC@>8L?f&+D8g#2t{eh9G z>Qc;AyxWKSF57S#V3k1uR+r)?b}%|Lz$$|RtS)1xLl7DOl|cbe55LnE=T48WL)TDM zp%|!#-^thkn`$Z#GY>|=OfD09c&aKC0rkyR>2dO?B((n-G#V;{qM=ShwX`2!8V!{} z(NL!*xt?;lZOhP*s0<2;s)%T*9BeWTipmtoDVRw13=NOUpztV{==%%}j>@3msD`+j zxg5pO;HV4=j%t|j%OgLOZbwfvNGgMZq#A0b>|-G^G(swaBBUC6WlTeRaH?DsA$1nq znW{$09-itJMMynHr_LO@ohlbaNSPRmz@cZTa#3hhE0zzx4_uC-X=9jVhGsSFH~lFLxsJpA7Lkm@d^W96CGc=o@CMond4)RZ)j zF8hIlCr1OPGAMAWcO9z4$y(6RsZ0r9;}O zyc_SEixs)>=8w@y_UErOyee}HkCKDcwJEiEG>#K>HF5ZXRPHFyYA75cR5LWhDuY6- zhViZ{pEnk3dkQLe6l67w?;K8GX^2$@g;+hF(;1zIF4vAU(kg=@t;RvDy%77;ZyIQo zL4j7|*c3Fjcm8OgRR#rGJ^W6D4p*f#(kg=@tsZ`-g>CtSpSjGB@nO(OAu0cBNSwj+I5>SbiMLcK1Wh?o*b= zWo1!ZmY*2EK-i<_DSDncH^W$&8|6RC`N+~Ztt?Ck_vrNu)k@fh7-wmuR`z+3-ej9t zHrJ!AEDhGmKF9eut{kwBDfTgZA$Fwa*WBe1T9!s^Wl_Y|KyQS*FKLrZ<872_{E7O_etRFeOOU&6)te#oIm14GDrc+%gHH&2w+hY(ntl5@Ew$*C z<3M;ndJzXfqHd#)?U*_&KX3<^UYx#8^}0t+kU9W8%h1ui=|Tp2gODl*{2ar_Y1qU( z62xCe=y`??9K;twJBSY01%?fLz!%cwF-xQae38M!q^R*+c^xsIOBSGqC`yc?1WU&! z+n;+gsU4@tGmo(&wO^0vyS9(5zv7sE1u5tt*DZl=|)nYtR`Zr(#U=)4jT*~)H@k2 zTzVBiA;P9h5t@C5_DK)@SY+JZIqN0Q_9NR%#zx*S)iycVInl_Y0A zfInv+e_lqFn`u129<-kZnp?_yM@xHE=dh| zfK2=!ANoI|>+2x)x{@3)0r=4cfgb@se-z1NV~cz~AjszfB@Tss#vd&4d60sCDx1A5c>jP4;c2oxACkh;*aj% zJhO$qV9*!nnrst$;?BDix3TTFH_C*d0rXl9y|2IgC$Sbw66l2pSjVH&ON}m951(0M zw;6UDV(V38VvXHl*d2_$x_@KcDa|SYZ&p#gqEK3YcAJf78;@uE{>^M5U*Esq*bnK; z5#e|_f*dc?NNgTHm$5y?fc=aC`Wdk|5U*c5ptJ5DfbJjMzklU9S_GfhtD70ukAkn@~DBGv&V-08;6BjEU;U4xewhQ5$N^^ zW%i$_)x;W`9Q<99gTLsG9TEX-pLGd7?w0EyybUbjht;D~`02X_B&k#J@cGwYzPo?( z#~=R;aSW?~qs}<$=vqy~>(yj;G<5$av}Wls7T`mjKD>{Y5ASWUE2jjza*77w{!M)U z-aEa2^IFEMdutYa?zC&zH_p-h`fkKo z<^B6un;}bRy#DSQ;2L>#C1S6wmr87l5J!tC1IFCkzlr3^7P?&_(CrEqU8*&qeJs4t zKn6C%Z7{^`hxhZv!~5ufF>bRlZV#T~ai6mtHX6{x>d}}zytg(T9X1}F3YWL;tn%$z zn?q|BChtxS{)ElLd#`o>elj^s5}R{Sk$!3m?GtEt;&dY-d^fa@+&IF| z#&!)LjZPZPz2PFb_Btj8q$`#S743&vpNeR{R7>1*4e${e_7Nxxgjl7C*N?>#$45be zEF(aLbX1fq#3aR$ZV*1l;88{wku1`c?nP1$9unjk0ZQt!h`$*c&zD|#lP3E2Y=kvg zfsvv2;*S$TMRRNPB14BI?XI=E<}eT?23|rT2Kw8b(2p7VF}&=MdK40C@Dm1pf@3dS zCTNza9A*Bln2F=kFIgtvEG*KoahNSxCfqE1-y>X=n`O#H8NY|I{cvWPVo|1V{o&Jv z3ZnU1C~E?y(k#rxqzR+rQ%+IlzkJ4k0 zCf{~^;<6y-*z8B?u`k40C3=Ld!P(45`LT_i--v6yq+7$Yd5QnPT3q(%Y^6eJvb8g17Fl7Ai@g3KwI#_j0i$%;EhArT9;|;to(UJF%O1@% zx|6$>XVOLKvfC=K2WHAe8L}U{zNM$9H9Au+N|F7z0Wf~Za$#@N@=UgQSU^g8HEXkz zXQD+ZxLYFBhTeXDPZ(GuGnJx@+g*og!X%24ZTHQV)!N@HFomK#+aG4&z+nb5eHLJDu)u2=+w0pK zfFe^+k^8H@xV7*5BGV8`fjvsh=GAG*D>5CSEZ8lhQDNMU{=(j|EHXKvT-ZZly$>le zA)zGLeyaOEaadG~OiU;Vc7Guglc6HC-$fOfsESZjQtt!pdz`5X<-j&WIW@a)>p59s zlfDE?hwjkrH?So(K<}gMy@C>(`Y2iS z?%>sTa%@U$_M?2!9|*MRTrOgJr?SKpP%1t#OC5%RsQ~4SemrSO#kF?4rQYwi9K5Jw zCWqrE)__Mdy;=!}qp4%23zT!Y6^n^=PaQKsplr*=f1O8j>%*2~rUsNzS?@HL_AARV zlff}u0d!xxEsn=b1t^j7K)dz!KISo-{KxRDBGn78y?RgBj68wKzm0A~>+w8c)9?gd z`Xu)2$_bl>C_C^lcJSy=*c^lv*cZZFyo9L(KAVWJ1iOZJn1m;65}v>#Glzj>Q}6^H z6l?V?+Gls!x(oB`*GRnT8n;yw^3*N;Pq1iUI$@rt%olba-FD1 zdZ%Khjr*GE6fm`hgsC+|hh-uO?XhtP@2+7FJ5md|5ep@~L`?$eU8So<8gIX_CG**M zfPhUofibW^8!X=r?14qy2Ek$u3ZBH*IqzkJqvp^ z0^S1=g1zm#qe{A72VQwK2~h69UNF_|@?%^2F&mX}6BR;zod)_ZJMhjCo=xV@!IM#( z5n$Nahdm{*r^wudEC2@pR|#+xJ%4)mOwH3fTj(}{ZlmYQ6Z}FP;dTfvjv3q7!ukXj zxHIL4&*ox6Jf%J8E(@dT!Z4~~8sU6$!!AJ1GUR=a`dRyx~?}8XV zFh$ro)+aD5V*BpOy0>$zOHfS2SrE-!hBC*x1cgPk1FsdlvW8}p5XD6JGL)nU5yH;1 z-p<2gP8qvQlRWG2JUrId>nF>V3ej1Q=V2hqOz(|UdM9VL_yyKA=;F{ z4LjO7~* zHF}RE-q|(mxe_1MQtU?b0C1K810SU!J`&eLT4U!JHoA$CDNdjQk@E~0)$dc$T=(~N zYK>lC==*1W&&H;5V`|qW12&EWoY#NG7PJ9C&NAdILcagiMy$w+hR|~i9aVgJwNZ~n zUlwwWQ|>N-Ai0tDQ{{v~gP5uEM$bfRX!7i40H59Ht@iz|mcd$D&%g{5C)x>~$}pJX0>%)V z$rHV$IhE0T`Yx|4XTBkXtQ*?gS&<^>=8ks2+ zonJNA8Y{6j={Y7*bbQqb;vEG|?NLme4)2QjxVwkz>+C?{`{aV@}z$TfeU_nyvtQ)$OquZE zj>dPhiFu|?c-pC^!sXMMI^kibitINjd8W-gJZJjXL@XS@nL6>aolqnW*i4(~!A?g+ z(%uElGi~ArJ0>DjL?+Pj!P>FNGnJxuJDta>ZPw6CqUgm=C%ykHwC@v` zn&_EMH@I8l(>vDL}pSf;LHrl+K(+TC8D=EzPu6Ei+q6zu>gcP43;vU z3Hx17k*N_q`5CIEutj4kx3D~^BJbm)M5QCGBu(HJ`evOqVWD# zY5dAsI7KE(^w4MY@R_ha6Du-FqDMYs`^{^S=@C8dxlng=he^T&iQe{Hs%V~uGlvdg zqAa3Q+2vX<>(-m7B9mqD1lEPRd^%HR2`W>6dt^UXN=%mM5z?vN5<8r;mzXfo`=fHqUp*siCQkJ3sHy!Ohoig_ zQzv?JbRj}g(i%cDX_k)h6rXJW)qX6QHcM!CDIL!5kC`&j>!{vZPj0!fmd-JgCc3My zZvXXeY6(3&Di3fR=&l9KK^6%;+{doqwC^8UP^yx7Dk9k~_T-fBf;^ z4#h;gPGu52{(X4OBF6z^wbLbxrUe|52akot$APCWI>^RI<{U-buU0+<%?_)%(yZ|x1$ ztv$u?1)~_gKtDr9iT6UejJW@zM<`4SEUED#&?^|Jvj{10+1L*)h+-ZGPgInG9L+xC zX!dahJyO~XqMx-H`dJ%y(wR0UM{RMVK_w|jkC=wNQ^I6gz}9CX=GFr-9I;L^Eog@# zuJwP9`clMGOP2A`3tU6xL(6`7dd=`EP4Gr&ybs@P4hg(zK>jkQZ zyG&mN33*ur@!J%?jqyVfgd{x)A|TnTcP%JENxBMzJ$jv@*9p3q+ry8V41R zW?W{-rUmRmWs+W7j_NseHO{H45zQAV?v`@^f6-*{7fqbOWh3puUsCXwe=c~#XW*t~ z|GIE(>eZ+A=$91z^3O*fu|YL0`{=0%B}uXd(3@?B-b9byE@UXIv0J3N!>(n2rx677 z88$-4l~X>nfK8HGtfdQnv&-t_HW>P{`}R`s@2O_VuO(IsJ2cwf37Y7#9Sq ze^O`YpVT3`w(s`v7bDi~7sye)NTX0%cKam@0l&l{;IT=c0HH4dbe2J9(Tma8)4NVz zgo0!ZLJT>^0FE@PYNl59AD?H?;6Ssf_sny9=mLW-pmk}^UZ^AJB7=ta@$029g}1zZ zu$tI&lo&_pDUMMzPp|Arju{C$;FNQhkv(D1qAPZ21n@~%_$98!kglBvAj2e! z0>eKLxb=R9DHnx@w`DYw*2qk;8TcyBe&%PGUQuXxXA*4ek(pdkY&n2=1cC{DbS+`9BSlPQW3@7#*$&K{b{6vc=4Z&?y0fShFlMd9It zba`w2%~_^QSVSScN$ssxmPr!Dh5HK`ZG^*;lx4Ea=5WmoIkU%R%0!XjLoreI(=W@U ziQ>YCP94GoiK4<0mFiqy zsMRKRr@_m~t^p(uS&H~t9Pn$f@!vH7nrjuyJ9m;IBSC={HzHC;FJcibYvk!H!Ev4HoH(9di&;(fypz?!(j2vQib9WngY{2ioov8f&&=X z$e?)Xz6!OMZ~zG#5EO(wkT+3qz^YPUD;J9G_0yF*Ly?UJjKU4#NL|bD02($VC}_5& zx0VN)#Ue8;D8RL~2;#LmU{H!oCn##PwO9q=L7q{GjSLFJtmq%J6bFznU!s)9B#~Sk zGBPFR=9Nl}=f>S{sUKzVY43*?SYb!+neeNZ(Ra0C|AZk+8AuTcNBkVcM_Q2+HFtu~ zQ+SkWF1-!#IShj@P9Nkf+*$WZlWpn5lHPZuh_b@~A%%22{r1j}2eUvNzE(e^K0If0*0aP+EvBB<|! z(5dg1MZQ~Kgds_k!0@SRq4%a@mAG^cb=|TVm|`_nbms!$scezw))JGzysMNmCl0Dz zc!{sQ&$-h8Q}H6-&6lx?mQJ5f)r)+$@+ys;3@7#6ve~i+bo?q!Bg#G zr)?R`0vCMhvSqQ$w!C#2TI#W7vl;AihJn+U*Ta>{UDLk5)+UKV(GlVZu_!0`$jh*`an1UbUu++rq+fOJw+Rmws3pm})3n>#1hZ?yW1XaqcXJP9=*rZ{1t9ahmZ| zuxRtvOVXuFzthcI4sG5p)oS9>?{wpqLmRi=il1sibSl>zTw#~D+Ss_@Q^BI0Tg$v+ zaT!>;bIakK+eGSxFm;)jbmNwT@u&uE?(_?&XmjwP#Y`Uc^m6F59@33l4sG0eW;Jy1 z1@m;{mdCeo(M(R#Idx{w5k;0#;2XI`5N)IjL5>oDdD>8t-giUvJVnpLy90i41K|r4 zzJTFl#qf$w%!SiR zMt6C6{62S?uG~~iH+p%r(bN6!Xol!kFQ0=V-aSH}xagSf_wqUThJGpMPQQRk7(U^X z%QT(1ER=M^mxmiZapy9;bi0>F+r5@d#9S}*ZX-+;jP`qjTl0RyWgVyxc#yQWqH$t@ z9~{>Ak!u7Qx2~PT-4#z!CqL z^%6=;eyZ_fSAskv$fK?Mt-*GsV8d_>hT%-aH*)RDz((O1x}VW}B_eMS1g~rfP6~wJ zqyU3ESDRS4$iY7D79=(NzHa z1yj}wpsW{q1W4i|gk5K|r(Q;RepEcS@09w8VUJKm47tMuz>OvWZZuKd)LS8?Ep(Ih z2g*3xRJYch(X29HP^6(b6ANn*HQDrNg6Yu=Lm4S)Z5(j|;a$T%cr=s3-g32=;cfZw zyw1U#JY1jY$|rD zFm_=>og@U#EMcdHz-44@20KjkohEYSrwhI7j;z7iJV(`FI#RDhYn9lKDO*H4J%qkq zt2DMYS)D!u@AJ){sbynk<&&EoM8-7R@$oM)e_dbO{}|>GbcQJ0E=nAf)n(p5w?aC~ ztC(vz=|I7x)vYp$s#rSAq%MjN`63je55lOK2v}dAXua_pFd3@x$xR2Q4I|7UEC=m-= z;4#zNxQz0U>P@DD*2YW=W1xl6U-*CF?HF8pXUuwM40>lA#4GKc*kYfv4mvNR175x6 zM)Snp-kq~9ItN{JF4t2Pt))FWTNcm9_=>=D9pZDgI-Y~o@qD9Jl0+2mVPCMb#S3z_ znCM*vo-!Bv*yL>Pf}G7oL8!D5vPN$+^ftPwieu%qMNFaz5Ppy0lcPU2q5!)(BG^@M zHdIYxaTi&C_lqjQeo=KhFGJWq!}c+@{cyI~WP!&D;zoq@{Wd`FFm!yZ5YAM@4ssDX zn<{XFs|{mtBWHm%e4l+h^w-43B&*qeWs5%O67&J^69(Hal-CXBvGvCfSlj=4*!fHpONaHYQ24Djm|pT@4@^fQrO#-A?uT2 z8D;XHrW$ND+5XxZf5`Ar`KLILQ6jvJH?+1fmq)nh*{)%Kv5RK%iZ~gU``D)=f_*wd zaW|i`2wnjFqD-J)fS!M$*SJ6a`0v;i8h}4*68N(wO2>3}CqA`>?$!x(w~mtrMC{NK zfuA$*a~OG`-c7Rv~4k3yxtr+js1^ z0U>t?atB?u#w=1EA$JLK7w*@^++Ha?g4{z@=Yt#J)z%s>vHtQt!SAEWAaRn;(_n24 zJ|N%&d`B#uj-@kio&l zWg1HB&NwHy=O_&C+GNLzrc$iE7uxp4BDA-EpAaH&`7_oVQhWJa5a0_KNSFw(ywV*X zTaW1_!AFs(D`6s0EkC`>9Of=`nA<{nmBscK@Zrt43(&I+9Tj}t{@nOd(i%L+z|q_C z?a#eBt7H$+^9&u;Xc5Xg{ran4IlvbfJaUx(?QP_>g*A4OVFMTWV*7tb-s$$OeYckw zI(PvxOg9GqW$M!NCyW54g#7xex6#})J!FfYVe%Zt(a;?XF>ed>4A=IN~#lgC5il7ear3WBY6C$)9B=2fd~3 z&KF<*_OJ9N-5Qey!@j)+X zhr0XU3valP+DaTXG~fSk&BM$PdQ$tXigCa?#AhmlZ)#QK4O4q;X5r9t+Hb#ozWvltJ}OLI)aFniKs`;})_F>7?DVf2(%fB9Gc zY3a4i|5_6;8N=ta+JZisxPP%WaXBVp^p>{0{l)6B|Nqv012;OSPLM}#Dz+n?gZf2KY1pxJo3tw{(So-EYGGt zs-SwRm#~4>uJDe$@xwc_7;JxT?f&PP5b`MNU*zVX=a~xf1wP8gU%9qsU@}0}Ux)JR ztbKJ*8^HG`TC})(fda*gyB29FQd(Ss6nA$*ahKBKP7B4MSn%Q$iWj%wP(tt!2qBm6 z-Oc>wW^V3gZtjmHo4nm@cJtob-M8<3-d3*{lHl!=pq!G|4XO1wZwZ1*qMN)7y4!xn z2?;-=N+)Ac1ZqwJ$Ydxnc}--KZ{9M?(j{P-*%BTJIv`G}93QR75C@9d zJy|Og#=D|!)R)9tPXDNKx(RXPQU3p0TWKBK7zy8+|M+|cMOZ9GU}YMX4!Qc?Y(1Ks zv7gGuOUK99j?4fa))axrwZ3B0_vnBnl_6?@h}1?Xiv2w;uk!kg;1A!^6885>8IZ3` z-B;}S@@m2%3@F(2>U9?=UQHq-QTWBl_(H$H&yJ`>dPNzKedYCgmp}=RL~d82twP13 zzsn+Da`uJf9f*?d=odx%hU~e=F}+bs!KcWYG1;$zeGw}t{VXU}=Nf7${62g6T4=xI zSFh#L~AKJ@;1OU9z>VXSBp4JJ3wrYWEEdDC0_o6>UtpLNLwk119y998LLcPg0eK&EMlKTSyBLT;Diul4^k%U)SV5Bh9-?+b{X0(w=#_HwuRk} zGlUx3gDY@xGJ98hoAJ&HMb0;0@Ha~~p&k8{e|AF{l(J|8`TQPjSZTGamN3c=4|cd3 zV^~rMuJO$!Qr$h(+QY0+KZtij{GXy&;3jt*0|)e z^+kBjLUU7*oW{SSn8Xk4Zhb(A=o40p^@-Nk?4`B6s51W#v9SS582^5|dj7aod&*`+ zFA|@NKqE-h%lZ6*3PVUnEQfDBYG-vPaF8v@hQwzdT+yMG9?-3ai&4eq?>|HmdCO*w zHkSa8H^_Xwf2_8XvsO%OS@oS{H3-Qj0sp9{?%2doG&yrdvHwGTk9IBn6x|@bJ*ab_ zA>KzYLe&LVNdQ8nvMz zoKsk@oL&sib-tHD=YNbSoFhj^_VLIb5+e&rPKr8ufFAM-c(oo+N<5y}H2*~T7b}78W+Dh*CttF#^<~L%q#g$H#41I;rC7)Jk_+mfp~5DDt1zr zR{381$=WG+2Vp#N*eW}@3u2D8X?yv+&aG-{r9t|T7oIv?KP|(_>$e2{+<*TQ8x5o) zd-8>!$p#&dJN3c>4Reh~PpSSQP(QC{Y)eb$-!DKfjUyy-&W@?*%{)h8%ahUaN)S9s z{6N<;*3nC{^O}t7zoVPdC;^hb-~8Ai)aZ&08_d$tj%Cm~_qp^9U5X&wiZ}bT)n-|A z79}~3)BR3;#m4q?F0FC^ofvG8LzKV@`(@)Z+j;3&mN3y}C zu@!;AjVa8?XP;3QeJE1k9czyj^Ei(*A^S@LY>(#(@`RJkCTEI|`#|_a--Cx-=7ukW z@6b)_BXqU|sW{d;ZkIS*4gr|gp=_Slqxkf+uyLp`urTm3l2xO;rG1Gss7^mDcI5mNH?ws!ZiwR8U{;N&6X>FwliZ5t%u8{~BObub-&3uzL+E4wzn>Khf`qe;(M@HGO*vEb2zVPET)6 z@-qQ)Qu0Cm$jh}%K!N#IZ}|D~@UUVb@Nx&XcfAL@*;w=FMIuj@&gQm|*H@>%g&t4$ zGIgPqDEF;!A4O#T-Oc1_SA)mq#wJP;o+ zLBW^iLymVt?sgGf^M}WAhj(%Lch>##V!@+#UijVK()`2Y@v%qcaeF1;?L4h+ zxa1_t`~Jk~-tX}xYWT4k1<#*`p#o7W>Dt=RF<|n~_@z9dG;E@jzyHLWM^Gi0Mbd ze#C?r(hjvUQCWc(nyH64IDVYR`%B)Z#nv~$1}stP)nNlTq!)K8>=joYPa~!+K`iI! zE`m`jPyK$KM1MI+{&Gt-#xFMKu=O6JxKdPM+9C8dKnYrW&z;^Uv{P~|PTWl>*AQv+ zjKZkDhTOUo?_pOA>4jQZ;GwJ7J}rePB8=YS%)>nuS#@ThIGT!JN1XSzA<^t~m5vC; zc~08~vA99a2(skX?_PjKRyL^Uwq_|do0wc)wNpb*uSbjkv(_gu74@h?7O!t-F0B2& z8u2}2m1c-HJ}l>Eqdqjz9k zU=ykbC|IP1*PzZ-ixnuZJAYFk>MM%#Q$4#D%b26xM%Or-o%B1?!<6_>VOTWvfTtPW zx9ya;9YSOap`}HsR;}2#t@JE&c+4q4Pf6VGacbncTmJdn!3oOH8v1mI9_t0qb~WF^ zpoN+8zNu^S_b1?Lt-d$e;a`Q2yYsf6L-J#ITgFOlx8u%_t{k#MzOiZzVntd$at|eN zm06}u1-feKbZJPOs!l%Il;Z16jnphpZ9X(AvK8JB%M$>b`x13+&O+lKqqu%6X&K1? zp-^?#{sy8OA5XtfG8!i=I`H^NFgaAhMd?Pl*3?_*JV2=3Q;4nXgeC6E#wqebvSQ;U z#bsovWVnLv{FV96wL7eig*tf%?w{AY0fa5Ki4)+(-wuGcF;GRa1+EWfd!7+$9{hZR zIOp_#w`Z^#I6sYVC6eFdoyZ=bdmT8ozmaFP_)Rvi z#wt$-G?QeHN6Bhjt|XaS_}S4F%lM<)9XW1FWT4ZZHkdEGQtg7z=!5uZMfGu{kn(K{ z(zSDrQQ~V)){hDTuri@fJ_e06sfU@kEPE==W4u(ER|K462eOX&{E{q5A2tTOm_C}G zWj;>=Hjr%*t@rcoV0c*wmw8`|CicDCIirBvO2-=5sB%JUsQxi%rJfj&;FE5x%B*)1 zBZ$(}=g8ga@>SwXOs2!z8S9fa+Ao*6(;wdP3p+SglYex?nd`S;BdNqx7p3>aNo&J$ z6>~e(nStpf$d9f70$b{0eEm|8d0KK}sz%3i21AcEdPlUrfH9hPOWLfS@0)l&O}u9H zl3H75F8XDC#+DJ4`LurPga&sw7PKCN4Lt;Pkz-BOn4Qs2go= zjV3bg9#vIZ0j2k7I&_aYbmv-Zz56wV_1D*mZ%0HWA~#-dVx;pJ?YzRqxT>m8KPq1L z9jPIGt0WEo9@412&ivuFHYSAK=a*^6cH(o-2+*L4;xobO3vQ|zKfDgITM@=}d%F6c zPXpdVR10Cx0)P=xsa_Kd+$U?C<{r(S$UlqQcj;5`XT=vjgnOenm$pxWw&=dG`S1j4 znNs8M+ogOzV~8ka0y>bwr>y-r&BN^v$ve|AaFnqqT+PgN2&{Nl78+`YpZ ze12v+sI3-KO^!pj&MiQ^WcjjS$(5C^>FfuLRF+gyD=JIGk6=`fm3PP`85b^Wy>C4- zJ`q&%kMg$25XGCk{lq?Har5JrFXI5m+A6gz@E0MMrQOSAEdga;-E^H2yA-x%Im_V9 z@Wd}&v6mjdFGw@oZ7v5Ys`-zrjRFK&>c(j$qfU3X2K@|oMfCJq=9G;Z!=|zoz;|-m zhe7iPNril81e&wGqvHxK-r4wvf|OI`L3C!z;o-QORt1d@79*FWSHhZ20lQx6O~w5> z^Q~dvfyaTzpb6W25zKS`!VCW5i*bMWXapJy3VOkE@cnM0fTfx*Bwil>v%TuhDgX-4 zZbGLO{pJzER@f=p=3(!K886h*aQ@tu)R>%rs7Lr|TCb2!r0=I>SviJhp!I6LAMzC2 z|8D9Hz0;wSk%V$Ty+-P<>&x-*c7JS(yW(9G zx#5_Xr-%YueAxmXEL1k5-oFy@8u|T>s)iRFFbG#n7P zr|p4F?QDudKf-7}7_KZ8T??mwDsunW_hK|W;w$Qr8+Gs$b$5n6_48K~1g&h@+~wiT zn>Et8!yO*5(cDX1&rvE;<@5ayaPkYd=~pv`heU(zM*l2{sIe`E$`M?nlr-$T47hb< zaiYp|q3uD@r$V7L1I3B2+E*s|v~(hMtA5~DzrlSn!G;@k^}vHaim!CvkbBE|1?_!G z4^+OT--nU%Sbe;uZh@Y!Z;{geh1NB|__#J+j9656_H9MffKS&ZPp`tD`d=kBAVw90 zyqJ)JQtI>Lg|O6M9v zsl&%I-@dW?mk>;j2Qu4hzVh&~s0&Qi(&M1EW*Aw`lXBDW^!bs+Anebl_sy5{n4kHA zpY_6-#r_Ygp!?K3U(uma=t~zqvv!5xiQeBX)(gTLeV11nu0p*x13qKu6)$qPf@4RE zk#vqZ9&jhsC(R=C0Qo+?P#-@2%jNn}`QEALV1Ae^z{l1APxbL=Q>;O9NMqxYVGBj> z6%lE{>euB3;lf{e+VIerUqRX>r|Dk_8P9Zy0&`qqGX)6ZkvxudbNzz_$pf9oQ5LNFrXsit=7MbU2$_*}l;dOuc#hO7YODIbKl!W=o) z%U94dpBLW*-PZ%io}4M^!$Sy(7A`C$>Q%B{=(EUSQYTgAeP%J}A?hci4OMM^zbK|t zhh=QE-$VGVnw}PZ?XQ`-EG_OrUMQ*pAV?OTEGUT9J43IfWTq z1HD%SVOpwWRrR=&jh%_zh6m&s^}%#06S-WreVn`6kHl;zIU$x;%x&pu1LB6uyCDA~ zj3rRg*cvrq?|~o0dU0gsj_5)49%1oND0`j}j8Ann4H9-=gM5+4`RpmUSN!f7>s{R5 zcT!yi!nS?^>KM`i%J1ws8&Np=R0QBR8HZ}?c<+Yz1Mjy}ELg+|Z)TKBK-lkIun3u6@TUlR8%gB{yp#E% z^t3n_oRXsOhAT+>)7q=<+rJurl}jgNG##%$K}lFXSB_cMX61zsq}ZL_^DO);o$(kd zXc#a&eulZKkyS^OSpXa-`6m<9pj=8qeFSUJKPyfBp2k@RWcwC3HH$0g`cjKJM%F;_mj&4A!^_eQx^-eF#Wfh@QKcy~hMktH4)b#nHC@Hy6@Oud71puE#q)Ui~ zO##*D&TEMfJEn=C7VXVD?WZSp`fMYo|JD3zXAbG zv4PoOWRp?FDBgTx3#Z*x0B7JdG59MKAsQ&AaD9d1Tc=Vv&bN?gsBgc z)HHENW7t8Cj_$ep>)Q)#oHSOmg>IO}Vw9$i>4qm_l=J#Yh9`0HGsYQWDORhg{T!N7 zq+QMUIwCFWmD*wVm@d&`I*~OwdwR07wSVe5FRP6WOJ9@IS_Xp41@oVmKn>VVR6T+jZ5P|f zAoJoc?RQgaU4-q>dh(yor#YTNTeY-LS9zTBolaKr|RK z+kG<*8(5d@pSs7`JnRAKkx4?y&wNBn0@Jas@oSltcfXcYd0N}n03*s;X~9PlBJYF-f4=8XfI(k ze+pM&8og#DEIY!or?FxnHC>JS385T?VP(WFDZeI2rM$%ucuJZrDy5F|i3^dOjwB{! zViV{(r}#97d39z11@vQrRYL0t?8&FK$+WO)39K@5^VzaM9RyahxRO!EA9J|k)wCm; zlMNg{_FFi0wqPAZK3~M~Ddqb?a1d#0cU$zlW>)LXoIi(CO6u3-0(0DlM2<9rtPJZxjb#8 z5t!s+iP$4d?k+480w2!_*u>P&w6K2C7 zX7LIiPiZLRr0UXYHFPvo>92cBQDya*39(+u195!4mRg9l;B`H>a<%-}G zAM-CuD4Y>W*+c z!mxN0w3GWp!(X8iqTmEUu_I|?lqBIpxKUH?EcB=^4}Z*oTzfZ|3UzY;v3u-2HI7v3 za4SdzFPVcvk>>+z6ZZ2{QnJcdOl;wH=A1jVk$t5$fo^z^L!m(#VJcFQf$-gjCCmn2 zGS2Ulu|;U%1wZo4F0~a)vNRVQcP_)hb$|$5Q(uR z&xY&>L}D}?uUcXm<}3Cv{;1k~&bQWpd1hTvq_}}E=%xsy7)?~nJFQ$_%`RV^zA1ZY z1DH9{iG<-d30bk!aTOP-IqH6b;_NC6E?cV4Q=LwET?@YrNC1t zNkPF9Elf>J@|X)a@9$VM1>a=+0GV?TJ4s_y|26ZU&a>S`SVA&Kw;OkUT+L#oka;^k zAHz2wDrjS%)G-~J!4LHL-Ajd`E01sJw5`SE6+s=%9HJEZn>~C$A#0ZKhgst<6Hl-H z&%+G1+`)Zy7+XYMPm$_F+6MFB1T|EUEcTPQhpn03wjrQia@g|{;N4MRJ{d0Wv_j5V z8Z}Bf3`Q=&^cyN4fc^8gBRZe(^kF!_ymr;B4(-6nCS@5sm=%o2VEN9u^@A?{W@Ge0 zmTWd%1FmJph3TeVI>Z||SSBM8qIy527IYN-g<(5lOUiWhuU?t_N^S5^r?C%&V<-5H z=P`pkS<-P-PQ0PBRcR<~fLfueOeb4ZEH~LI$mONg!B|)$O)A9$?y2n{>%q6{gW7BR z_pAl(o`W@a$Jnd4tGji!z8;LJHldqQVV6InZUi0)eY=x?A}Q~)W8rB8{U?49TG^EV zHiF1}Y?2SRTYe6JczeYP3ZSy`xyrA}U3#tEuf`;jASR83Unfn#S0U}LApMTr%bzJp zx!7N+-SU!fCFb&RF14TB{W31azx*4SL^Q)ecd@FW*aP0dzTZ(6I2p|%?@f2R=m9SdV|@ETRAiwg95W++T5or z!L&Z;lFXsvY1z_HK`Yd zgNHscwk+qcRQAg?oJ)zQ2Ui6iK8j)OyEUQPvP^Hid^W+i9Bz>&KQYkOkc6KEHouRY zu#CTC+Jgl1Fzw0i0`?N(`fgN)+)g;B6!%m-ey;QgRUUhzR*c#x0{TN??3zqgvvn_{ z#@=)2$PrG&(Ne=(y}Tj2%s~`~(_?G;Qa|ssXnhv!e~jdk;Vnm50}>V8E!_lgKK}{D zeSGfqaks3NsuzZJB;+5sy%E(gwPm_J#sm5J(t;HDdi$(pr_AA5jn$~g%cAoUnywghqG}_}ax#{z>JuMZsIARe`m}S;_*A-i4n_alXGqucz-lT<$X_*)FN%n`R zKR-YpF@JR8TK=ZbY}As`>N8@%aP| z$0ohc2%WkQ6&is$^PcAkE^w&=LQ*`|hGu~;Gh&*y(XodLp9m|J?lEzF zt|6l(1W%B&nIk0I!!m_wTrT}zUfmO%s$siZ?MRMb#uCHVvL@A<6Od$oF5t_Xg#Jr9 z&~r#opi;l?E(otCSnQ0lIM0n0qo0%~2Lu&zh3t##EW?`DMtw%sRf*w5b37Hcno{%I ztqgFbe-&A!k-me!c>+i-z;$(%Z#P32QbxL9CKML#$+toqy=`z^9VjU9i|E2O>e}rjO}<6 zw5TRr-kuMV1f(o7aTz~(1)%r97&_FP^}iV)iJ?h%AlNf`5+sf&@)m<$0ca!`)FEr7 z8@6m93sLhplq0`}M#lP5Ri|!r$#zPm*r{4p7X`iBw-Lu%=2Yc%yXr7ra!Ed?WPBSt zI0xYrPrS@rOMG0&d6V~G2j{5Cpc(6c+7@j#N{X_}*0^y5TVves9(D4Nf7VAgjQIAH z+>lr3Kp z>T%p8vVjt6bsM%WxtZMfS$_+fm?s1<_$r=Bm~ElcLfSu#-OP@Sdrv&+52GEyoRh40 zsgLRLm5GY(9JK6BO9YR>jvujfS9{+ot8>)R>i0zd8ZO^o@&?t<;!B!pCG)1RroGC( z^CGx?y|Ua_csMK8VT;IqtH?poU#~=MLyb>o0aV*tM5%u{HFfJVIys)tE85y{DV3i6 z^O!jw=3@<+m_BZ~33yEvo~^b``ax)CaEOOgw7mP9(xw*MnO(Za;b2N2VT?oHk?^lY zV&=J6-KoJ*I-GQH6FubYS4hFflU;tZzO5Af7fv2?V2q8b)m|o96;U^5xyr5=wrN(k zf>sD|C{$!d9n&8g2VJ=ZX1m7T>-l;ZOM_yEmDb55Jn;b_9USHpW@#yG=$^{JKB>|v|^kcx*3h640kSE_amT!i((_I z;@;BjZQ6=D-1P!-D${KT;@6QedBku`T;x9NS#gr@VN&QJqw6I?PMg)66Yf(jR;^Bi zzR1h256f;q#$%Z{L=3x2Q|W<1bOT>rv>X@N-$$fT&dN=_x4hj%63k02OB0#n*iZ^)i z`DW+QCb{jF>1)Z#ohFz20oX1x^z&6%cHx=E6=s*P%_L(8VuF>;opKlBB$hkcvWTfl)-96XD5?*uF^?<4}sIl~SdaMTJQ=NtDm<@-fj$&QmKOe}B znz>!>BgEtonpFt!TT}PK=Wd}{trrVZ3*Y)%x!vt{Ld}=T+LQ^jD(4g{IKuyEbe}PJ zk`3KK5UDudH^L&QaMo~5s1XvPd3YX~4BeB?=glhtC0s?20dbe#mVW@D9ViMN7x8{` zGLW{g7Sk?n<2RFhjI2Pvso%8y!&`{+@@pC%=;zlCr$f_IJd|^AS%0 ziqVVt zp;xJ~AX(jiiAx{Gp2au~i`tPMsNq#FD&p+09`SPJo*JifdknwuT53*nxh-QvZ^f>r z28eUHa)?Z%WxvGH3p8or;nX?8*Hc2S#5N|5d)sBVq=%svhknETO&t!Hrk>72U9J$? zOfUwLkz|xnos;J)1=SvS!6?#MsoRt0ESIkSI!Exdjq42rA#QxP}^2)E`{16(iqBBKEbhRB_UZx1&>per^pd*U$7jz-UQcX4=@PZv5Iw zIrXQER(+t9hfVy(VpjtBF8`GaRQBjWezUm!M}$a;0NFVfUyP7y?gq9L&Od&afi-#N zeSDbY2(WuAG~qS9wQ}~#PuF`*v80Q^OA%nlxYhalpWj!+wlH58=D=3-Pl2Bs1MZPL zp4T`p4;9)ruD!hE=>Fn)<3y((cH#8@wpJ$map_5W$k+|}u%ZI%C#g_!2Osz@FYA~2 zEV&MOZD{Touqkq;vx;4ohn0%ndmA1od4ac97V1gB7=d>PYbNJ?&huI1ijgIu z>r+e%nAe+62p*~ZnpfEkA{LL1u7-fh_m+q6dyB(%rJ4V{Z1NBqCXL7bkYi}zWs0I% z*8aLjsIPlXev0_IJ^?!Yp3U|>F*|zpL%b1c)|iHF6S}YtlIhQI)m?nVPQqOYMFjB; zif`g4;^NkS0q!;^$|9y6&sop*6bF0aPI0iJssxJ|5T75_GZi(6Y@FfrPG8el^-8XA z1CMvVfD(_(dom~VktI(;ZF>Aa0#l~bpHKK+pc@}w=jU(WkZyuuk2(k^> zT=<)f2dXHyo8rlZtqLn6c4bVb!8BVD7|229$Q^E!pW3K^xjkt-Ag74^tXJ`k-d40v zp?o&}$5NCEKElo~5lgjKE}KS5Os>F0#&LS!jGu1!(^(6-7~`^wgE3LF6PVOANY5jJ`U?_|}EApB{lesR0hmv9bxY#U=sgX=#Q& zc_|#Tkw(p@U+#SVHy%qGOU4t#w^dQr;T+x_4B{=KlXbZgj zu9{a`LW0{CZKdNUW4|;D@amMp{4>4L!G%{~!-}pS(0(3Y($W=LQ(_03RRAdN+jlE& zCPB9c%+5G^&!7DOyH@!r>asJQ$PLChUOiFufI0TmyCEoyb3(!N95j4ZBxd*N8m4vU(pH1es!Od9!I)P zp4>c$r(~c9=M8*@oqj=CkHupQ`ZSGm$vWq%nEZjwt9R`=W`+^vW1|nX4P16rXs>BY zM->nVx{-#S1U@)i&kMzCR^a`PqAHHO-hN!Xn2YfCRe=J-_viKB{|d)z5yYzqCb)4mMw4BBiASy zJ=)rt&LPRb#+5M1?|sjA`ab>w;_i4_>!Sohp#SEXi-5`F0O=K>bN{Vb_u{)@C%yDH zZP~<;Hgw8tpAC6Hgx@ccwS+K>a7*(@79$r=#S#P+Ay#&Lb%1rsH1d0~Hmn9Lj@gip z7qaulG=IzP5NU9NX^Q;Y1Q#q4D< zp$Cz%L2PV*d`k$(HD1RY_tg>2i)jv)v(7ME{tDkSrdOpT7HnRZ9(o)A{V8(_7Ne&t zeU7;rtwW`KA_Zmv!DK`2kTrqDOQYUeb=|wJIz^#&5(&KXE z?l*Jp*<4WJ7*dy@Ww7e*6L&bp4!!kjze;k-7T|l1c8iGiGV+ER&N>!#_UwWmBQyd> zXXYy&-iFjL-MYlS9~e|iQrF_y#vl1k6Rv{b_N{b!7=Q`IyT~9INsahzra4DViA7Pu zL!HDHmKuYD?F4hD7Ljir6_oC8S&0Ns`%*T;^CFO3vY0E$k}p`YNg~x>A}-)iGf@pQ zV@F-#-8!Dpg+e5TAFu$-EvNMp#Xs>_vtpp=2Q~t?aiuyIkYJPor0GWvs_a>Ewa9g~h9Zu9TsW+2p?o)gehgZFr) zE+Ie%5cbaam1WsLNG{>l)ZoyMbE6?$N8Na-&v@5+s_c$6gu?%xfath)s_s~@ad-5P0WplL>HdD`m$-zUVU zci6sZ5s1nJoPPNb{$1lHUAw2*U5t7U2taNHL%%yF@H+$$YA@9DkRewTdXsGlQ_GY3 ze43dB`-Hh(cN6e?gtC6{tM&%k6(!q6pLH?{BH0j6l=j4f`nS2x{QhHeL#lQp3w_}I z02~up!#*NPK6Ve?lh#1!}4M=TETHkUg^Weu%?&MCFH9m6JT_VRs!K@!g zHPTqVj+n;po@rFs0pmKS3wcj;LE_!{zHyy;yV;N1d+V8>O3_nHDieHJLV7^5a zcfVV1(*)O; zQdvboTYc=d2Lt5PO|qoFNMHNIRmwQu?8Jcx-pG6&q+PIZlC9kMN}anRU!QXhWX<^1 zH7O_$JJ@z>CKYg<4JYj)D^%1c_WRwsp|W8*I)`vxdN0w{yXTCLEhw`f{z;3 z9+tF$+E;+g$God`-sz2kL!BU~&J}+ae$z2cvbhhg zFaFx|n71;uygn~`bL|fOS{VF2H_T~I3mq=4&sZ%z{U*rJ$NsCG#O;Aj&&r=7@ypu0 zlbR1(Vj=RsPn=myzN#CFJ6OCx7o)z8SQ$(L4enjsL%z33`V^(?OSiY_yVP{_j{kEx z=wm?lZSs5C*}v>FWo2$*^5kyqoK($$LiJybh4s~OQ`xkHH?}s4OYKHsh@fbQxC+c? zF~qK?*sR4UiNwc+)cMrfeO6h3u=z@PYN`3EwLtNY3G0^|YnD2^ltp0&nlCHXEE#wy zW4i^1?}Atk%~Qwsr5wUOERL>xFE^K&x?wf|$lv&eJ;y?cdQT=7Wy%9! z2r6ox%4?pw@^WAfC^GXkOq%jpo0bs$W4lxP(yNGBZleFHV8W!Mz#)6e`&iB)%y2Q= zBq-xjB*Z_%A-m&G(s=iu-F+K}UAZZSlxBNF34IBlG;xWwKO_>~jk!JUdw#NM>4qHN z3P(e0UT#f^`qt2Q7Fsy(>&wc{jwkp-zZ&+;O|`aH_Y|6~Rkxl(LirB6I@AhH|4!!m zjo0QkL%;Y*F3nWMx=%H~FWP{(a|d*Fpy93kt~8-ypSwCnWp0cm+e_awc1kzvGq+n(Us;w#8k2gTbr z4VK3a*})xur<=7KRw{B`JU)a-7JC&=f!ys2(X1KL5Z*U++EpDk7`k4v8yiGL%z!%x zSz()wT}e>ZA@(VXx@0=I5OM4#_DOou>XX|XHK%0*AFc<96kV^>a; zErC<5z-pLRz4eEuyX(F0P8l7h)q^Ca1Q-^|M%T;YOsVX-BMb0-PL*5ZI(kd)2MYZ3 z7I)3Z9l!sPXZ|~H?fy2CEkYaFTWGPmN70_iwmvU; zbL|F$a80{63rXI27f*lfn)-?`AG8o(KV*PbZ!5IJ>K7Fb!ht`wNSnLxIuHeEA^$*2 z>%G~lw|OCu?<}%^T3HWPQHMZy7c1&KjESKICD5Yrv9G}bH4O;A z+g}1cUL0ELUTd4cNzBQ;~FC5s; zFT16Tj{DnrE2UaMbQn*xjpg;ioZXV3fvh2BX=`V26!sG=t@J<8Ju6&)}XSwm3IDaHIZ5nY_c;UExP(xB+(pb#@_-C=GjkyM_J z?8m_-gb5MAgs5^zpCV?io$cpsMc3+{yzLssoVV2WR8#htgmmO0O`_TqcS}w^-dr+g zST*uKskd29uGkye7l$p7zWFJ5whap3n~8f`Z_X9V;6LUSLm>1~F6mb8)_VOspzK)?VVbJ(wP zmu&_w(dX-Lqt{#pPQQ8vXP%vFKO+BW!_@=z2f19A!-~(tPy)TZTVHJ5wT2?|3EX+4 zSUDTl04R06@2@{5=;>7@RuAx2_hy%znRtq0wv~0_61acDsa%sF&}kPSn&OhmENNvj zohp6mY|q%3Tem;sO=8hmWX4Wq-uK26`6g1K=Y*e}!2M8$V3VA{((LYhHIj5!!*vh= zjO@Y*z^TFEi$vhu;*b)s;ImN5QPfh_5)@I$k#}9faUvT`!ym-BLS2TZ_abUnP5I;5UfyPCuAPoiGs4O zu$C}K4tEYEKLtO27C{z{5kWnEJ*7G2Iw!sdxIN8{I>a|Yb0+RT>_PP-f+Y>*dYjP4 zK@sh?W33gq8aq_!#@3HCf8H<4-mgk6R}#52>QBpO%ue;!jKEp2wEnGLQ;DeZDAq*j zqYjBA<+>gj7s0@*$U=suHIYsFt)>!j<%$1rAR&n^0f{bEL0zJ#;X9Mh)Z8Zj{b}Q^ zr)qzAP0_MP6L)I65e1Wn5XG|NcE7&I?Z_<;XnYjSzm)_tqxU9S=e4grSDk z`TXbC;-nn;{372l))iZ)?CiMM$!9#v_P8wVC?%~+c5hGS+odK7rnGu;s!ZR4_ot+} zf9}HSCF9DlrNB`WE^?bET&fH#7_RYP0O#ATnS5bX^uE44_8)qfm4#`=p$g&deR*I$ zB1^7c&ByZxb-G0$g#ZW2lHbKfT9@OmH~%ITOG1WAs0Okwrr@c#A(RjkPm%tF|40N) z3zwsbaGur2{*PFp*aWhLg4S`BT2d~S!5Tb{{Bf~9Gv5Y$-Zaj?IKQLnY~cE}O~a2_ zzoo7M&JfN`qU@5g@Oy(E<}ljN$G&my2hm+e#^aEtH=NkQ41|E9QcK?01axc)#wpWkfgef|i3S`74gCTBI-u zT2b^;L*@hqpZ{OP>mqBLf-TF%bHcMdyDCJ?fYk(g&M{G_@N<1zBTF{yW5-|`f582i zcO05!RH zj5zlaQ|G~6p6XJ8xzABvrVELMkN0G$i)DR~@8pDdT76K*!K}E@l3eG(exAlsxB1mk zUbYL1#g+GD`F~Zi^A=ED66we)b;&iKm}z}mZ+B$MwIt#zkm*9-nf1Gs*kWSOG`9Y~ zs&$x8%(j}>?;n{8E_wS3WV=XrW=*s*SxoGkrq=I!3ur8PcVv~j{8zPSON734=`OgP zhGVT*7JPfApX#Z->y(xVI}A%)V$AtwT2<<)k4*oo+ITC41>e4DLj70ox>rkYIt{U1a=dSY}CQRZc0J{pg68<@h#K?PAK@aG_0HH~bu?_a@jpc-0uldN|x z+>ec$27)G&6V_6bx)1M)z4px0vziR`6lFa%q6}42?w+P7_rTS#_`q^ecKIV|oWt@1 zi{HDTp``J0LEf~$cR_#E_Q{2K(>j01?Wpa)qy4JU{X_0w*=0Ps^nYt~`nru-@poG@ z_lo}m=h~&WM`RJM5M{R>87o#iIpN%lIXjIQm!qbh#X{o0#JV4K)Z*=BI#D4BdY(P6 zJ$o|1A0sJ=rY=ZKlQn0C7|ma*^efi|YVaJ@MyF~WJ)tD#0p)@3nU#f=t@=MkCg74% z%4w#%(~aWe8c~|Rn=t|SuG98BMHIz__4VX|$I)7$h^LV$cxpR%J}^*&e>oo`#Amr$ zC{_SA?70vTq``?VQHGWF!_$y@JJ}#wXM`q7aQF=j)Bo^gRx zECi#|@3Sy2OyF-pH1*_?15a(gFIqWIzK6dCy+2^K-`t4yh-V!j9jfjc?3_1SVT(*>|sFZH*Dc)C%`!y#-w5gKjydKmm(U;HkHuESR=VeT;IH6xu#zw*9lj31fb>O`*ImNyrKr43BThW&zzpWD z#>3k{04%)G!wBb)ip2Gt-6uA=COn1AJp5b3c^G$iUeA!;9A#X zl#9{{Q(Kz+etM+L%@<5{BHP{mAH&Akg65Rw)mES?YiVnln@BJ`e;h9)ps2)_W~+Sn zrt)2Ez;4j`(SRcXJZ#EQNJA$k?d6?Ubj2m$m1VfLV3?uEXkWMmj-!Om9Yr9OzY`Jw zW(0gzi$?J15E@=y!dyo6zJb&tUOxCTA;^n0v@$6NZ=guzd%HT-Cls*2Y!gx(ZbX98 z>w$Oe)_w?FNS%AIa-l1_hQLc$bl_AulDrSoe+IYkfYkr6!JPF4)76=Kuxg$F+-K}| zc;Uml8WXgymNfz-{h_oguaG|MI9&}NxaY~yGj1WL%1Od?kZ=(kln7Es*>RK&CC0<6 z1cFq#-X=24nuJtSBjvTHKmH23+woa7-twveqQqNYy*L5tjZ2e zTv7$-P$?SRzDe^c^@4Wp_S*Rn|7KNL=BUyyV7?U>A7;%-7dlr!zFiH+cn8Mv)%=&b zt@G9s%=Mip4CzlO+Tv70Xv6}p;Mh2n^X%B9U9R9@Z3Xh#-11uIWXIiYD4W*}YL%a7 zu(;EvBlRK_aEu|> z7t|P!Nex)#6S(LbMp^4eX~_@t-uc$9feUSd6P?UpWEjfykcksI7rmEQCgl!<}(SFp<&2j2{$jXK)e$A_4KvKAy{`gtreuRg5;-7K7?o zgV6deQXb{99(6X$;HL^E+ym#rqK9F_rCu4ab0RWsurj-muV`fpK7mp`H*CzY2acWt&P!c(CNam@c%E z4F?bQhcc@m`H@p1pTQt4P0I-${1KBKjqMZQ4~<)axJzr24mC+fM$2!7YvlMAw)!sb zI59JZH_+X^NG>O%6mCdROoG5hR3+Ubfw(qGD?@&=KvZL^pPQrS3w)Btd3ODX;mMm$ z`PCK$3gYhmmg5`du(c}eT<uefMwZ0XQaH#>xsPYWVKF%U<8W$h^8HzLM zo?O~+E^Q_|MYV>7xnlU5N#ts=Y-jq1?FWesS4s4A4M8rwBWTu4fw4EB6pAp$yc5zc zgcxce#*7eS>k8zl`{<>S>E>`Ma-zC7<99%04)7Ep_r){2KLS%eaem=oCU!szwy23J zW}PrWE+$vMs&_Sm$)Hmqx0uimH<<$(QoOq&S)lT4o)auQXR+1$7^{Lrleff&`)H&} znTFbN*(X5JrCgYpKH5$!=;Z4f`ec`1L^qN~6%3_5Fxr>65)O$;m1awq-YmRaMgv0WIta`j0vR^d2@@VR6y(R_I_r_D0k3mN1p z4}Z6yn|O~Netb8F;U)AkM}hXe*|p1smU1n3rzTj)A979jo$2Fb0Y_CkN>97%$6?go zAB#yZY`*S&uL6dEkHt$bY={6miJje|cW@jn`EmNHEr?MF`Rl5p5g#EFj1@bFm7bK7 zGb5U2CLGOV4k3o1>6MPc$1Et@jpf7;9>9F`s%qHqUC>R(W+&5VhDQpyApOYbqs->u z%mi_n6TcVKG`;lnH-!R6!HgFS6R1@YsZ|lIr?sJN9fQZ+gIWG^5?xvK)%0Y}2&=#b zhVQo1xdsa`98@gMG=cCUDh&}1xuj%W^#go9#`CmyeLZwRUuZPhFO=Jz9WgLJjAbl(#Va$jx&4?kTlUk*d^p0>N4s`l^#rw@ECg^dkQUxmPaRHkE7YpcMy&U zSA;lP0v+{_IXp}ocJv_Ye%j9g=N5x&nQrbroBIJ?38HDx#n2!jk~Tq;5DVZa9zMpE z%BIgLQ7m+mX1s#r$Lv5s0g^qQIerFIj4hXkV9?}p=)loS=@TuqRJwBTFgeob<#%t8 zvJaYRsY1+g^sSWn&#@oZ8ksX?5_1XR1I-g@%+{jn@t_CKRBK((8V_g556-fX4YdO$Rd@JKIva+7#~y z!^8-}jp(N%iK(lJsbD!faXj-Zg?D-CnSc+|qU*qg9zASnT1xNSW*d3vz=;eJM)pk9 zScKu9UHp;G3w-3_$tblNz2#tdDu(Clkr8i z=wZlE^T)QVkNzCf!slYN6wW*M-jsdKnEsj}R)~D6b*vEiMM21v`?6aw zaRnRck>YvTKVIys)$R3*h<;9v+{kV9G-a;{^535p8%p?P1oefhKr}ko>w53_!<;%J zSU@RMz(A4CZBDk3>t` z@X()TyC!Cu_{*K4A+OGsKn>I>3XxkY65M`1c`6k|8Yg=sf@F3*!c;QR_K50eh!>O@ zB(ULAZWm4+QH_ONgiRc+c4rn{SGo}tVdE6V0(JUq?SLmbM}mZUMM2-mIg9b2THq4lFD@(vYli8 zC|-E3vh4h@wcU?8)VfN83_u6N&INGRvl&k&Z0bgi+vQIw|3Wi*%_nGGI6%-+$o%lt z4)y`dTdOxA9^SMbj`m1Xtk#NSn97h2c1q-<51&Xmv=*lM2`K7UglL(G& z(sNP=KB+VRzH_e@V2!eH6GzLC;JQpvVifi#mslJzhbAp$H7sd4!?kd%#LMsyxOlC4 zEz3A_)ZQ+C0X_ahJf^Osj^bp;;u|0}ysa@p^uV-KVEp_S2T*aYk)!V_#|x+Z^3v~ay0K>pMMs8!UD zCbS5X*@83=_sD~Rq6eiSGZm?tFo(Uph+NEpXzX(1?O%mn7j#JEna@ z(;UBmSBahRP0O53RP5?r)lPGq^A+wov!)m`d&n>C{px zq^NLun9j=LeK~veKkR4@UQbnCZfTL_?EI!eeQ@BmYh+R3=V2}@i?(v9>WVmLXHQr0 z!GXEq|J;%LO@->9^ljJRBG>6*YO9fV52qe@EcnsS<2sJBBk0)`KrQ9nAwn|~Alr#IhPG*77pC_c{MVl9Rpx;kdGt8he> zce4zj!%>qO&953-w)FgYgNCPwj%+Dr-4A>sR6p$Ih}CdJc1*^+ezygq(rkCSHq8FM)T+W5?`BaX3mpKJ z#SVH+F10+QlD1Xnlbq^>$S(O150``5@`mw5_kxpXe!gofs7EEkoN`pZMd|C`>aG_A zQjIe35QupBc*&!%c9cA~jYRV&Sm?Rf&K?Wrf7cpLWqh&K$aA1QD%n>G4eHq%DE`>N z!jtNFdTs;u=-dT61b8#K1;e);j8^g&UIZQK&h3zu6A?gfUsp4tmnS3 zb+_X@ z7YZIm<)fSv*le0omRLVG#~;fvn8NXvV8KP9PZMrrn+-5}x)KiHeOva~4C9o26_Wvj z*MjP&u8s46yIO2c=bl=T-0`p(q}*9=oe_eR>zVfpK1f8E5wY>aaN$#z%7kmqqUCWW zPRsaflqu~amuGKS;NE<~_a}oByW!PlTp1PyMb(UyO0BkcC{F0y6LEK5853{tsgzm2 z=-*U7*S-){h~F?TsTbr>)V|lT#8{mc0M5ag4x9<9GB|32QNAW^iRAaXw6|fZx1qGO zW~#I%SzEFMn{BWYb*X!V{Zf~Sz>coant}w*7SZGbA@c=02F4BG&hb<81)tK}Fx%Tu zxqh*0@zJFoHDYhcgn)^jHEuOX7UyW?qpUrw$hM*6|J;)8W*K0iLDou;P&u&2>z z2t4M+-J}>j&o55y1tvibbx|j9^0%Qj>z&`CE=lqLJ|Brtuai4Fl&mJ2HO@lk5_pQR zWslnIxIZBirWx~95_e76I4QsNl|A}BM1(_IxREG1tSI+IQEr&G@X5-}NDZk~@UyuT z%H9zzikps&DXR>AGQo%t&!Rk;h0w9TFH)laK1_cZ5S?Tm_%A-v}nUbVC5cd1}f90_~xcV}`urGTBS5;M+7*}x0&SD@YY zX6CB~zlRvA(+}pTR8Ye|G!`RHEn>Z2SGq}Cu}NF{l?Y#duaV=X=U55$CM8dQ%ve=2 zsby?U-T?_GUFfu9%8yB>+awcDkYaFKLXD>$^y+) z&%TYiz8)OkAFZwnp8Yc8^KC*yu|vVd&3xXErqGqLe9L2ys`I`>2iCC1=X!jE8^;4I z3r`k}a(eF&u!~`>HYUSY(HahnzYx%q{Zec|~hK-%|C+L*sluy1dgnugE zc~rE#Ij_3#le(8Rs!JY})uhMuMYWLdlNe46u`K~4wWC4-cv_heWU~0Ade!04JOU%V z-qdYIS{f-a+a|;Uqp1&^jD?=|qBPW-U$oJ>M>-a8A%InxWjv3-%aNvhL8Yg%X~v`& zV}hB_nq^aRTn$|$838dx81r|)KmLy-KDr3 z7@|r%WpZ+KuFsf!HVaOZ0uZ{fiGyCHlh>m{w^*M?zLl2?-ZocXP(aE{$t%jp9~#cg zOf~PfE0xdZT8tZ;^Zw2n$inA&g8_bn3U?3wG?R@>{(U!;A&^dm;%>H*ecU~QF1!`s zuNb4}?B{tca3#c9cd${e=L}liOeVoAKiEQXD>G}J!PMqrE-|0hYs+ILDQNWD&ZGpI z7kvW#5UY$mSos*k zHE>S|j6C>f3QN+=H~{5*_G{ZC$BRq zc=s_CzT&HT<@aLcB={Ksg$58q++{BlQy+0lLE{?}kz!#$NL#h+~>RY(i;5X!=60RzLkW~O5;*TMIBq8HlR`Wm9 zzHKiy!VJ=tC^kHwRZXO6S9EhPgCM7jrlG}0^oF@6krrwxWm3)1%23%`&f#os3?3f` zm_#(V(~>`4+|qR^?w1m}l@dbL#Wq%fdO91ZjXU^<48B57=5|n7BlB_NM*)T` zWO-Pb_6{x*7EBTjP}@WF+W;L_YvSau?OI&DX}X!IOVSr4UptNdz~*Y_>(&l4#BY>< zE!Z7jVHGeQ6Dj!S2@25qHK>grCUTEZE7==h7#K@s!*?S68M66k4CNoV!CC8UV)K-e zZ1$;{6kXk# zf?k*>^2f`ehBeMNs;AmLO`^w2TJc;DeRv+8WZ%VLY))*iyW(a6dBODsT#!2ptODcE z6=r^934ZLTB1=&*bkGFD`O+KRCvkZhk!RJ3aclPE}h`$b8U1J9461!Ab>kpERaS{L7Ji2I|3~%lchfE&=8ry05 zm2AhF1QY?Cohj&jQ-6$Nk@1y+?U+aMh|oE)u6=%dD|&#z1;u+o=0VsoYKFJ>)K2Z4 z@!lhpBg|yFl*&X@Uf)*11kX{B$>DmsI~@RJ#RWe1M;6}Hs?seJ9?CUfkUpVoal6G) zYsMrUqlfuG-|N($&B5KDhSUp>HGTA;9MJIOfFge|dij}Y*aI&vYF@@*-KLBtJ6LR{ z$yv{K-&Q+R#Hv2$Q6(pPnM?pNMc!UvipP;L;LYRXGuSUqo z&YFne9uTZGR)}+FIdNGB9FL&+xw&)&urZS zz676NJ^j$?Y0+c9+Vg%5TYw+ulQLK55m)8qLa9x$?Et)^NM~;NMX1gYDp|WOGfyVf zK}NBa@AYSjiaB)X8Ra3#26;fr_YN>x zZPM$}r`D9sx<_PBw!~jGb zxhi251AlIh*a7=o{-#3Y9C2FSZ$# zWkJS=oW7vc312WAYT2Py#OE$twO74CbuG*ShoAT71g9wScE9hyO%n@06?(?(%}OshdOas>cFmp zstNujbo}<598L0P%C6h!@u!pxLLs(BkfI|HHmom;FvB;=NRV|VRh8lg9x7DWzy4}B z`PEK74>{i~Gz2+Kgxpq{3tSDFGnGvGCE2&~_?)g`_lx``Xz^bIA@#`u6)7TVayp|tyz^`m=P`y(a|GdVr&kUj9RUV|Hew5KAU3* zND?5fQ}87^@MzbMI%QDz=Q{Hm&*Oh7u^h>CR(MmPHaIC05B}@&DN~%tvJGP1p!b}4 zz_=@v*5r2K`N7F-Wso|8#1N#8hvV|tlg!`u(diUka|+2eK3#s^P&CmzGcQ)Vu)FVa zw(-pc)kJ>DRnj9i@bG5y_4H*?e_^5dVzBRP_&z!1;(AE<=V2DB{8;nIy#9r!Ts1F@ zaZ(&X#!imf%LRN24$)F4YS9=A-|^?OgZ(D*86%r;rWqfM<&+s5ygZ=g|ZpjjWfrzD-*O1)H~TEs9nEMP%h zaZyRyHk#o}z_~7(y1PuIU8S44yf9tfJ>^rb#Gw)`bzkM@vssG#J*x(3(Q{vOA2>HZ zaK0KJF(i~#eXUsn>g3btP4kjN@&`%Y_U2yv9o448SQxE*!@jhMAFEu96k$y-y!D3g zn!NIMVY*)e&UcA?I-MyVdZ!o z9g{_+(@~D^t+K~^`f6H+d0U6ADn@(aYJ{T|boqQf-TpohrxmZvW<}X-JtWwPJk$IJ z7FES8!Td-1O@+M6AE+nl^as{OxBQDhS_btA2LKV71e}Z~Jc3!C0Q6v0FOijbPc$$F zL<9hpC4wtj#kuQkU9(?ai&{~OI#p<2now}(Q;M#qs*dmPIlj**z1CQIt=sH0*6j40 z*dc=0AP0AapOn{>y?;a)kXCWT1Z_`2Jo7Dlv$aDM?mKLV?GK zb0ho=Zoh{_il79N9z5R<;6m*T7WBm|?m*`^g#^6m=YNv5{x{7Nztgh*;Us<{dHsWy z^{4o1r0|lw+Z$<6#sN;xbg`Q&I&!lUXjO!I=*cK`#)r4|IS>&<7o=flx?b+Bh5%3` z1hMQOh8SnE>QEZL6ANfHi{jWP%L!aWb-0>oM~kt)*+S2ilUhpV3xzHcmT`V%eQj5w zU^ZhR5e zhYmmoK(W+N7xrA;=lC|Z=uZK+cO-=VX-(K*zy&xsGE-Sa5l;W3?;@6?9)Tw;S9~)Xh)Qa&A;*wl1& zuxssV@9z)tmth#Mx#hW%jrNy`toQhcmC@x2yqk$$IeB(#_k8e#&CRu)5bB18?zwOD z3_*7@dp0?_ws$exuTFPKu3E}E{?`7Gl{dfO+MHi$VB#)lT^CvH$4y)S+Bxlxo<7U$ znVU|B4e2G0977-5i3ees$@V+kuQK^IkZ3;a18;aBEA7A zY~!n&ZyxT=e9Br}pfiL>d<|exj-ypn2Wqdr1xWH%fBV5Fw&6^9Z^I!t0aaNP_5}~% zCGaNYlR}VUH5g5&M-J`>Bq`o0mW@RfjWh!fx+PUYkb7yl>N4XHo^+oCii7le2?~3!_q;XJi_*B_p-daHl;PDk z(SaQ)+Pew@BV+!ZrUV`VMhx6@8|C(c8|=b3VmR4@(3q*gRPdLJvv4dgD=rUS12O}8 z|K>zk=}pbn8BDsD&5BZc%1bR3vYZ{?c&iU~&R_P1GIL_6Ca+Xo zsAaW2Z2jwF9W(2=wk_7xHRsE=GSL2A@a%^lpRaC8GgV?OP5mL07D6vT{{(zVcWWqp@D!{|v1geA71w0B5)QkaZKr4nh(pY-Oko*EuzxWRG!vs5y9m&Dv1A zIy-@oWdBj>EpqTcOwn=S+Ux{Rol`iew=bL@LW__8;See|YfkNIi3CEm?ME*(a*)cf zx)R9D{v>ApFNpmA*eCEWGywdbl-g?mAUD0X_sY4qZCDD6Z)UZLz;T)j;VB;3MIQ64`Ao*O13 zOc)(m(m6GF3J9CG%_qUbV ztcaHP>wjl)`?_w?(EC`qH})bXGt&=KEfZrzz5j0GAZT?-wM>q2_P*_^TlDFDx!ju@ zB7e8hLDy~QeWctQI}w?g>Cn{Cgcxq`+kd%HRb-4)ZoWRi?*5tke=pQqdQs@XQ|z&^ zu0-aWB|Nk~0OD;|+cZKS#w}D8dGuyjF}b@?lFyplT?jLV0>R{F)p4T;{qE!M6~>S~ zCv7T8ZG)?Og6EwC0v~;G4s!7U;i8uf3a|eWFVU6I-lff1|R*9Lrhc}VJ^&X>An)j-I1G6X0lZHB?cG9QRBK* ztvqA$W5NFI%i229}Y^XW>73mMiN3{DNu6R^J_!VNK3kWG|&+>#hf~? zx{d1s)0kS}L5-y?mjW9vYtc(K>_f<|ZU&S1TK{L-#aCt>A^#Qr+fSCrqVCqdA)-nqV9O0W! zEF>2MX^6=tVJ{&JM`=XV#XxV%Lxi&sy8Hs0 zeheDqS|AK`oyM3%ttzLh4SR*Sp+|!eJK)2N&vyw~y1(AyEwOb+DA7v@SD~=iDFYe~ zPPw}QxfIQz^{7kC6$>3GrPmo!s*e!Cu`nzvV+<5_g3*!ODcbl-_73|j_vef`=Gx>AV8BD`AbIzW_sSuI+iz%Z@EI34JB)fdVvT25z}|IGYt9>6julL?W8 z-6X>9L({zFQ{{DZ6Am~UulE!+)5i>>Z!Q>+t6X!9LAYmOmb0R~xc1CFf>}s9z;U$z zK948&C*-uv$pIv>7*vPV8U_AW#{>s>`-CWJj?T?BYAo(&fQM2uWZw%ue)9!2Pae@Z zOzRx{O1$=v&|dYIe7Zq(W_Y)tX}L?@_3i*}!!fl&WfAtRRCgrZt7^wA2W%hRE$H2F zb4qt&)Mo`JB^v7vPVp=G`!z6UjlCY3@u5~+y7J`tBPrigRi z{>o<0=jyEorrk-u#4IryWA1B7#>h+Otvs9FBfe$%TJzO=L8Im~ z!Ucl2fykJ|kY=T(;5_I&K9ve^uoA32rDR@DPWdVOzZWaB!q0ptH5iZh;_^%;OOg9? z$1%{c|I_RP$+-Oma}e>MkW6WR+)Z{rIvDPlwK|JB9Eh!dZ3 z|J6n8p>q2y;~RYsBh3>HM5Udss}~OC{=-HK(Sgy2<>mGrg{D7Q-KjG!&_>wd!|n^ zjNy(`!c=Hsh@I@(ma%SxDe5%(;a(QR+;Eyra8Hy?*0tXkO1$&*4S5kFWCr_nA!mk) z`Op8p6f(R0bZc4YXa(!>+Pf(2iIo0()_vZct&-m-!+~23>do8?sy}7>^ zg8f`C7F-PRsf=DTo#0`Ps&P6=fqQki>J=@&*BVQ7l)_1Xl&#H;GVrI?yi7L)9awv< zg@8yWkmm!1Kt0fINb{1~g2rpLWS5d3-l(#*x^7{^w36Het;H$qQ!Dvq5i=bH)~pgniJ0jk`WOZ4gi)31HTVc? zpeTG!vzZRs-a4t}Ush&vcYVmFPJ6!RnQ9#4&H3Cc4MlS!hYnRolg3i#D;;bD+aCpj&;q+# zL2Xfq;w(^aqjU&J6VQmWVze2Vq7N82JOsK!j7)y2oE+=Qy!%({~@gwV=L z2P-Zd&F%fR!0$?q7asXsK)?oa86A20Gy5OYi!;7~m9rL^ zVxIA>T96Gl8dvR|q-+5$-M*92SXW4AZ;0ZxbA(5Hv_WshM}Eb^$U=fW z0YRn~iT6o{UX_UKQCa3~Z9UY}`{GOi~Q*7P4hKPW_h zw3$&u0Fv)zQ-YUG1#M#jJj{BSAt96NK_Tr-#H^yfqNKLHuPiP`lcLHX;N_YAWa1Vy zkw9CeCV>K*mVd7!lgxv9#8yF&o3H$%z4)hxDXbLUmG7(ee(X%+xhycqI5%8!nPkdR zbX_a5BBch|#fz)@)(sl!`cTx|1MkYIt(^DP$P{q2$|B%2L4z`;u*Lk5Lm?;rP+J_I z>ze_njdQcpCi_V0ObOR)$!0#+I}@IF#L#J0X1>_3;fXZ9eBG;i7`vf{3UMwzDl|2E ztMo?LnNG&pHA8=pGN$f2D(-aowJJ?1;J@{k5)PHn0U9Rwih`|^N9h+Jyf7H!EKn$l z1rSrEUe$5I>8?dI#B`&9}qi=n4p6+&GOYPSyQP^LION1nShA~%{qkjG{U;B zY=DkzfDq5!rvTY&=)-eXe0$kv{dibPTo|OZmUqCepavp`jg1qd4mjeI5by~qpFsQp z1b}JY^#t>x>N^DrK_6-%Ncxw?woV~eg1el)9k!BX^28~=pr5@jkrD>hx-)5CVfu*tV_2XGac#-=Y61f_$DjoSWCHSx!f|C6Ql@N3av*zdyYRxtc8A7;Zp zVquw24l;v>$<^&E&%~Mw$7JvN1Iek%aR*ieFE!mB=o9oA!InUM(XD%h zb2-L{JR!7Hp-@~n1bvy-5>pn-)XNc=u$Qz#qRq*!1(yu*teLj2jwaMw1=-X@kKKL( zAR4mnA4r+T-Py+7p+NQFz&>t=+taHYTfLX4a+S1?{_gQ|&g_kkJ;mQvHB z!>?*`#Jx8?4awMm%p#uK%V)=cp^ZvC!n8XvLlAw~%>sCjq&Pu!rRXLNAAvetQuUmw zYhmsi_8jkM@}DA&G2udM{bX*MuG8t_-4^4u zCIPf$hJc$VKQO0(D;)wK+t&`_cdM=@knNd5qgS3uz6>3{Z4y8a(1rJdDU9U zXcn?anQqd(8W8Ktx(M$uL{*||1Tel}0QiF2Ks&eeol2uKKA^P}lcfuF*3?{wW`r%G5r~Z?-|v{|;=AUicirShPlW}4hGBUYM&!u1 zd1tEy*%_zo1a9;;)Rk<|fd<_=a&v1cG>SXPh3a0VZwXu!jJasG{RGP>Rk~R_+7P$|z9XuGp?lH?B_Gs<~kg%ak!ehiRb7 z;_vlzzw~zE^^|K`t5^af^lf{w?|N;AJ}jUv<_E9ez>e>Cj9h@V81R})ohhT)>&QKA zDgIs*vTkY=9Uwe4q4S`Tvn>Qf${glYI$Gz(QGxyXHD z``TE0^kLqU{*^h4`xP#SU~O@#78U!euD4}~TTqX_zTO=tzR&H`>6>kF6qPQA%C5KF z1H;3wuQ!5W*H5RhapJ-MEr%(YQIE!4@17Cd=casGQMvv_PuT@>Rh%WT-o~1lA*p&F zvDVvk>iqp!Y!zgsk8VYAuMf>M**22W-3VTUv0zWjzqE1+IB>;6jm1z$m4p)$4GwX~ z7Ha>}fKyiN4J*^zdTeYH#YJc{2(IUm!p3J&2g*4$p44F2y4`mV46J?5rD))hnrk!I z7bNG@4aT=`_iZnbS;z+twK&u}K+R50^(nEifx{+~`?p zrD_z=6IMP}CR$utF`05`12@BOVl4L)+o2&pqBpeZb1z(HqS%bGxf~Wr_B6vT85oxZ2MVmfYmMzhQp9lS4f@GrtlXzr4Jz3)l#Fh4BJCNAZ#geOae%GR{UYpGL zx7TtM9m=vJOif;}CNKk8_M-I>>oK^h`-FcGFM|D<`(2 zLMEgXMm@Z%^0n~YLWcw8fz&t^rS==i_(zAjJ&(1YRDA#3qx#!Uf1+>ty=T*JJ8`U> z5d-qb`xC4IkKE_An)a+A4(YXs9B8I71M^YI6I&O3=7NX!jGDgzr)yG%Fo3 zsI)%@@BPE;D~rll;$OdGlN}P)^4QXS z=(m}k7UACFaiea&<8$bWc%?Dp+gnN z%YQF-x6>{BgDpbUI|kDAqOV53HRfHTFR*Wtu<|@f&V!-kVl~R&38bR%Fc&~?r@sVK zdm&58Nh$tn~VWJfZWjt5C9}HKawfjAIArHy1k0UBNOC@ z+&F5Y7U`m_+dYu|lLLm6Pw=u^AD()Ix8X}QBphVd%|PL5n!RyzA-7LmTU>d(3NkUN zwx#U>Wd=q|xG`niSQ?74#lh9^mLG{Dh;5TRxrK(!} zPHt+|qOI*u$itsdjDJNQ{)A%u_o&1@^6}rH68Ct=e~(JsBOm`ADshi@{5$IKrzGRw zQHMVz8UK(;Y3FZB{FlvKMX*&wc4DQ{kOC5|xh(rI{anN~JMdnp7KjNn?U zQ38OPyB0v<5KN!|a$VLay)R(yBE{GM&?gsv&x@v?7lrryvIhUM zHt+KRNVT*Q8w6IJx6mj6-T24>DLP0Z$^KF*A?e7eF3P=6x=)x-AG)sBCQ_y$ z{3wD}D&eK@s5G>a0e7}@U(_nW!(TYSpEW1~s*<5q#$eLuT7{^<(NzImm9!;AC5~=5 zR1_<1sX79q8Vwb_$z61B1XgFm%eu2M0>kZ=MCPmjC|$)1xcPZ`zSWpOx+AA{>&e*I z$y-obrpvD2oM5(OQK}L6f>TVhph<~rgEUG6oNqpCs76>(WuvG3hvhFBW+c)Vkbz8K zd9xNV&X(Te-!KgIL3VvZcKKrpuQ;D7@3O$8%V73oPX=DVd`urlL7#qAR$RuKKL;My z4t}Q8Q6Yhp@i1$W^4iC5E04zS<^*mH0G)La@C^h;WH_gISh*n21Uw;cmUG7#DryQ@-9(!&7ryN?e>q_9^h0;$k=&^lo43|g4b2V1d$vt z{JRNjP5f-Tgw_{el@O*PyhCV3s%)Q{2dmq-Xicz+W4ha39QS+oouz+;tBK1SJ5-@y zSaZh3C)?X`2(P`|i5g2BPPMB!Q?AhD?daqay4=}^j~FTJd5XLF{;c_F9`XNlOZ^EW z^M7ip`b=DC_{xVkR@dX#wH{PFd;Ri)#pO>|o{PV9Io=a@rQ+F5t>YG#%dWrlE=G-s zmivb+Tj!05x@=xvwzjJ=aoVICsk>5RGK7yEuETg2_u<(q{0qH{OU9hb{UevH^T(WB zHqDo<9cl)hHn~RXjB5sm@a4mGE#B^ZCU%C;w(+(al1FL5*S61WeS5z6?fK|OA=ZyV_&Dk7IOvrmIQ*<` zZ^GY?Ek3Pj)xulHk1AQ;-mETKCRSGrdbvgqme&2FK`N?1XI4t3NN@-l-t*cIg+0rMj@lqz){OF>=BEa4&+ks<@O!Em<0+!`Li@y`dn z;vJf6c(U-fBjE84eLs$#1$hCDh6mS?W92a)Jz5g*NShvXdL7pk*ibC}^q9&Vuwlf#Bde2y)qa%>n?UK9<11 zNum%E9|!66iid!zQpSh~jHgFcqKJtP4?f9I(IbP)-8@1!qkc$60`WB$#Bz1?vjlcu zxkX%pOZi2hPs- zAOKbyFmpuxSopUwV5YKc1`G{YJspLi z5W{WpuLmWwl?P^3hBlO4 zqIO4jpJm9bDVz+BE1Jy6-AE(5qW#dszN%@grti119SaFnyWf;~bxdf$>J^@KQR2GO zgX}mbp9df?3F10^7G5>UjlCUDwHawn~ish zbvedWo@rZ6{QMuAx8qmJ?0;x_OiOJYD0!HEw%4B={9oDpJ@ri6?^Lw!+f&+y{(jN@ zzFoK^LZswxmrHFvNpF5AeX;l6yRf#wS=;{iFI`#Nd|BK6cG(b+ljzJ=UF|0IZZ-&CsdnkNQ>oYPIz3vQ98mHrW+UO>l6Fb@H~$RwRmvS4=5 zbkp3cxCBq~x-UfH2#Y3-{3w1{fbfw_*Eb+-kW($Rds@niAo)j5>4p7THW*a!^u(Db z4kq|+8<0~HpI<aGs;o0 z;&zV)oDX2B7r-MGntgBz_K|UX8f`{-R;SAIg^NUWEloPKO;Qr6Bg-jThcmKh6#e>a zxjZDP)=eAv=od;rciydIy0Qmy$R^nneL^xzPm^1Q8UCZ;Z8)&id%}psOMf?)K*O=C zk7E+s&P1;@qSv}@{Tglk79tlOxL?()>sKTe&g?jWc1d!ZeXDyv5J3QO{1h3ELt0!P zx1Rwmi|j&fssT_7_5T^IA87 z>KdrNHwNrsM>|PJJJL@Zl%F=#6uyOXMBV2k}#BBU|n1h@2Z4o|AKX=3g%U zaI#;Mlyt9R;NL=5z^I9{=I1J1&91#_nym-RG1vFTgLWV5mzeG*uO-0J%~JB!YvTde z=jyFCU}*^O${)Ka)PRbQOXPK=3gimNOO$mKZelllH>qo}8krhWHwi!uWX5*dHc1am z4{1+`-fun08p#@>WwK?I4pKLgzcnG>QNBZkfPwZ<^n`EI^x*V(2Dttw8n{(;e%UEA zgeNP)8!D z>97)?Pt$5OGLx)zFue#>zF<1!MVA zpfC5&j@)oEh&6?gq<<4?T_K+)^ol|bO|TWkbg}@Myg!-zzXmq^E4C=xAZcn>IwUdC zp}a$8qDyiQPfHo?=pR=-y+V6@l7Q3H?^F5rw|_&5J={^i8Tc2AGyuRI{M*bl5B04@ zsl8J*^5&X^u&3tYZ*mSl72it~v0E3J2w6(=Zw`A`HQnGodfQ%#tr`GW+ygxStxKq_ zu1*_|px6rsV?f(1fS~y=*!{EY8o&Y2O$&$`{x&xSLIWc2zco3{LB%*bZwxzcNcM=I zkJwKyItSD|0L=Jr;Z3HUD7y^^fDr@m!yuqmYbQa0OW&e6@*4avVfiz)=A_w}@Lx&= zKpqMZP)GeO0t1jM{5!+-uY!!}`5aoPWs+O~2kh9LUzV%{oDEWElX>hTz-U~p0<_0N ztOXB}t@`H;Amgqw)a>R3AkDr^@{DaA@)Y~aGN1p$uEB@wAhcJ`{E}CGLI#SbFB7p!<*+h4CdjCdx?8EX?vJQZGi?*2p ziiY-7WdOTHw$AXkz3DG!I-oS?FZ}|1FYt}_bIdaVH^RWz%a6~?zoY&_PpW-iPC|7* zABuH-KVD>cKLDekzAlTuZc=o8K0ZPL6QHKLKNLQ0=>SvymbycHUqX35-m7`P9`ERU zt9;yHzV-;854yc{Z~r|R>g6ltoVR=K`NsEar&;$CFazqn^leY~^Y$+L>&)8fYY*^g zjGyz_P(GeX8i2r|gJR+^NzTL%gtxYdI zBE+_9>kDd#OmuO5QaXu%fiNt&vpI21KISjP-3?CyaEuU|k=mq18IRs5hZGl19_36)Fq9GEm%m3oB6QRk2NoU~m!>Yt z&Q@C%BZMARqHYx2iFS*=3z=v|&aa0tAG&iy57uVM1eO{Mj6@J~70jtjV65c=SjvTgk9oEru+S|NozpIIUU%`dv z^acLs)`_TGFkb-!0U?0_0dWD|ZDVU>tZQv+;8=B(DfHB-*Z#4)~( zqCŦV4_%7bT2Y1bvCJIo80MM0J{2u8>ImkxJi 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 diff --git a/package.json b/package.json index e2c927e9..c1c9bef0 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@transcend-io/handlebars-utils": "^1.1.0", "@transcend-io/internationalization": "^1.6.0", "@transcend-io/persisted-state": "^1.0.4", - "@transcend-io/privacy-types": "^4.103.0", + "@transcend-io/privacy-types": "^4.105.0", "@transcend-io/secret-value": "^1.2.0", "@transcend-io/type-utils": "^1.5.0", "bluebird": "^3.7.2", diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index 1116a817..e5f03406 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -12,7 +12,11 @@ import { import { OneTrustPullResource } from './enums'; import { mapSeries, map } from 'bluebird'; import uniq from 'lodash/uniq'; -import { OneTrustGetRiskResponseCodec } from './oneTrust/codecs'; +import { + OneTrustAssessmentQuestion, + OneTrustAssessmentSection, + OneTrustGetRiskResponse, +} from '@transcend-io/privacy-types'; /** * Pull configuration from OneTrust down locally to disk @@ -28,6 +32,7 @@ async function main(): Promise { parseCliPullOtArguments(); try { + // TODO: move to helper function if (resource === OneTrustPullResource.Assessments) { // use the hostname and auth token to instantiate a client to talk to OneTrust const oneTrust = createOneTrustGotInstance({ hostname, auth }); @@ -48,10 +53,10 @@ async function main(): Promise { }); // enrich assessments with risk information - let riskDetails: OneTrustGetRiskResponseCodec[] = []; + let riskDetails: OneTrustGetRiskResponse[] = []; const riskIds = uniq( - assessmentDetails.sections.flatMap((s) => - s.questions.flatMap((q) => + assessmentDetails.sections.flatMap((s: OneTrustAssessmentSection) => + s.questions.flatMap((q: OneTrustAssessmentQuestion) => (q.risks ?? []).flatMap((r) => r.riskId), ), ), @@ -64,7 +69,7 @@ async function main(): Promise { ); riskDetails = await map( riskIds, - (riskId) => getOneTrustRisk({ oneTrust, riskId }), + (riskId) => getOneTrustRisk({ oneTrust, riskId: riskId as string }), { concurrency: 5, }, diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts index 33e90c01..2595bc42 100644 --- a/src/helpers/enrichWithDefault.ts +++ b/src/helpers/enrichWithDefault.ts @@ -1,17 +1,19 @@ import * as t from 'io-ts'; import { - OneTrustAssessmentNestedQuestionCodec, - OneTrustAssessmentQuestionOptionCodec, - OneTrustAssessmentQuestionResponseCodec, - OneTrustAssessmentQuestionResponsesCodec, - OneTrustAssessmentResponsesCodec, - OneTrustAssessmentSectionHeaderRiskStatisticsCodec, - OneTrustAssessmentSectionSubmittedByCodec, - OneTrustCombinedAssessmentCodec, - OneTrustEnrichedAssessmentSectionCodec, - OneTrustEnrichedRiskCodec, - OneTrustEnrichedRisksCodec, - OneTrustPrimaryEntityDetailsCodec, + OneTrustAssessmentNestedQuestion, + OneTrustAssessmentQuestionOption, + OneTrustAssessmentQuestionResponses, + OneTrustAssessmentResponses, + OneTrustAssessmentSectionHeaderRiskStatistics, + OneTrustAssessmentSectionSubmittedBy, + OneTrustPrimaryEntityDetails, +} from '@transcend-io/privacy-types'; + +import { + OneTrustCombinedAssessment, + OneTrustEnrichedAssessmentSection, + OneTrustEnrichedRisk, + OneTrustEnrichedRisks, } from '../oneTrust/codecs'; import { createDefaultCodec } from './createDefaultCodec'; @@ -19,48 +21,48 @@ import { createDefaultCodec } from './createDefaultCodec'; const enrichQuestionWithDefault = ({ options, ...rest -}: OneTrustAssessmentNestedQuestionCodec): OneTrustAssessmentNestedQuestionCodec => ({ +}: OneTrustAssessmentNestedQuestion): OneTrustAssessmentNestedQuestion => ({ options: options === null || options.length === 0 - ? createDefaultCodec(t.array(OneTrustAssessmentQuestionOptionCodec)) + ? createDefaultCodec(t.array(OneTrustAssessmentQuestionOption)) : options, ...rest, }); // TODO: test the shit out of this const enrichQuestionResponsesWithDefault = ( - questionResponses: OneTrustAssessmentQuestionResponsesCodec, -): OneTrustAssessmentQuestionResponsesCodec => + questionResponses: OneTrustAssessmentQuestionResponses, +): OneTrustAssessmentQuestionResponses => questionResponses.length === 0 - ? createDefaultCodec(t.array(OneTrustAssessmentQuestionResponseCodec)) + ? createDefaultCodec(OneTrustAssessmentQuestionResponses) : questionResponses.map((questionResponse) => ({ ...questionResponse, responses: questionResponse.responses.length === 0 - ? createDefaultCodec(OneTrustAssessmentResponsesCodec) + ? createDefaultCodec(OneTrustAssessmentResponses) : questionResponse.responses, })); // TODO: test the shit out of this const enrichRisksWithDefault = ( - risks: OneTrustEnrichedRisksCodec, -): OneTrustEnrichedRisksCodec => + risks: OneTrustEnrichedRisks, +): OneTrustEnrichedRisks => risks === null || risks.length === 0 - ? createDefaultCodec(t.array(OneTrustEnrichedRiskCodec)) + ? createDefaultCodec(t.array(OneTrustEnrichedRisk)) : risks; // TODO: test the shit out of this const enrichRiskStatisticsWithDefault = ( - riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec, -): OneTrustAssessmentSectionHeaderRiskStatisticsCodec => + riskStatistics: OneTrustAssessmentSectionHeaderRiskStatistics, +): OneTrustAssessmentSectionHeaderRiskStatistics => riskStatistics === null - ? createDefaultCodec(OneTrustAssessmentSectionHeaderRiskStatisticsCodec) + ? createDefaultCodec(OneTrustAssessmentSectionHeaderRiskStatistics) : riskStatistics; // TODO: test the shit out of this const enrichSectionsWithDefault = ( - sections: OneTrustEnrichedAssessmentSectionCodec[], -): OneTrustEnrichedAssessmentSectionCodec[] => + sections: OneTrustEnrichedAssessmentSection[], +): OneTrustEnrichedAssessmentSection[] => sections.map((s) => ({ ...s, header: { @@ -77,20 +79,20 @@ const enrichSectionsWithDefault = ( })), submittedBy: s.submittedBy === null - ? createDefaultCodec(OneTrustAssessmentSectionSubmittedByCodec) + ? createDefaultCodec(OneTrustAssessmentSectionSubmittedBy) : s.submittedBy, })); const enrichPrimaryEntityDetailsWithDefault = ( - primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec, -): OneTrustPrimaryEntityDetailsCodec => + primaryEntityDetails: OneTrustPrimaryEntityDetails, +): OneTrustPrimaryEntityDetails => primaryEntityDetails.length === 0 - ? createDefaultCodec(OneTrustPrimaryEntityDetailsCodec) + ? createDefaultCodec(OneTrustPrimaryEntityDetails) : primaryEntityDetails; export const enrichCombinedAssessmentWithDefaults = ( - combinedAssessment: OneTrustCombinedAssessmentCodec, -): OneTrustCombinedAssessmentCodec => ({ + combinedAssessment: OneTrustCombinedAssessment, +): OneTrustCombinedAssessment => ({ ...combinedAssessment, primaryEntityDetails: enrichPrimaryEntityDetailsWithDefault( combinedAssessment.primaryEntityDetails, diff --git a/src/helpers/tests/createDefaultCodec.test.ts b/src/helpers/tests/createDefaultCodec.test.ts index 9962e046..821a5694 100644 --- a/src/helpers/tests/createDefaultCodec.test.ts +++ b/src/helpers/tests/createDefaultCodec.test.ts @@ -3,6 +3,11 @@ import chai, { expect } from 'chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { createDefaultCodec } from '../createDefaultCodec'; +import { + OneTrustCombinedAssessment, + OneTrustCombinedAssessmentCodec, +} from '../../oneTrust/codecs'; +import { flattenOneTrustAssessment } from '../../oneTrust/flattenOneTrustAssessment'; chai.use(deepEqualInAnyOrder); diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 441ab96f..7be22bd4 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -1,1261 +1,170 @@ -/* eslint-disable max-lines */ +import { + OneTrustAssessment, + OneTrustAssessmentNestedQuestion, + OneTrustAssessmentQuestion, + OneTrustAssessmentQuestionRisk, + OneTrustAssessmentSection, + OneTrustAssessmentSectionHeader, + OneTrustGetAssessmentResponse, + OneTrustGetRiskResponse, +} from '@transcend-io/privacy-types'; import * as t from 'io-ts'; -// TODO: move to privacy-types - -export const OneTrustAssessmentCodec = t.type({ - /** ID of the assessment. */ - assessmentId: t.string, - /** Date that the assessment was created. */ - createDt: t.string, - /** Overall risk score without considering existing controls. */ - inherentRiskScore: t.union([t.number, t.null]), - /** Date and time that the assessment was last updated. */ - lastUpdated: t.string, - /** Name of the assessment. */ - name: t.string, - /** Number assigned to the assessment. */ - number: t.number, - /** Number of risks that are open on the assessment. */ - openRiskCount: t.number, - /** Name of the organization group assigned to the assessment. */ - orgGroupName: t.string, - /** Details about the inventory record which is the primary record of the assessment. */ - primaryInventoryDetails: t.union([ - t.type({ - /** GUID of the inventory record. */ - primaryInventoryId: t.string, - /** Name of the inventory record. */ - primaryInventoryName: t.string, - /** Integer ID of the inventory record. */ - primaryInventoryNumber: t.number, - }), - t.null, - ]), - /** Overall risk score after considering existing controls. */ - residualRiskScore: t.union([t.number, t.null]), - /** Result of the assessment. NOTE: This field will be deprecated soon. Please reference the 'resultName' field instead. */ - result: t.union([ - t.literal('Approved'), - t.literal('AutoClosed'), - t.literal('Rejected'), - t.string, - t.null, - ]), - /** ID of the result. */ - resultId: t.union([t.string, t.null]), - /** Name of the result. */ - resultName: t.union([ - t.literal('Approved - Remediation required'), - t.literal('Approved'), - t.literal('Rejected'), - t.literal('Assessment suspended - On Hold'), - t.string, - t.null, - ]), - /** State of the assessment. */ - state: t.union([t.literal('ARCHIVE'), t.literal('ACTIVE')]), - /** Status of the assessment. */ - status: t.union([ - t.literal('Not Started'), - t.literal('In Progress'), - t.literal('Under Review'), - t.literal('Completed'), - t.null, - ]), - /** Name of the tag attached to the assessment. */ - tags: t.array(t.string), - /** The desired risk score. */ - targetRiskScore: t.union([t.number, t.null]), - /** ID used to launch an assessment using a specific version of a template. */ - templateId: t.string, - /** Name of the template that is being used on the assessment. */ - templateName: t.string, - /** ID used to launch an assessment using the latest published version of a template. */ - templateRootVersionId: t.string, -}); - -/** Type override */ -export type OneTrustAssessmentCodec = t.TypeOf; - -// ref: https://developer.onetrust.com/onetrust/reference/getallassessmentbasicdetailsusingget -export const OneTrustGetListOfAssessmentsResponseCodec = t.partial({ - /** The list of assessments in the current page. */ - content: t.array(OneTrustAssessmentCodec), - /** Details about the pages being fetched */ - page: t.type({ - /** Page number of the results list (0…N). */ - number: t.number, - /** Number of records per page (0…N). */ - size: t.number, - /** Total number of elements. */ - totalElements: t.number, - /** Total number of pages. */ - totalPages: t.number, - }), -}); - -/** Type override */ -export type OneTrustGetListOfAssessmentsResponseCodec = t.TypeOf< - typeof OneTrustGetListOfAssessmentsResponseCodec ->; - -export const OneTrustAssessmentQuestionOptionCodec = t.intersection([ - t.partial({ - /** The translationIdentifier */ - translationIdentifier: t.string, - }), - t.type({ - /** ID of the option. */ - id: t.string, - /** Name of the option. */ - option: t.string, - /** The key of the option */ - optionKey: t.union([t.string, t.null]), - /** The hint */ - hint: t.union([t.string, t.null]), - /** The hint key */ - hintKey: t.union([t.string, t.null]), - /** The score */ - score: t.union([t.number, t.null]), - /** If the option was pre-selected */ - preSelectedOption: t.boolean, - /** Order in which the option appears. */ - sequence: t.union([t.number, t.null]), - /** Attribute for which the option is available. */ - attributes: t.union([t.string, t.null]), - /** Type of option. */ - optionType: t.union([ - t.literal('NOT_SURE'), - t.literal('NOT_APPLICABLE'), - t.literal('OTHERS'), - t.literal('DEFAULT'), - ]), - }), -]); - -/** Type override */ -export type OneTrustAssessmentQuestionOptionCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionOptionCodec ->; - -export const OneTrustAssessmentQuestionRiskCodec = t.intersection([ - t.type({ - /** ID of the question for which the risk was flagged. */ - questionId: t.string, - /** ID of the flagged risk. */ - riskId: t.string, - }), - t.partial({ - /** Level of risk flagged on the question. */ - level: t.union([t.number, t.null]), - /** Score of risk flagged on the question. */ - score: t.union([t.number, t.null]), - /** Probability of risk flagged on the question. */ - probability: t.union([t.number, t.undefined]), - /** Impact Level of risk flagged on the question. */ - impactLevel: t.union([t.number, t.undefined]), - }), -]); - -/** Type override */ -export type OneTrustAssessmentQuestionRiskCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionRiskCodec ->; - -export const OneTrustAssessmentQuestionRisksCodec = t.union([ - t.array(OneTrustAssessmentQuestionRiskCodec), - t.null, -]); -/** Type override */ -export type OneTrustAssessmentQuestionRisksCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionRisksCodec ->; - -export const OneTrustAssessmentResponsesCodec = t.array( - t.intersection([ - t.partial({ - /** The maturityScale */ - maturityScale: t.union([t.string, t.null]), - /** The effectivenessScale */ - effectivenessScale: t.union([t.string, t.null]), - /** The parentAssessmentDetailId */ - parentAssessmentDetailId: t.union([t.string, t.null]), - /** The display label */ - displayLabel: t.string, - /** The type of the parent question */ - parentQuestionType: t.string, - /** The ID of the parent response */ - parentResponseId: t.string, - /** Whether it's local version */ - isLocalVersion: t.string, - /** Whether relationshipDisplayInformation */ - relationshipDisplayInformation: t.union([t.string, t.null]), - /** The lock reason */ - lockReason: t.union([t.string, t.null]), - }), - t.type({ - /** The controlResponse */ - controlResponse: t.union([t.string, t.null]), - /** The relationshipResponseDetails */ - relationshipResponseDetails: t.array(t.string), - /** The textRedacted */ - textRedacted: t.boolean, - /** The responseMap */ - responseMap: t.object, - /** ID of the response. */ - responseId: t.string, - /** Content of the response. */ - response: t.union([t.string, t.null]), - /** The response key */ - responseKey: t.union([t.string, t.null]), - /** The response key */ - contractResponse: t.union([t.string, t.null]), - /** Type of response. */ - type: t.union([ - t.literal('NOT_SURE'), - t.literal('JUSTIFICATION'), - t.literal('NOT_APPLICABLE'), - t.literal('DEFAULT'), - t.literal('OTHERS'), - ]), - /** Source from which the assessment is launched. */ - responseSourceType: t.union([ - t.literal('LAUNCH_FROM_INVENTORY'), - t.literal('FORCE_CREATED_SOURCE'), - t.null, - ]), - /** Error associated with the response. */ - errorCode: t.union([ - t.literal('ATTRIBUTE_DISABLED'), - t.literal('ATTRIBUTE_OPTION_DISABLED'), - t.literal('INVENTORY_NOT_EXISTS'), - t.literal('RELATED_INVENTORY_ATTRIBUTE_DISABLED'), - t.literal('DATA_ELEMENT_NOT_EXISTS'), - t.literal('DUPLICATE_INVENTORY'), - t.null, - ]), - /** Indicates whether the response is valid. */ - valid: t.boolean, - /** The data subject */ - dataSubject: t.union([ - t.type({ - /** The ID of the data subject */ - id: t.union([t.string, t.null]), - /** The ID of the data subject */ - name: t.union([t.string, t.null]), - /** The nameKey of the data category */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - /** The data category */ - dataCategory: t.union([ - t.type({ - /** The ID of the data category */ - id: t.union([t.string, t.null]), - /** The name of the data category */ - name: t.union([t.string, t.null]), - /** The nameKey of the data category */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - /** The data element */ - dataElement: t.union([ - t.type({ - /** The ID of the data element */ - id: t.union([t.string, t.null]), - /** The name of the data element */ - name: t.union([t.string, t.null]), - /** The name key of the data element */ - nameKey: t.union([t.string, t.null]), - }), - t.null, - ]), - }), - ]), -); -/** Type override */ -export type OneTrustAssessmentResponsesCodec = t.TypeOf< - typeof OneTrustAssessmentResponsesCodec ->; - -export const OneTrustAssessmentQuestionResponseCodec = t.type({ - /** The responses */ - responses: OneTrustAssessmentResponsesCodec, - /** Justification comments for the given response. */ - justification: t.union([t.string, t.null]), -}); - -/** Type override */ -export type OneTrustAssessmentQuestionResponseCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionResponseCodec ->; - -export const OneTrustAssessmentQuestionResponsesCodec = t.array( - OneTrustAssessmentQuestionResponseCodec, -); -/** Type override */ -export type OneTrustAssessmentQuestionResponsesCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionResponsesCodec ->; - -export const OneTrustAssessmentNestedQuestionCodec = t.type({ - /** ID of the question. */ - id: t.string, - /** ID of the root version of the question. */ - rootVersionId: t.string, - /** Order in which the question appears in the assessment. */ - sequence: t.number, - /** Type of question in the assessment. */ - questionType: t.union([ - t.literal('TEXTBOX'), - t.literal('MULTICHOICE'), - t.literal('YESNO'), - t.literal('DATE'), - t.literal('STATEMENT'), - t.literal('INVENTORY'), - t.literal('ATTRIBUTE'), - t.literal('PERSONAL_DATA'), - t.literal('ENGAGEMENT'), - t.literal('ASSESS_CONTROL'), - t.null, - ]), - /** Indicates whether a response to the question is required. */ - required: t.boolean, - /** Data element attributes that are directly updated by the question. */ - attributes: t.string, - /** Short, descriptive name for the question. */ - friendlyName: t.union([t.string, t.null]), - /** Description of the question. */ - description: t.union([t.string, t.null]), - /** Tooltip text within a hint for the question. */ - hint: t.union([t.string, t.null]), - /** ID of the parent question. */ - parentQuestionId: t.union([t.string, t.null]), - /** Indicates whether the response to the question is prepopulated. */ - prePopulateResponse: t.boolean, - /** Indicates whether the assessment is linked to inventory records. */ - linkAssessmentToInventory: t.boolean, - /** The question options */ - options: t.union([t.array(OneTrustAssessmentQuestionOptionCodec), t.null]), - /** Indicates whether the question is valid. */ - valid: t.boolean, - /** Type of question in the assessment. */ - type: t.union([ - t.literal('TEXTBOX'), - t.literal('MULTICHOICE'), - t.literal('YESNO'), - t.literal('DATE'), - t.literal('STATEMENT'), - t.literal('INVENTORY'), - t.literal('ATTRIBUTE'), - t.literal('PERSONAL_DATA'), - t.literal('ENGAGEMENT'), - t.literal('ASSESS_CONTROL'), - ]), - /** Whether the response can be multi select */ - allowMultiSelect: t.boolean, - /** The text of a question. */ - content: t.string, - /** Indicates whether justification comments are required for the question. */ - requireJustification: t.boolean, -}); - -/** Type override */ -export type OneTrustAssessmentNestedQuestionCodec = t.TypeOf< - typeof OneTrustAssessmentNestedQuestionCodec ->; - -// TODO: do not add to privacy-types -/** OneTrustAssessmentNestedQuestionCodec without nested options */ -export const OneTrustAssessmentNestedQuestionFlatCodec = t.type({ - id: OneTrustAssessmentNestedQuestionCodec.props.id, - rootVersionId: OneTrustAssessmentNestedQuestionCodec.props.rootVersionId, - sequence: OneTrustAssessmentNestedQuestionCodec.props.sequence, - questionType: OneTrustAssessmentNestedQuestionCodec.props.questionType, - required: OneTrustAssessmentNestedQuestionCodec.props.required, - attributes: OneTrustAssessmentNestedQuestionCodec.props.attributes, - friendlyName: OneTrustAssessmentNestedQuestionCodec.props.friendlyName, - description: OneTrustAssessmentNestedQuestionCodec.props.description, - hint: OneTrustAssessmentNestedQuestionCodec.props.hint, - parentQuestionId: - OneTrustAssessmentNestedQuestionCodec.props.parentQuestionId, +/** OneTrustAssessmentNestedQuestion without nested options */ +export const OneTrustAssessmentNestedQuestionFlat = t.type({ + id: OneTrustAssessmentNestedQuestion.props.id, + rootVersionId: OneTrustAssessmentNestedQuestion.props.rootVersionId, + sequence: OneTrustAssessmentNestedQuestion.props.sequence, + questionType: OneTrustAssessmentNestedQuestion.props.questionType, + required: OneTrustAssessmentNestedQuestion.props.required, + attributes: OneTrustAssessmentNestedQuestion.props.attributes, + friendlyName: OneTrustAssessmentNestedQuestion.props.friendlyName, + description: OneTrustAssessmentNestedQuestion.props.description, + hint: OneTrustAssessmentNestedQuestion.props.hint, + parentQuestionId: OneTrustAssessmentNestedQuestion.props.parentQuestionId, prePopulateResponse: - OneTrustAssessmentNestedQuestionCodec.props.prePopulateResponse, + OneTrustAssessmentNestedQuestion.props.prePopulateResponse, linkAssessmentToInventory: - OneTrustAssessmentNestedQuestionCodec.props.linkAssessmentToInventory, - valid: OneTrustAssessmentNestedQuestionCodec.props.valid, - type: OneTrustAssessmentNestedQuestionCodec.props.type, - allowMultiSelect: - OneTrustAssessmentNestedQuestionCodec.props.allowMultiSelect, - content: OneTrustAssessmentNestedQuestionCodec.props.content, + OneTrustAssessmentNestedQuestion.props.linkAssessmentToInventory, + valid: OneTrustAssessmentNestedQuestion.props.valid, + type: OneTrustAssessmentNestedQuestion.props.type, + allowMultiSelect: OneTrustAssessmentNestedQuestion.props.allowMultiSelect, + content: OneTrustAssessmentNestedQuestion.props.content, requireJustification: - OneTrustAssessmentNestedQuestionCodec.props.requireJustification, + OneTrustAssessmentNestedQuestion.props.requireJustification, }); /** Type override */ -export type OneTrustAssessmentNestedQuestionFlatCodec = t.TypeOf< - typeof OneTrustAssessmentNestedQuestionFlatCodec +export type OneTrustAssessmentNestedQuestionFlat = t.TypeOf< + typeof OneTrustAssessmentNestedQuestionFlat >; -export const OneTrustAssessmentQuestionCodec = t.intersection([ - t.type({ - /** The question */ - question: OneTrustAssessmentNestedQuestionCodec, - /** Indicates whether the question is hidden on the assessment. */ - hidden: t.boolean, - /** Reason for locking the question in the assessment. */ - lockReason: t.union([ - t.literal('LAUNCH_FROM_INVENTORY'), - t.literal('FORCE_CREATION_LOCK'), - t.null, - ]), - /** The copy errors */ - copyErrors: t.union([t.string, t.null]), - /** Indicates whether navigation rules are enabled for the question. */ - hasNavigationRules: t.boolean, - /** The responses to this question */ - questionResponses: t.array(OneTrustAssessmentQuestionResponseCodec), - /** The risks associated with this question */ - risks: t.union([t.array(OneTrustAssessmentQuestionRiskCodec), t.null]), - /** List of IDs associated with the question root requests. */ - rootRequestInformationIds: t.array(t.string), - /** Number of attachments added to the question. */ - totalAttachments: t.number, - /** IDs of the attachment(s) added to the question. */ - attachmentIds: t.array(t.string), - /** The canReopenWithAllowEditOption */ - canReopenWithAllowEditOption: t.boolean, - /** The riskCreationAllowed */ - riskCreationAllowed: t.boolean, - /** The riskDeletionPopupAllowed */ - riskDeletionPopupAllowed: t.boolean, - /** The allowMaturityScaleOnQuestions */ - allowMaturityScaleOnQuestions: t.boolean, - /** The questionAssociations */ - questionAssociations: t.union([t.string, t.null]), - /** The issues */ - issues: t.union([t.string, t.null]), - /** The responseEditableWhileUnderReview */ - responseEditableWhileUnderReview: t.boolean, - }), - t.partial({ - /** The businessKeyReference */ - businessKeyReference: t.union([t.string, t.null]), - /** The topic */ - topic: t.union([t.string, t.null]), - /** The questionLaws */ - questionLaws: t.array(t.string), - /** The attachmentRequired */ - attachmentRequired: t.boolean, - /** The responseFilter */ - responseFilter: t.union([t.string, t.null]), - /** The linkAssessmentToResponseEntity */ - linkAssessmentToResponseEntity: t.boolean, - /** The translationIdentifier */ - translationIdentifier: t.string, - /** The readOnly */ - readOnly: t.boolean, - /** The schema */ - schema: t.union([t.string, t.null]), - /** The attributeId */ - attributeId: t.string, - /** Whether it is a vendor question */ - vendorQuestion: t.boolean, - /** Whether the question was seeded */ - seeded: t.boolean, - /** Whether the question allows justification */ - allowJustification: t.boolean, - /** Whether it refers to an asset question */ - assetQuestion: t.boolean, - /** Whether it refers to an entity question */ - entityQuestion: t.boolean, - /** Whether it is a paquestion */ - paquestion: t.boolean, - /** The inventoryTypeEnum */ - inventoryTypeEnum: t.union([t.string, t.null]), - /** Whether it is a forceOther */ - forceOther: t.boolean, - /** Whether it is a isParentQuestionMultiSelect */ - isParentQuestionMultiSelect: t.boolean, - }), -]); - -/** Type override */ -export type OneTrustAssessmentQuestionCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionCodec ->; - -// TODO: do not add to privacy types -// The OneTrustAssessmentQuestionCodec without nested properties -export const OneTrustAssessmentQuestionFlatCodec = t.type({ - hidden: OneTrustAssessmentQuestionCodec.types[0].props.hidden, - lockReason: OneTrustAssessmentQuestionCodec.types[0].props.lockReason, - copyErrors: OneTrustAssessmentQuestionCodec.types[0].props.copyErrors, +// The OneTrustAssessmentQuestion without nested properties +export const OneTrustAssessmentQuestionFlat = t.type({ + hidden: OneTrustAssessmentQuestion.types[0].props.hidden, + lockReason: OneTrustAssessmentQuestion.types[0].props.lockReason, + copyErrors: OneTrustAssessmentQuestion.types[0].props.copyErrors, hasNavigationRules: - OneTrustAssessmentQuestionCodec.types[0].props.hasNavigationRules, + OneTrustAssessmentQuestion.types[0].props.hasNavigationRules, rootRequestInformationIds: - OneTrustAssessmentQuestionCodec.types[0].props.rootRequestInformationIds, - totalAttachments: - OneTrustAssessmentQuestionCodec.types[0].props.totalAttachments, - attachmentIds: OneTrustAssessmentQuestionCodec.types[0].props.attachmentIds, + OneTrustAssessmentQuestion.types[0].props.rootRequestInformationIds, + totalAttachments: OneTrustAssessmentQuestion.types[0].props.totalAttachments, + attachmentIds: OneTrustAssessmentQuestion.types[0].props.attachmentIds, }); /** Type override */ -export type OneTrustAssessmentQuestionFlatCodec = t.TypeOf< - typeof OneTrustAssessmentQuestionFlatCodec +export type OneTrustAssessmentQuestionFlat = t.TypeOf< + typeof OneTrustAssessmentQuestionFlat >; -export const OneTrustAssessmentSectionHeaderRiskStatisticsCodec = t.union([ - t.type({ - /** Maximum level of risk in the section. */ - maxRiskLevel: t.union([t.number, t.null]), - /** Number of risks in the section. */ - riskCount: t.union([t.number, t.null]), - /** ID of the section in the assessment. */ - sectionId: t.union([t.string, t.null]), - }), - t.null, -]); - -/** Type override */ -export type OneTrustAssessmentSectionHeaderRiskStatisticsCodec = t.TypeOf< - typeof OneTrustAssessmentSectionHeaderRiskStatisticsCodec ->; - -export const OneTrustAssessmentSectionHeaderCodec = t.intersection([ - t.type({ - /** ID of the section in the assessment. */ - sectionId: t.string, - /** Name of the section. */ - name: t.string, - /** The status of the section */ - status: t.union([t.string, t.null]), - /** The openNMIQuestionIds */ - openNMIQuestionIds: t.union([t.string, t.null]), - /** Description of the section header. */ - description: t.union([t.string, t.null]), - /** Sequence of the section within the form */ - sequence: t.number, - /** Indicates whether the section is hidden in the assessment. */ - hidden: t.boolean, - /** IDs of invalid questions in the section. */ - invalidQuestionIds: t.array(t.string), - /** IDs of required but unanswered questions in the section. */ - requiredUnansweredQuestionIds: t.array(t.string), - /** IDs of required questions in the section. */ - requiredQuestionIds: t.array(t.string), - /** IDs of unanswered questions in the section. */ - unansweredQuestionIds: t.array(t.string), - /** IDs of effectiveness questions in the section. */ - effectivenessQuestionIds: t.array(t.string), - /** The risk statistics */ - riskStatistics: OneTrustAssessmentSectionHeaderRiskStatisticsCodec, - /** Whether the section was submitted */ - submitted: t.boolean, - }), - t.partial({ - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), -]); - -/** Type override */ -export type OneTrustAssessmentSectionHeaderCodec = t.TypeOf< - typeof OneTrustAssessmentSectionHeaderCodec ->; - -// TODO: do not add to privacy-types -/** The OneTrustAssessmentSectionHeaderCodec without nested riskStatistics */ -export const OneTrustAssessmentSectionFlatHeaderCodec = t.type({ - sectionId: OneTrustAssessmentSectionHeaderCodec.types[0].props.sectionId, - name: OneTrustAssessmentSectionHeaderCodec.types[0].props.name, - description: OneTrustAssessmentSectionHeaderCodec.types[0].props.description, - sequence: OneTrustAssessmentSectionHeaderCodec.types[0].props.sequence, - hidden: OneTrustAssessmentSectionHeaderCodec.types[0].props.hidden, +/** The OneTrustAssessmentSectionHeader without nested riskStatistics */ +export const OneTrustAssessmentSectionFlatHeader = t.type({ + sectionId: OneTrustAssessmentSectionHeader.types[0].props.sectionId, + name: OneTrustAssessmentSectionHeader.types[0].props.name, + description: OneTrustAssessmentSectionHeader.types[0].props.description, + sequence: OneTrustAssessmentSectionHeader.types[0].props.sequence, + hidden: OneTrustAssessmentSectionHeader.types[0].props.hidden, invalidQuestionIds: - OneTrustAssessmentSectionHeaderCodec.types[0].props.invalidQuestionIds, + OneTrustAssessmentSectionHeader.types[0].props.invalidQuestionIds, requiredUnansweredQuestionIds: - OneTrustAssessmentSectionHeaderCodec.types[0].props + OneTrustAssessmentSectionHeader.types[0].props .requiredUnansweredQuestionIds, requiredQuestionIds: - OneTrustAssessmentSectionHeaderCodec.types[0].props.requiredQuestionIds, + OneTrustAssessmentSectionHeader.types[0].props.requiredQuestionIds, unansweredQuestionIds: - OneTrustAssessmentSectionHeaderCodec.types[0].props.unansweredQuestionIds, + OneTrustAssessmentSectionHeader.types[0].props.unansweredQuestionIds, effectivenessQuestionIds: - OneTrustAssessmentSectionHeaderCodec.types[0].props - .effectivenessQuestionIds, - submitted: OneTrustAssessmentSectionHeaderCodec.types[0].props.submitted, -}); -/** Type override */ -export type OneTrustAssessmentSectionFlatHeaderCodec = t.TypeOf< - typeof OneTrustAssessmentSectionFlatHeaderCodec ->; - -export const OneTrustAssessmentSectionSubmittedByCodec = t.union([ - t.intersection([ - t.type({ - /** The ID of the user who submitted the section */ - id: t.string, - /** THe name or email of the user who submitted the section */ - name: t.string, - }), - t.partial({ - /** The name key */ - nameKey: t.union([t.string, t.null]), - }), - ]), - t.null, -]); - -/** Type override */ -export type OneTrustAssessmentSectionSubmittedByCodec = t.TypeOf< - typeof OneTrustAssessmentSectionSubmittedByCodec ->; - -export const OneTrustAssessmentSectionCodec = t.type({ - /** The Assessment section header */ - header: OneTrustAssessmentSectionHeaderCodec, - /** The questions within the section */ - questions: t.array(OneTrustAssessmentQuestionCodec), - /** Indicates whether navigation rules are enabled for the question. */ - hasNavigationRules: t.boolean, - /** Who submitted the section */ - submittedBy: OneTrustAssessmentSectionSubmittedByCodec, - /** Date of the submission */ - submittedDt: t.union([t.string, t.null]), - /** Name of the section. */ - name: t.string, - /** Indicates whether navigation rules are enabled for the question. */ - hidden: t.boolean, - /** Indicates whether the section is valid. */ - valid: t.boolean, - /** ID of the section in an assessment. */ - sectionId: t.string, - /** Sequence of the section within the form */ - sequence: t.number, - /** Whether the section was submitted */ - submitted: t.boolean, - /** Descriptions of the section. */ - description: t.union([t.string, t.null]), -}); - -/** Type override */ -export type OneTrustAssessmentSectionCodec = t.TypeOf< - typeof OneTrustAssessmentSectionCodec ->; - -// TODO: do not move to privacy-types -/** The OneTrustAssessmentSectionCodec type without header or questions */ -export const OneTrustFlatAssessmentSectionCodec = t.type({ - hasNavigationRules: OneTrustAssessmentSectionCodec.props.hasNavigationRules, - submittedBy: OneTrustAssessmentSectionCodec.props.submittedBy, - submittedDt: OneTrustAssessmentSectionCodec.props.submittedDt, - name: OneTrustAssessmentSectionCodec.props.name, - hidden: OneTrustAssessmentSectionCodec.props.hidden, - valid: OneTrustAssessmentSectionCodec.props.valid, - sectionId: OneTrustAssessmentSectionCodec.props.sectionId, - sequence: OneTrustAssessmentSectionCodec.props.sequence, - submitted: OneTrustAssessmentSectionCodec.props.submitted, - description: OneTrustAssessmentSectionCodec.props.description, -}); - -/** Type override */ -export type OneTrustFlatAssessmentSectionCodec = t.TypeOf< - typeof OneTrustFlatAssessmentSectionCodec ->; - -export const OneTrustApproverCodec = t.type({ - /** ID of the user assigned as an approver. */ - id: t.string, - /** ID of the workflow stage */ - workflowStageId: t.string, - /** Name of the user assigned as an approver. */ - name: t.string, - /** More details about the approver */ - approver: t.type({ - /** ID of the user assigned as an approver. */ - id: t.string, - /** Full name of the user assigned as an approver. */ - fullName: t.string, - /** Email of the user assigned as an approver. */ - email: t.union([t.string, t.null]), - /** Whether the user assigned as an approver was deleted. */ - deleted: t.boolean, - /** The assignee type */ - assigneeType: t.union([t.string, t.null]), - }), - /** Assessment approval status. */ - approvalState: t.union([ - t.literal('OPEN'), - t.literal('APPROVED'), - t.literal('REJECTED'), - ]), - /** Date and time at which the assessment was approved. */ - approvedOn: t.union([t.string, t.null]), - /** ID of the assessment result. */ - resultId: t.union([t.string, t.null]), - /** Name of the assessment result. */ - resultName: t.union([ - t.literal('Approved - Remediation required'), - t.literal('Approved'), - t.literal('Rejected'), - t.literal('Assessment suspended - On Hold'), - t.string, - t.null, - ]), - /** Name key of the assessment result. */ - resultNameKey: t.union([t.string, t.null]), -}); - -/** Type override */ -export type OneTrustApproverCodec = t.TypeOf; - -export const OneTrustAssessmentStatusCodec = t.union([ - t.literal('NOT_STARTED'), - t.literal('IN_PROGRESS'), - t.literal('UNDER_REVIEW'), - t.literal('COMPLETED'), -]); -/** Type override */ -export type OneTrustAssessmentStatusCodec = t.TypeOf< - typeof OneTrustAssessmentStatusCodec ->; - -export const OneTrustPrimaryEntityDetailsCodec = t.array( - t.type({ - /** Unique ID for the primary record. */ - id: t.string, - /** Name of the primary record. */ - name: t.string, - /** The number associated with the primary record. */ - number: t.number, - /** Name and number of the primary record. */ - displayName: t.string, - /** The relationshipResponseDetails */ - relationshipResponseDetails: t.union([t.string, t.null]), - /** The entity business key */ - entityBusinessKey: t.union([t.string, t.null]), - }), -); -/** Type override */ -export type OneTrustPrimaryEntityDetailsCodec = t.TypeOf< - typeof OneTrustPrimaryEntityDetailsCodec ->; - -// ref: https://developer.onetrust.com/onetrust/reference/exportassessmentusingget -export const OneTrustGetAssessmentResponseCodec = t.type({ - /** List of users assigned as approvers of the assessment. */ - approvers: t.array(OneTrustApproverCodec), - /** ID of an assessment. */ - assessmentId: t.string, - /** Number assigned to an assessment. */ - assessmentNumber: t.number, - /** Date and time at which the assessment was completed. */ - completedOn: t.union([t.string, t.null]), - /** Status of the assessment. */ - status: OneTrustAssessmentStatusCodec, - /** Creator of the Assessment */ - createdBy: t.type({ - /** The ID of the creator */ - id: t.string, - /** The name of the creator */ - name: t.string, - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), - /** Date and time at which the assessment was created. */ - createdDT: t.string, - /** Date and time by which the assessment must be completed. */ - deadline: t.union([t.string, t.null]), - /** Description of the assessment. */ - description: t.union([t.string, t.null]), - /** Overall inherent risk score without considering the existing controls. */ - inherentRiskScore: t.union([t.number, t.null]), - /** Date and time at which the assessment was last updated. */ - lastUpdated: t.string, - /** Number of risks captured on the assessment with a low risk level. */ - lowRisk: t.number, - /** Number of risks captured on the assessment with a medium risk level. */ - mediumRisk: t.number, - /** Number of risks captured on the assessment with a high risk level. */ - highRisk: t.number, - /** Name of the assessment. */ - name: t.string, - /** Number of open risks that have not been addressed. */ - openRiskCount: t.number, - /** The organization group */ - orgGroup: t.intersection([ - t.type({ - /** The ID of the organization group */ - id: t.string, - /** The name of the organization group */ - name: t.string, - }), - t.partial({ - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), - ]), - /** The primary record */ - primaryEntityDetails: OneTrustPrimaryEntityDetailsCodec, - /** Type of inventory record designated as the primary record. */ - primaryRecordType: t.union([ - t.literal('ASSETS'), - t.literal('PROCESSING_ACTIVITY'), - t.literal('VENDORS'), - t.literal('ENTITIES'), - t.literal('ASSESS_CONTROL'), - t.literal('ENGAGEMENT'), - t.literal('projects'), - t.null, - ]), - /** Overall risk score after considering existing controls. */ - residualRiskScore: t.union([t.number, t.null]), - /** The respondent */ - respondent: t.type({ - /** The ID of the respondent */ - id: t.string, - /** The name or email of the respondent */ - name: t.string, - }), - /** The respondents */ - respondents: t.array( - t.type({ - /** The ID of the respondent */ - id: t.string, - /** The name or email of the respondent */ - name: t.string, - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), - ), - /** Result of the assessment. */ - result: t.union([t.string, t.null]), - /** ID of the result. */ - resultId: t.union([t.string, t.null]), - /** Name of the result. */ - resultName: t.union([ - t.literal('Approved - Remediation required'), - t.literal('Approved'), - t.literal('Rejected'), - t.literal('Assessment suspended - On Hold'), - t.string, - t.null, - ]), - /** Risk level of the assessment. */ - riskLevel: t.union([ - t.literal('None'), - t.literal('Low'), - t.literal('Medium'), - t.literal('High'), - t.literal('Very High'), - ]), - /** List of sections in the assessment. */ - sections: t.array(OneTrustAssessmentSectionCodec), - /** Date and time at which the assessment was submitted. */ - submittedOn: t.union([t.string, t.null]), - /** List of tags associated with the assessment. */ - tags: t.array(t.string), - /** The desired target risk score. */ - targetRiskScore: t.union([t.number, t.null]), - /** The template */ - template: t.type({ - /** The ID of the template */ - id: t.string, - /** The name of the template */ - name: t.string, - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), - /** Number of total risks on the assessment. */ - totalRiskCount: t.number, - /** Number of very high risks on the assessment. */ - veryHighRisk: t.number, - /** Welcome text if any in the assessment. */ - welcomeText: t.union([t.string, t.null]), + OneTrustAssessmentSectionHeader.types[0].props.effectivenessQuestionIds, + submitted: OneTrustAssessmentSectionHeader.types[0].props.submitted, }); - /** Type override */ -export type OneTrustGetAssessmentResponseCodec = t.TypeOf< - typeof OneTrustGetAssessmentResponseCodec +export type OneTrustAssessmentSectionFlatHeader = t.TypeOf< + typeof OneTrustAssessmentSectionFlatHeader >; -const EntityTypeCodec = t.type({ - /** Indicates whether entity type is eligible for linking/relating with risk or not */ - eligibleForEntityLink: t.boolean, - /** Indicates whether the entity type is enabled or not. */ - enabled: t.boolean, - /** Entity Type ID. This can be Assets, Entities, PIA, Engagement, Custom Object GUID in form of String. */ - id: t.string, - /** Entity Type Name. */ - label: t.string, - /** Name of the module. */ - moduleName: t.union([t.string, t.null]), - /** Indicates whether this type can be risk type or not in Risk */ - riskType: t.boolean, - /** For Base Entity Type Seeded is true and false for Custom Object/Entity Types by default. */ - seeded: t.boolean, - /** Indicates whether this type can be source type or not in Risk */ - sourceType: t.boolean, - /** Translation Key of Entity Type ID. */ - translationKey: t.string, +/** The OneTrustAssessmentSection type without header or questions */ +export const OneTrustFlatAssessmentSection = t.type({ + hasNavigationRules: OneTrustAssessmentSection.props.hasNavigationRules, + submittedBy: OneTrustAssessmentSection.props.submittedBy, + submittedDt: OneTrustAssessmentSection.props.submittedDt, + name: OneTrustAssessmentSection.props.name, + hidden: OneTrustAssessmentSection.props.hidden, + valid: OneTrustAssessmentSection.props.valid, + sectionId: OneTrustAssessmentSection.props.sectionId, + sequence: OneTrustAssessmentSection.props.sequence, + submitted: OneTrustAssessmentSection.props.submitted, + description: OneTrustAssessmentSection.props.description, }); -const RiskLevelCodec = t.type({ - /** Risk Impact Level name. */ - impactLevel: t.union([t.string, t.null]), - /** Risk Impact level ID. */ - impactLevelId: t.union([t.number, t.null]), - /** Risk Level Name. */ - level: t.union([t.string, t.null]), - /** Risk Level ID. */ - levelId: t.union([t.number, t.null]), - /** Risk Probability Level Name. */ - probabilityLevel: t.union([t.string, t.null]), - /** Risk Probability Level ID. */ - probabilityLevelId: t.union([t.number, t.null]), - /** Risk Score. */ - riskScore: t.union([t.number, t.null]), -}); - -export const OneTrustRiskCategories = t.array( - t.intersection([ - t.partial({ - seeded: t.boolean, - }), - t.type({ - /** Identifier for Risk Category. */ - id: t.string, - /** Risk Category Name. */ - name: t.string, - /** Risk Category Name Key value for translation. */ - nameKey: t.string, - }), - ]), -); /** Type override */ -export type OneTrustRiskCategories = t.TypeOf; - -// ref: https://developer.onetrust.com/onetrust/reference/getriskusingget -export const OneTrustGetRiskResponseCodec = t.type({ - /** List of associated inventories to the risk. */ - associatedInventories: t.array( - t.type({ - /** ID of the Inventory. */ - inventoryId: t.string, - /** Name of the Inventory. */ - inventoryName: t.string, - /** Type of the Inventory. */ - inventoryType: t.union([ - t.literal('ASSETS'), - t.literal('PROCESSING_ACTIVITIES'), - t.literal('VENDORS'), - t.literal('ENTITIES'), - t.null, - ]), - /** ID of the Inventory's Organization. */ - organizationId: t.union([t.string, t.null]), - /** The source type */ - sourceType: EntityTypeCodec, - }), - ), - /** The attribute values associated with the risk */ - attributeValues: t.object, - /** List of categories. */ - categories: OneTrustRiskCategories, - /** List of Control Identifiers. */ - controlsIdentifier: t.array(t.string), - /** Risk created time. */ - createdUTCDateTime: t.union([t.string, t.null]), - /** Risk Creation Type. */ - creationType: t.union([t.string, t.null]), - /** Date when the risk is closed. */ - dateClosed: t.union([t.string, t.null]), - /** Deadline date for the risk. */ - deadline: t.union([t.string, t.null]), - /** Risk delete type. */ - deleteType: t.union([t.literal('SOFT'), t.null]), - /** Risk description. */ - description: t.union([t.string, t.null]), - /** ID of the risk. */ - id: t.string, - /** Residual impact level name. */ - impactLevel: t.union([t.string, t.null]), - /** Residual impact level ID. */ - impactLevelId: t.union([t.number, t.null]), - /** The inherent risk level */ - inherentRiskLevel: RiskLevelCodec, - /** The risk justification */ - justification: t.union([t.string, t.null]), - /** Residual level display name. */ - levelDisplayName: t.union([t.string, t.null]), - /** Residual level ID. */ - levelId: t.union([t.number, t.null]), - /** Risk mitigated date. */ - mitigatedDate: t.union([t.string, t.null]), - /** Risk Mitigation details. */ - mitigation: t.union([t.string, t.null]), - /** Short Name for a Risk. */ - name: t.union([t.string, t.null]), - /** Integer risk identifier. */ - number: t.number, - /** The organization group */ - orgGroup: t.intersection([ - t.type({ - /** The ID of the organization group */ - id: t.string, - /** The name of the organization group */ - name: t.string, - }), - t.partial({ - /** The name key of the template */ - nameKey: t.union([t.string, t.null]), - }), - ]), - /** The previous risk state */ - previousState: t.union([ - t.literal('IDENTIFIED'), - t.literal('RECOMMENDATION_ADDED'), - t.literal('RECOMMENDATION_SENT'), - t.literal('REMEDIATION_PROPOSED'), - t.literal('EXCEPTION_REQUESTED'), - t.literal('REDUCED'), - t.literal('RETAINED'), - t.literal('ARCHIVED_IN_VERSION'), - t.null, - ]), - /** Residual probability level. */ - probabilityLevel: t.union([t.string, t.null]), - /** Residual probability level ID. */ - probabilityLevelId: t.union([t.number, t.null]), - /** Risk Recommendation. */ - recommendation: t.union([t.string, t.null]), - /** Proposed remediation. */ - remediationProposal: t.union([t.string, t.null]), - /** Deadline reminder days. */ - reminderDays: t.union([t.number, t.null]), - /** Risk exception request. */ - requestedException: t.union([t.string, t.null]), - /** Risk Result. */ - result: t.union([ - t.literal('Accepted'), - t.literal('Avoided'), - t.literal('Reduced'), - t.literal('Rejected'), - t.literal('Transferred'), - t.literal('Ignored'), - t.null, - ]), - /** Risk approvers name csv. */ - riskApprovers: t.union([t.string, t.null]), - /** Risk approvers ID. */ - riskApproversId: t.array(t.string), - /** List of risk owners ID. */ - riskOwnersId: t.union([t.array(t.string), t.null]), - /** Risk owners name csv. */ - riskOwnersName: t.union([t.string, t.null]), - /** Risk score. */ - riskScore: t.union([t.number, t.null]), - /** The risk source type */ - riskSourceType: EntityTypeCodec, - /** The risk type */ - riskType: EntityTypeCodec, - /** For Auto risk, rule Id reference. */ - ruleRootVersionId: t.union([t.string, t.null]), - /** The risk source */ - source: t.type({ - /** Additional information about the Source Entity */ - additionalAttributes: t.object, - /** Source Entity ID. */ - id: t.string, - /** Source Entity Name. */ - name: t.string, - /** The risk source type */ - sourceType: EntityTypeCodec, - /** Source Entity Type. */ - type: t.union([ - t.literal('PIA'), - t.literal('RA'), - t.literal('GRA'), - t.literal('INVENTORY'), - t.literal('INCIDENT'), - t.literal('GENERIC'), - ]), - }), - /** The risk stage */ - stage: t.intersection([ - t.partial({ - /** The currentStageApprovers */ - currentStageApprovers: t.array(t.string), - /** The badgeColor */ - badgeColor: t.union([t.string, t.null]), - }), - t.type({ - /** ID of an entity. */ - id: t.string, - /** Name of an entity. */ - name: t.string, - /** Name Key of the entity for translation. */ - nameKey: t.string, - }), - ]), - /** The risk state */ - state: t.union([ - t.literal('IDENTIFIED'), - t.literal('RECOMMENDATION_ADDED'), - t.literal('RECOMMENDATION_SENT'), - t.literal('REMEDIATION_PROPOSED'), - t.literal('EXCEPTION_REQUESTED'), - t.literal('REDUCED'), - t.literal('RETAINED'), - t.literal('ARCHIVED_IN_VERSION'), - ]), - /** The target risk level */ - targetRiskLevel: RiskLevelCodec, - /** The risk threat */ - threat: t.union([ - t.type({ - /** Threat ID. */ - id: t.string, - /** Threat Identifier. */ - identifier: t.string, - /** Threat Name. */ - name: t.string, - }), - t.null, - ]), - /** Risk Treatment. */ - treatment: t.union([t.string, t.null]), - /** Risk Treatment status. */ - treatmentStatus: t.union([ - t.literal('InProgress'), - t.literal('UnderReview'), - t.literal('ExceptionRequested'), - t.literal('Approved'), - t.literal('ExceptionGranted'), - t.null, - ]), - /** Risk Type. */ - type: t.union([ - t.literal('ASSESSMENTS'), - t.literal('ASSETS'), - t.literal('PROCESSING_ACTIVITIES'), - t.literal('VENDORS'), - t.literal('ENTITIES'), - t.literal('INCIDENTS'), - t.literal('ENGAGEMENTS'), - t.null, - ]), - /** ID of an assessment. */ - typeRefIds: t.array(t.string), - /** List of vulnerabilities */ - vulnerabilities: t.union([ - t.array( - t.type({ - /** Vulnerability ID. */ - id: t.string, - /** Vulnerability Identifier. */ - identifier: t.string, - /** Vulnerability Name. */ - name: t.string, - }), - ), - t.null, - ]), - /** The risk workflow */ - workflow: t.type({ - /** ID of an entity. */ - id: t.string, - /** Name of an entity. */ - name: t.string, - }), -}); - -/** Type override */ -export type OneTrustGetRiskResponseCodec = t.TypeOf< - typeof OneTrustGetRiskResponseCodec +export type OneTrustFlatAssessmentSection = t.TypeOf< + typeof OneTrustFlatAssessmentSection >; -// TODO: do not move to privacy-types -export const OneTrustEnrichedRiskCodec = t.intersection([ - OneTrustAssessmentQuestionRiskCodec, +export const OneTrustEnrichedRisk = t.intersection([ + OneTrustAssessmentQuestionRisk, t.type({ - description: OneTrustGetRiskResponseCodec.props.description, - name: OneTrustGetRiskResponseCodec.props.name, - treatment: OneTrustGetRiskResponseCodec.props.treatment, - treatmentStatus: OneTrustGetRiskResponseCodec.props.treatmentStatus, - type: OneTrustGetRiskResponseCodec.props.type, - stage: OneTrustGetRiskResponseCodec.props.stage, - state: OneTrustGetRiskResponseCodec.props.state, - result: OneTrustGetRiskResponseCodec.props.result, - categories: OneTrustGetRiskResponseCodec.props.categories, + description: OneTrustGetRiskResponse.props.description, + name: OneTrustGetRiskResponse.props.name, + treatment: OneTrustGetRiskResponse.props.treatment, + treatmentStatus: OneTrustGetRiskResponse.props.treatmentStatus, + type: OneTrustGetRiskResponse.props.type, + stage: OneTrustGetRiskResponse.props.stage, + state: OneTrustGetRiskResponse.props.state, + result: OneTrustGetRiskResponse.props.result, + categories: OneTrustGetRiskResponse.props.categories, }), ]); /** Type override */ -export type OneTrustEnrichedRiskCodec = t.TypeOf< - typeof OneTrustEnrichedRiskCodec ->; +export type OneTrustEnrichedRisk = t.TypeOf; -// TODO: do not move to privacy-types -export const OneTrustEnrichedRisksCodec = t.union([ - t.array(OneTrustEnrichedRiskCodec), +export const OneTrustEnrichedRisks = t.union([ + t.array(OneTrustEnrichedRisk), t.null, ]); /** Type override */ -export type OneTrustEnrichedRisksCodec = t.TypeOf< - typeof OneTrustEnrichedRisksCodec ->; +export type OneTrustEnrichedRisks = t.TypeOf; -// TODO: do not add to privacy-types -export const OneTrustEnrichedAssessmentQuestionCodec = t.intersection([ +export const OneTrustEnrichedAssessmentQuestion = t.intersection([ t.type({ - ...OneTrustAssessmentQuestionCodec.types[0].props, - risks: OneTrustEnrichedRisksCodec, + ...OneTrustAssessmentQuestion.types[0].props, + risks: OneTrustEnrichedRisks, }), - t.partial({ ...OneTrustAssessmentQuestionCodec.types[1].props }), + t.partial({ ...OneTrustAssessmentQuestion.types[1].props }), ]); /** Type override */ -export type OneTrustEnrichedAssessmentQuestionCodec = t.TypeOf< - typeof OneTrustEnrichedAssessmentQuestionCodec +export type OneTrustEnrichedAssessmentQuestion = t.TypeOf< + typeof OneTrustEnrichedAssessmentQuestion >; -// TODO: do not add to privacy-types -export const OneTrustEnrichedAssessmentSectionCodec = t.type({ - ...OneTrustAssessmentSectionCodec.props, - questions: t.array(OneTrustEnrichedAssessmentQuestionCodec), +export const OneTrustEnrichedAssessmentSection = t.type({ + ...OneTrustAssessmentSection.props, + questions: t.array(OneTrustEnrichedAssessmentQuestion), }); /** Type override */ -export type OneTrustEnrichedAssessmentSectionCodec = t.TypeOf< - typeof OneTrustEnrichedAssessmentSectionCodec +export type OneTrustEnrichedAssessmentSection = t.TypeOf< + typeof OneTrustEnrichedAssessmentSection >; -// TODO: do not add to privacy-types -export const OneTrustEnrichedAssessmentResponseCodec = t.type({ - ...OneTrustGetAssessmentResponseCodec.props, - sections: t.array(OneTrustEnrichedAssessmentSectionCodec), +export const OneTrustEnrichedAssessmentResponse = t.type({ + ...OneTrustGetAssessmentResponse.props, + sections: t.array(OneTrustEnrichedAssessmentSection), }); /** Type override */ -export type OneTrustEnrichedAssessmentResponseCodec = t.TypeOf< - typeof OneTrustEnrichedAssessmentResponseCodec +export type OneTrustEnrichedAssessmentResponse = t.TypeOf< + typeof OneTrustEnrichedAssessmentResponse >; -// TODO: do not add to privacy-types // eslint-disable-next-line @typescript-eslint/no-unused-vars -const { status, ...OneTrustAssessmentCodecWithoutStatus } = - OneTrustAssessmentCodec.props; -export const OneTrustCombinedAssessmentCodec = t.intersection([ - t.type(OneTrustAssessmentCodecWithoutStatus), - OneTrustEnrichedAssessmentResponseCodec, +const { status, ...OneTrustAssessmentWithoutStatus } = OneTrustAssessment.props; +export const OneTrustCombinedAssessment = t.intersection([ + t.type(OneTrustAssessmentWithoutStatus), + OneTrustEnrichedAssessmentResponse, ]); /** Type override */ -export type OneTrustCombinedAssessmentCodec = t.TypeOf< - typeof OneTrustCombinedAssessmentCodec +export type OneTrustCombinedAssessment = t.TypeOf< + typeof OneTrustCombinedAssessment >; - -/* eslint-enable max-lines */ diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts index 3daa79e9..876bf284 100644 --- a/src/oneTrust/constants.ts +++ b/src/oneTrust/constants.ts @@ -1,14 +1,14 @@ import { createDefaultCodec } from '../helpers'; -import { OneTrustCombinedAssessmentCodec } from './codecs'; +import { OneTrustCombinedAssessment } from './codecs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; /** - * An object with default values of type OneTrustCombinedAssessmentCodec. It's very + * An object with default values of type OneTrustCombinedAssessment. It's very * valuable when converting assessments to CSV, as it contains all keys that * make up the CSV header in the expected order */ -const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustCombinedAssessmentCodec = - createDefaultCodec(OneTrustCombinedAssessmentCodec); +const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustCombinedAssessment = + createDefaultCodec(OneTrustCombinedAssessment); /** The header of the OneTrust ASsessment CSV file */ export const DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER = Object.keys( diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index 6fd45cbb..aa13edcc 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -1,19 +1,21 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/no-explicit-any */ +import { + OneTrustAssessmentNestedQuestion, + OneTrustAssessmentQuestionOption, + OneTrustAssessmentQuestionResponses, + OneTrustAssessmentSectionHeader, + OneTrustRiskCategories, +} from '@transcend-io/privacy-types'; import { enrichCombinedAssessmentWithDefaults, extractProperties, } from '../helpers'; import { - OneTrustCombinedAssessmentCodec, - OneTrustAssessmentNestedQuestionCodec, - OneTrustAssessmentQuestionOptionCodec, - OneTrustAssessmentQuestionResponseCodec, - OneTrustAssessmentSectionHeaderCodec, - OneTrustEnrichedAssessmentQuestionCodec, - OneTrustEnrichedAssessmentSectionCodec, - OneTrustEnrichedRiskCodec, - OneTrustRiskCategories, + OneTrustCombinedAssessment, + OneTrustEnrichedAssessmentQuestion, + OneTrustEnrichedAssessmentSection, + OneTrustEnrichedRisk, } from './codecs'; // import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; @@ -67,7 +69,7 @@ const aggregateObjects = ({ }; const flattenOneTrustNestedQuestionsOptions = ( - allOptions: (OneTrustAssessmentQuestionOptionCodec[] | null)[], + allOptions: (OneTrustAssessmentQuestionOption[] | null)[], prefix: string, ): any => { const allOptionsFlat = allOptions.map((options) => { @@ -79,7 +81,7 @@ const flattenOneTrustNestedQuestionsOptions = ( }; const flattenOneTrustNestedQuestions = ( - questions: OneTrustAssessmentNestedQuestionCodec[], + questions: OneTrustAssessmentNestedQuestion[], prefix: string, ): any => { // TODO: how do extract properties handle null @@ -97,7 +99,7 @@ const flattenOneTrustNestedQuestions = ( // flatten questionResponses of every question within a section const flattenOneTrustQuestionResponses = ( - allQuestionResponses: OneTrustAssessmentQuestionResponseCodec[][], + allQuestionResponses: OneTrustAssessmentQuestionResponses[], prefix: string, ): any => { const allQuestionResponsesFlat = allQuestionResponses.map( @@ -138,7 +140,7 @@ const flattenOneTrustRiskCategories = ( }; const flattenOneTrustRisks = ( - allRisks: (OneTrustEnrichedRiskCodec[] | null)[], + allRisks: (OneTrustEnrichedRisk[] | null)[], prefix: string, ): any => { // TODO: extract categories and other nested properties @@ -158,7 +160,7 @@ const flattenOneTrustRisks = ( }; const flattenOneTrustQuestions = ( - allSectionQuestions: OneTrustEnrichedAssessmentQuestionCodec[][], + allSectionQuestions: OneTrustEnrichedAssessmentQuestion[][], prefix: string, ): any => { const allSectionQuestionsFlat = allSectionQuestions.map( @@ -198,7 +200,7 @@ const flattenOneTrustQuestions = ( }; const flattenOneTrustSectionHeaders = ( - headers: OneTrustAssessmentSectionHeaderCodec[], + headers: OneTrustAssessmentSectionHeader[], prefix: string, ): any => { const { riskStatistics, rest: restHeaders } = extractProperties(headers, [ @@ -215,8 +217,9 @@ const flattenOneTrustSectionHeaders = ( }; }; +// TODO: update type to be const flattenOneTrustSections = ( - sections: OneTrustEnrichedAssessmentSectionCodec[], + sections: OneTrustEnrichedAssessmentSection[], prefix: string, ): any => { const { @@ -236,9 +239,10 @@ const flattenOneTrustSections = ( return { ...sectionsFlat, ...headersFlat, ...questionsFlat }; }; +// TODO: update type to be a Record export const flattenOneTrustAssessment = ( - combinedAssessment: OneTrustCombinedAssessmentCodec, -): any => { + combinedAssessment: OneTrustCombinedAssessment, +): Record => { // add default values to assessments const combinedAssessmentWithDefaults = enrichCombinedAssessmentWithDefaults(combinedAssessment); @@ -253,6 +257,7 @@ export const flattenOneTrustAssessment = ( ...rest } = combinedAssessmentWithDefaults; + // TODO: extract approver from approvers, otherwise it won't agree with the codec const flatApprovers = approvers.map((approver) => flattenObject(approver, 'approvers'), ); diff --git a/src/oneTrust/getListOfOneTrustAssessments.ts b/src/oneTrust/getListOfOneTrustAssessments.ts index 1b1eaa39..2ca9abea 100644 --- a/src/oneTrust/getListOfOneTrustAssessments.ts +++ b/src/oneTrust/getListOfOneTrustAssessments.ts @@ -1,10 +1,10 @@ import { Got } from 'got'; import { logger } from '../logger'; -import { - OneTrustAssessmentCodec, - OneTrustGetListOfAssessmentsResponseCodec, -} from './codecs'; import { decodeCodec } from '@transcend-io/type-utils'; +import { + OneTrustAssessment, + OneTrustGetListOfAssessmentsResponse, +} from '@transcend-io/privacy-types'; /** * Fetch a list of all assessments from the OneTrust client. @@ -18,12 +18,12 @@ export const getListOfOneTrustAssessments = async ({ }: { /** The OneTrust client instance */ oneTrust: Got; -}): Promise => { +}): Promise => { let currentPage = 0; let totalPages = 1; let totalElements = 0; - const allAssessments: OneTrustAssessmentCodec[] = []; + const allAssessments: OneTrustAssessment[] = []; logger.info('Getting list of all assessments from OneTrust...'); while (currentPage < totalPages) { @@ -33,7 +33,7 @@ export const getListOfOneTrustAssessments = async ({ ); const { page, content } = decodeCodec( - OneTrustGetListOfAssessmentsResponseCodec, + OneTrustGetListOfAssessmentsResponse, body, ); allAssessments.push(...(content ?? [])); diff --git a/src/oneTrust/getOneTrustAssessment.ts b/src/oneTrust/getOneTrustAssessment.ts index de99a6ba..6a02226e 100644 --- a/src/oneTrust/getOneTrustAssessment.ts +++ b/src/oneTrust/getOneTrustAssessment.ts @@ -1,6 +1,6 @@ import { Got } from 'got'; -import { OneTrustGetAssessmentResponseCodec } from './codecs'; import { decodeCodec } from '@transcend-io/type-utils'; +import { OneTrustGetAssessmentResponse } from '@transcend-io/privacy-types'; /** * Retrieve details about a particular assessment. @@ -17,10 +17,10 @@ export const getOneTrustAssessment = async ({ oneTrust: Got; /** The ID of the assessment to retrieve */ assessmentId: string; -}): Promise => { +}): Promise => { const { body } = await oneTrust.get( `api/assessment/v2/assessments/${assessmentId}/export?ExcludeSkippedQuestions=false`, ); - return decodeCodec(OneTrustGetAssessmentResponseCodec, body); + return decodeCodec(OneTrustGetAssessmentResponse, body); }; diff --git a/src/oneTrust/getOneTrustRisk.ts b/src/oneTrust/getOneTrustRisk.ts index a649caef..78184b02 100644 --- a/src/oneTrust/getOneTrustRisk.ts +++ b/src/oneTrust/getOneTrustRisk.ts @@ -1,6 +1,6 @@ import { Got } from 'got'; -import { OneTrustGetRiskResponseCodec } from './codecs'; import { decodeCodec } from '@transcend-io/type-utils'; +import { OneTrustGetRiskResponse } from '@transcend-io/privacy-types'; /** * Retrieve details about a particular risk. @@ -17,8 +17,8 @@ export const getOneTrustRisk = async ({ oneTrust: Got; /** The ID of the OneTrust risk to retrieve */ riskId: string; -}): Promise => { +}): Promise => { const { body } = await oneTrust.get(`api/risk/v2/risks/${riskId}`); - return decodeCodec(OneTrustGetRiskResponseCodec, body); + return decodeCodec(OneTrustGetRiskResponse, body); }; diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index 530f3f0c..a87d1ce5 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -2,14 +2,16 @@ import { logger } from '../logger'; import keyBy from 'lodash/keyBy'; import colors from 'colors'; import { OneTrustFileFormat } from '../enums'; -import { - OneTrustAssessmentCodec, - OneTrustGetAssessmentResponseCodec, - OneTrustGetRiskResponseCodec, -} from './codecs'; import fs from 'fs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; +import { decodeCodec } from '@transcend-io/type-utils'; +import { + OneTrustAssessment, + OneTrustAssessmentCsvRecord, + OneTrustGetAssessmentResponse, + OneTrustGetRiskResponse, +} from '@transcend-io/privacy-types'; /** * Write the assessment to disk at the specified file path. @@ -31,11 +33,11 @@ export const writeOneTrustAssessment = ({ /** The format of the output file */ fileFormat: OneTrustFileFormat; /** The basic assessment */ - assessment: OneTrustAssessmentCodec; + assessment: OneTrustAssessment; /** The assessment with details */ - assessmentDetails: OneTrustGetAssessmentResponseCodec; + assessmentDetails: OneTrustGetAssessmentResponse; /** The details of risks found within the assessment */ - riskDetails: OneTrustGetRiskResponseCodec[]; + riskDetails: OneTrustGetRiskResponse[]; /** The index of the assessment being written to the file */ index: number; /** The total amount of assessments that we will write */ @@ -122,18 +124,26 @@ export const writeOneTrustAssessment = ({ ...enrichedAssessment, }); - // transform the flat assessment to have all CSV keys in the expected order - const assessmentRow = DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map( - (header) => { + // comment + const flatAssessmentFull = Object.fromEntries( + DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map((header) => { const value = flatAssessment[header] ?? ''; - // Escape values containing commas or quotes - return typeof value === 'string' && + const escapedValue = + typeof value === 'string' && (value.includes(',') || value.includes('"')) - ? `"${value.replace(/"/g, '""')}"` - : value; - }, + ? `"${value.replace(/"/g, '""')}"` + : value; + return [header, escapedValue]; + }), ); + // TODO: import from privacy-types + // ensure the record has the expected type! + decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); + + // transform the flat assessment to have all CSV keys in the expected order + const assessmentRow = Object.values(flatAssessmentFull); + // append the rows to the file csvRows.push(`${assessmentRow.join(',')}\n`); fs.appendFileSync('./oneTrust.csv', csvRows.join('\n')); diff --git a/yarn.lock b/yarn.lock index 9e62ba0d..670dcbf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -515,7 +515,7 @@ __metadata: "@transcend-io/handlebars-utils": ^1.1.0 "@transcend-io/internationalization": ^1.6.0 "@transcend-io/persisted-state": ^1.0.4 - "@transcend-io/privacy-types": ^4.103.0 + "@transcend-io/privacy-types": ^4.105.0 "@transcend-io/secret-value": ^1.2.0 "@transcend-io/type-utils": ^1.5.0 "@types/bluebird": ^3.5.38 @@ -647,14 +647,14 @@ __metadata: languageName: node linkType: hard -"@transcend-io/privacy-types@npm:^4.103.0": - version: 4.103.0 - resolution: "@transcend-io/privacy-types@npm:4.103.0" +"@transcend-io/privacy-types@npm:^4.105.0": + version: 4.105.0 + resolution: "@transcend-io/privacy-types@npm:4.105.0" dependencies: "@transcend-io/type-utils": ^1.0.5 fp-ts: ^2.16.1 io-ts: ^2.2.21 - checksum: 4661368b34b36ae305243ac03dc931822db98b280cfdcb27831be57b67c9c96e09cbe6933139635f6b459d705dc573d36865bc2fb4ae68e59b4e53c39e08ab38 + checksum: fa0f2af076301aa376446b5bb5e0961b78d86d5ff95f1cc73df9e51f8d7214929fe02ac104a95f23ae6474fb539ffb8ad64a46d7a6d45cdb21e7dcf54decb83f languageName: node linkType: hard From ad32c0a95125f8d5b29459447de307e53b28036f Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 13 Jan 2025 23:24:44 +0000 Subject: [PATCH 40/79] update type-utils --- package.json | 2 +- src/helpers/createDefaultCodec.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c1c9bef0..5ba4cabf 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@transcend-io/persisted-state": "^1.0.4", "@transcend-io/privacy-types": "^4.105.0", "@transcend-io/secret-value": "^1.2.0", - "@transcend-io/type-utils": "^1.5.0", + "@transcend-io/type-utils": "^1.6.0", "bluebird": "^3.7.2", "cli-progress": "^3.11.2", "colors": "^1.4.0", diff --git a/src/helpers/createDefaultCodec.ts b/src/helpers/createDefaultCodec.ts index a7520812..7f5364fe 100644 --- a/src/helpers/createDefaultCodec.ts +++ b/src/helpers/createDefaultCodec.ts @@ -3,6 +3,7 @@ import * as t from 'io-ts'; /** + * // TODO: import from type-utils * Creates a default value for an io-ts codec. * * @param codec - the codec whose default we want to create From 2b05b5df2aaadfad6e8dde983ba67acd1924a2df Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 01:50:17 +0000 Subject: [PATCH 41/79] remove enrich helpers --- src/cli-pull-ot.ts | 118 +++++++++++----------- src/helpers/enrichWithDefault.ts | 101 ------------------ src/helpers/index.ts | 1 - src/oneTrust/flattenOneTrustAssessment.ts | 19 ++-- src/oneTrust/writeOneTrustAssessment.ts | 3 - 5 files changed, 65 insertions(+), 177 deletions(-) delete mode 100644 src/helpers/enrichWithDefault.ts diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts index e5f03406..68f28d57 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-pull-ot.ts @@ -28,75 +28,75 @@ import { * yarn cli-pull-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json */ async function main(): Promise { - const { file, fileFormat, hostname, auth, resource, debug } = + const { file, fileFormat, hostname, auth, resource } = parseCliPullOtArguments(); - try { - // TODO: move to helper function - if (resource === OneTrustPullResource.Assessments) { - // use the hostname and auth token to instantiate a client to talk to OneTrust - const oneTrust = createOneTrustGotInstance({ hostname, auth }); + // try { + // TODO: move to helper function + if (resource === OneTrustPullResource.Assessments) { + // use the hostname and auth token to instantiate a client to talk to OneTrust + const oneTrust = createOneTrustGotInstance({ hostname, auth }); - // fetch the list of all assessments in the OneTrust organization - const assessments = await getListOfOneTrustAssessments({ oneTrust }); + // fetch the list of all assessments in the OneTrust organization + const assessments = await getListOfOneTrustAssessments({ oneTrust }); - // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory - await mapSeries(assessments, async (assessment, index) => { - logger.info( - `Fetching details about assessment ${index + 1} of ${ - assessments.length - }...`, - ); - const assessmentDetails = await getOneTrustAssessment({ - oneTrust, - assessmentId: assessment.assessmentId, - }); + // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory + await mapSeries(assessments, async (assessment, index) => { + logger.info( + `Fetching details about assessment ${index + 1} of ${ + assessments.length + }...`, + ); + const assessmentDetails = await getOneTrustAssessment({ + oneTrust, + assessmentId: assessment.assessmentId, + }); - // enrich assessments with risk information - let riskDetails: OneTrustGetRiskResponse[] = []; - const riskIds = uniq( - assessmentDetails.sections.flatMap((s: OneTrustAssessmentSection) => - s.questions.flatMap((q: OneTrustAssessmentQuestion) => - (q.risks ?? []).flatMap((r) => r.riskId), - ), + // enrich assessments with risk information + let riskDetails: OneTrustGetRiskResponse[] = []; + const riskIds = uniq( + assessmentDetails.sections.flatMap((s: OneTrustAssessmentSection) => + s.questions.flatMap((q: OneTrustAssessmentQuestion) => + (q.risks ?? []).flatMap((r) => r.riskId), ), + ), + ); + if (riskIds.length > 0) { + logger.info( + `Fetching details about ${riskIds.length} risks for assessment ${ + index + 1 + } of ${assessments.length}...`, + ); + riskDetails = await map( + riskIds, + (riskId) => getOneTrustRisk({ oneTrust, riskId: riskId as string }), + { + concurrency: 5, + }, ); - if (riskIds.length > 0) { - logger.info( - `Fetching details about ${riskIds.length} risks for assessment ${ - index + 1 - } of ${assessments.length}...`, - ); - riskDetails = await map( - riskIds, - (riskId) => getOneTrustRisk({ oneTrust, riskId: riskId as string }), - { - concurrency: 5, - }, - ); - } + } - writeOneTrustAssessment({ - assessment, - assessmentDetails, - riskDetails, - index, - total: assessments.length, - file, - fileFormat, - }); + writeOneTrustAssessment({ + assessment, + assessmentDetails, + riskDetails, + index, + total: assessments.length, + file, + fileFormat, }); - } - } catch (err) { - logger.error( - colors.red( - `An error occurred pulling the resource ${resource} from OneTrust: ${ - debug ? err.stack : err.message - }`, - ), - ); - process.exit(1); + }); } + // } catch (err) { + // logger.error( + // colors.red( + // `An error occurred pulling the resource ${resource} from OneTrust: ${ + // debug ? err.stack : err.message + // }`, + // ), + // ); + // process.exit(1); + // } // Indicate success logger.info( diff --git a/src/helpers/enrichWithDefault.ts b/src/helpers/enrichWithDefault.ts deleted file mode 100644 index 2595bc42..00000000 --- a/src/helpers/enrichWithDefault.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as t from 'io-ts'; -import { - OneTrustAssessmentNestedQuestion, - OneTrustAssessmentQuestionOption, - OneTrustAssessmentQuestionResponses, - OneTrustAssessmentResponses, - OneTrustAssessmentSectionHeaderRiskStatistics, - OneTrustAssessmentSectionSubmittedBy, - OneTrustPrimaryEntityDetails, -} from '@transcend-io/privacy-types'; - -import { - OneTrustCombinedAssessment, - OneTrustEnrichedAssessmentSection, - OneTrustEnrichedRisk, - OneTrustEnrichedRisks, -} from '../oneTrust/codecs'; -import { createDefaultCodec } from './createDefaultCodec'; - -// TODO: test the shit out of this -const enrichQuestionWithDefault = ({ - options, - ...rest -}: OneTrustAssessmentNestedQuestion): OneTrustAssessmentNestedQuestion => ({ - options: - options === null || options.length === 0 - ? createDefaultCodec(t.array(OneTrustAssessmentQuestionOption)) - : options, - ...rest, -}); - -// TODO: test the shit out of this -const enrichQuestionResponsesWithDefault = ( - questionResponses: OneTrustAssessmentQuestionResponses, -): OneTrustAssessmentQuestionResponses => - questionResponses.length === 0 - ? createDefaultCodec(OneTrustAssessmentQuestionResponses) - : questionResponses.map((questionResponse) => ({ - ...questionResponse, - responses: - questionResponse.responses.length === 0 - ? createDefaultCodec(OneTrustAssessmentResponses) - : questionResponse.responses, - })); - -// TODO: test the shit out of this -const enrichRisksWithDefault = ( - risks: OneTrustEnrichedRisks, -): OneTrustEnrichedRisks => - risks === null || risks.length === 0 - ? createDefaultCodec(t.array(OneTrustEnrichedRisk)) - : risks; - -// TODO: test the shit out of this -const enrichRiskStatisticsWithDefault = ( - riskStatistics: OneTrustAssessmentSectionHeaderRiskStatistics, -): OneTrustAssessmentSectionHeaderRiskStatistics => - riskStatistics === null - ? createDefaultCodec(OneTrustAssessmentSectionHeaderRiskStatistics) - : riskStatistics; - -// TODO: test the shit out of this -const enrichSectionsWithDefault = ( - sections: OneTrustEnrichedAssessmentSection[], -): OneTrustEnrichedAssessmentSection[] => - sections.map((s) => ({ - ...s, - header: { - ...s.header, - riskStatistics: enrichRiskStatisticsWithDefault(s.header.riskStatistics), - }, - questions: s.questions.map((q) => ({ - ...q, - question: enrichQuestionWithDefault(q.question), - questionResponses: enrichQuestionResponsesWithDefault( - q.questionResponses, - ), - risks: enrichRisksWithDefault(q.risks), - })), - submittedBy: - s.submittedBy === null - ? createDefaultCodec(OneTrustAssessmentSectionSubmittedBy) - : s.submittedBy, - })); - -const enrichPrimaryEntityDetailsWithDefault = ( - primaryEntityDetails: OneTrustPrimaryEntityDetails, -): OneTrustPrimaryEntityDetails => - primaryEntityDetails.length === 0 - ? createDefaultCodec(OneTrustPrimaryEntityDetails) - : primaryEntityDetails; - -export const enrichCombinedAssessmentWithDefaults = ( - combinedAssessment: OneTrustCombinedAssessment, -): OneTrustCombinedAssessment => ({ - ...combinedAssessment, - primaryEntityDetails: enrichPrimaryEntityDetailsWithDefault( - combinedAssessment.primaryEntityDetails, - ), - sections: enrichSectionsWithDefault(combinedAssessment.sections), -}); diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 7fcc69d8..81b6e2f9 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -4,4 +4,3 @@ export * from './inquirer'; export * from './parseVariablesFromString'; export * from './extractProperties'; export * from './createDefaultCodec'; -export * from './enrichWithDefault'; diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index aa13edcc..ce31f9f5 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -7,10 +7,7 @@ import { OneTrustAssessmentSectionHeader, OneTrustRiskCategories, } from '@transcend-io/privacy-types'; -import { - enrichCombinedAssessmentWithDefaults, - extractProperties, -} from '../helpers'; +import { extractProperties } from '../helpers'; import { OneTrustCombinedAssessment, OneTrustEnrichedAssessmentQuestion, @@ -23,7 +20,7 @@ import { // TODO: test what happens when a value is null -> it should convert to '' const flattenObject = (obj: any, prefix = ''): any => - Object.keys(obj).reduce((acc, key) => { + Object.keys(obj ?? []).reduce((acc, key) => { const newKey = prefix ? `${prefix}_${key}` : key; const entry = obj[key]; @@ -132,7 +129,7 @@ const flattenOneTrustRiskCategories = ( allCategories: OneTrustRiskCategories[], prefix: string, ): any => { - const allCategoriesFlat = allCategories.map((categories) => { + const allCategoriesFlat = (allCategories ?? []).map((categories) => { const flatCategories = categories.map((c) => flattenObject(c, prefix)); return aggregateObjects({ objs: flatCategories }); }); @@ -144,12 +141,12 @@ const flattenOneTrustRisks = ( prefix: string, ): any => { // TODO: extract categories and other nested properties - const allRisksFlat = allRisks.map((risks) => { + const allRisksFlat = (allRisks ?? []).map((risks) => { const { categories, rest: restRisks } = extractProperties(risks ?? [], [ 'categories', ]); - const flatRisks = restRisks.map((r) => flattenObject(r, prefix)); + const flatRisks = (restRisks ?? []).map((r) => flattenObject(r, prefix)); return { ...aggregateObjects({ objs: flatRisks }), ...flattenOneTrustRiskCategories(categories, `${prefix}_categories`), @@ -243,10 +240,6 @@ const flattenOneTrustSections = ( export const flattenOneTrustAssessment = ( combinedAssessment: OneTrustCombinedAssessment, ): Record => { - // add default values to assessments - const combinedAssessmentWithDefaults = - enrichCombinedAssessmentWithDefaults(combinedAssessment); - const { approvers, primaryEntityDetails, @@ -255,7 +248,7 @@ export const flattenOneTrustAssessment = ( respondent, sections, ...rest - } = combinedAssessmentWithDefaults; + } = combinedAssessment; // TODO: extract approver from approvers, otherwise it won't agree with the codec const flatApprovers = approvers.map((approver) => diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index a87d1ce5..b674eade 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -137,7 +137,6 @@ export const writeOneTrustAssessment = ({ }), ); - // TODO: import from privacy-types // ensure the record has the expected type! decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); @@ -147,7 +146,5 @@ export const writeOneTrustAssessment = ({ // append the rows to the file csvRows.push(`${assessmentRow.join(',')}\n`); fs.appendFileSync('./oneTrust.csv', csvRows.join('\n')); - - // TODO: consider not to convert it to CSV at all! The importOneTrustAssessments does not actually accept CSV. } }; From 74666c27939905856d004fdab31c198fa7c1e21b Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:04:27 +0000 Subject: [PATCH 42/79] import createDefaultCodec from type-utils --- .pnp.cjs | 10 +- ...-utils-npm-1.5.0-125f1a01fb-0d7d85e794.zip | Bin 78594 -> 0 bytes src/helpers/createDefaultCodec.ts | 96 ------------- src/helpers/index.ts | 1 - src/helpers/tests/createDefaultCodec.test.ts | 131 ------------------ src/oneTrust/constants.ts | 2 +- yarn.lock | 10 +- 7 files changed, 11 insertions(+), 239 deletions(-) delete mode 100644 .yarn/cache/@transcend-io-type-utils-npm-1.5.0-125f1a01fb-0d7d85e794.zip delete mode 100644 src/helpers/createDefaultCodec.ts delete mode 100644 src/helpers/tests/createDefaultCodec.test.ts diff --git a/.pnp.cjs b/.pnp.cjs index 202d28f6..e5888643 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -34,7 +34,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/persisted-state", "npm:1.0.4"],\ ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ - ["@transcend-io/type-utils", "npm:1.5.0"],\ + ["@transcend-io/type-utils", "npm:1.6.0"],\ ["@types/bluebird", "npm:3.5.38"],\ ["@types/chai", "npm:4.3.4"],\ ["@types/cli-progress", "npm:3.11.0"],\ @@ -686,7 +686,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/persisted-state", "npm:1.0.4"],\ ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ - ["@transcend-io/type-utils", "npm:1.5.0"],\ + ["@transcend-io/type-utils", "npm:1.6.0"],\ ["@types/bluebird", "npm:3.5.38"],\ ["@types/chai", "npm:4.3.4"],\ ["@types/cli-progress", "npm:3.11.0"],\ @@ -836,10 +836,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["npm:1.5.0", {\ - "packageLocation": "./.yarn/cache/@transcend-io-type-utils-npm-1.5.0-125f1a01fb-0d7d85e794.zip/node_modules/@transcend-io/type-utils/",\ + ["npm:1.6.0", {\ + "packageLocation": "./.yarn/cache/@transcend-io-type-utils-npm-1.6.0-6359184828-4663edb422.zip/node_modules/@transcend-io/type-utils/",\ "packageDependencies": [\ - ["@transcend-io/type-utils", "npm:1.5.0"],\ + ["@transcend-io/type-utils", "npm:1.6.0"],\ ["fp-ts", "npm:2.16.1"],\ ["io-ts", "virtual:a57afaf9d13087a7202de8c93ac4854c9e2828bad7709250829ec4c7bc9dc95ecc2858c25612aa1774c986aedc232c76957076a1da3156fd2ab63ae5551b086f#npm:2.2.21"]\ ],\ diff --git a/.yarn/cache/@transcend-io-type-utils-npm-1.5.0-125f1a01fb-0d7d85e794.zip b/.yarn/cache/@transcend-io-type-utils-npm-1.5.0-125f1a01fb-0d7d85e794.zip deleted file mode 100644 index 557ba49a66736c360e0ec9a1fff22e25b448abef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78594 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%0A>7TxX6a%xXfv?dglV>_h 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

x=jE>4^Ec*+gsF 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 - codec: C, -): t.TypeOf => { - if (codec instanceof t.UnionType) { - // First, look for object types in the union - const arrayType = codec.types.find( - (type: any) => type instanceof t.ArrayType, - ); - if (arrayType) { - return createDefaultCodec(arrayType); - } - - // First, look for object types in the union - const objectType = codec.types.find( - (type: any) => - type instanceof t.InterfaceType || - type instanceof t.PartialType || - type instanceof t.IntersectionType || - type instanceof t.ArrayType, - ); - if (objectType) { - return createDefaultCodec(objectType); - } - - // For unions, null has higher preference as default. Otherwise,first type's default - // If null is one of the union types, it should be the default - const hasNull = codec.types.some( - (type: any) => type instanceof t.NullType || type.name === 'null', - ); - if (hasNull) { - return null as t.TypeOf; - } - - // If no null type found, default to first type - return createDefaultCodec(codec.types[0]); - } - - if (codec instanceof t.InterfaceType || codec instanceof t.PartialType) { - const defaults: Record = {}; - Object.entries(codec.props).forEach(([key, type]) => { - defaults[key] = createDefaultCodec(type as any); - }); - return defaults as t.TypeOf; - } - - if (codec instanceof t.IntersectionType) { - // Merge defaults of all types in the intersection - return codec.types.reduce( - (acc: t.TypeOf, type: any) => ({ - ...acc, - ...createDefaultCodec(type), - }), - {}, - ); - } - - if (codec instanceof t.ArrayType) { - // Check if the array element type is an object type - const elementType = codec.type; - const isObjectType = - elementType instanceof t.InterfaceType || - elementType instanceof t.PartialType || - elementType instanceof t.IntersectionType; - - return ( - isObjectType ? [createDefaultCodec(elementType)] : [] - ) as t.TypeOf; - } - - // Handle primitive and common types - switch (codec.name) { - case 'string': - return '' as t.TypeOf; - case 'number': - return null as t.TypeOf; - case 'boolean': - return null as t.TypeOf; - case 'null': - return null as t.TypeOf; - case 'undefined': - return undefined as t.TypeOf; - default: - return null as t.TypeOf; - } -}; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 81b6e2f9..07a8fd6a 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -3,4 +3,3 @@ export * from './buildEnabledRouteType'; export * from './inquirer'; export * from './parseVariablesFromString'; export * from './extractProperties'; -export * from './createDefaultCodec'; diff --git a/src/helpers/tests/createDefaultCodec.test.ts b/src/helpers/tests/createDefaultCodec.test.ts deleted file mode 100644 index 821a5694..00000000 --- a/src/helpers/tests/createDefaultCodec.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as t from 'io-ts'; -import chai, { expect } from 'chai'; -import deepEqualInAnyOrder from 'deep-equal-in-any-order'; - -import { createDefaultCodec } from '../createDefaultCodec'; -import { - OneTrustCombinedAssessment, - OneTrustCombinedAssessmentCodec, -} from '../../oneTrust/codecs'; -import { flattenOneTrustAssessment } from '../../oneTrust/flattenOneTrustAssessment'; - -chai.use(deepEqualInAnyOrder); - -describe('buildDefaultCodec', () => { - it('should correctly build a default codec for null', () => { - const result = createDefaultCodec(t.null); - expect(result).to.equal(null); - }); - - it('should correctly build a default codec for number', () => { - const result = createDefaultCodec(t.number); - expect(result).to.equal(null); - }); - - it('should correctly build a default codec for boolean', () => { - const result = createDefaultCodec(t.boolean); - expect(result).to.equal(null); - }); - - it('should correctly build a default codec for undefined', () => { - const result = createDefaultCodec(t.undefined); - expect(result).to.equal(undefined); - }); - - it('should correctly build a default codec for string', () => { - const result = createDefaultCodec(t.string); - expect(result).to.equal(''); - }); - - it('should correctly build a default codec for a union with null', () => { - const result = createDefaultCodec(t.union([t.string, t.null])); - // should default to null if the union contains null - expect(result).to.equal(null); - }); - - it('should correctly build a default codec for a union with object', () => { - const result = createDefaultCodec( - t.union([t.string, t.null, t.type({ name: t.string })]), - ); - // should default to the type if the union contains an object - expect(result).to.deep.equal({ name: '' }); - }); - - it('should correctly build a default codec for a union with array of type', () => { - const result = createDefaultCodec( - t.union([ - t.string, - t.null, - t.type({ name: t.string }), - t.array(t.string), - ]), - ); - // should default to the empty array if the union contains an array of type - expect(result).to.deep.equal([]); - }); - - it('should correctly build a default codec for a union with array of object', () => { - const result = createDefaultCodec( - t.union([ - t.string, - t.null, - t.type({ name: t.string }), - t.array(t.type({ age: t.number })), - ]), - ); - // should default to the array with object if the union contains an array of objects - expect(result).to.deep.equal([{ age: null }]); - }); - - it('should correctly build a default codec for a union without null', () => { - const result = createDefaultCodec(t.union([t.string, t.number])); - // should default to the first value if the union does not contains null - expect(result).to.equal(''); - }); - - it('should correctly build a default codec for an array of object types', () => { - const result = createDefaultCodec( - t.array(t.type({ name: t.string, age: t.number })), - ); - // should default to the first value if the union does not contains null - expect(result).to.deep.equalInAnyOrder([{ name: '', age: null }]); - }); - - it('should correctly build a default codec for an array of object partials', () => { - const result = createDefaultCodec( - t.array(t.partial({ name: t.string, age: t.number })), - ); - // should default to the first value if the union does not contains null - expect(result).to.deep.equalInAnyOrder([{ name: '', age: null }]); - }); - - it('should correctly build a default codec for an array of object intersections', () => { - const result = createDefaultCodec( - t.array( - t.intersection([ - t.partial({ name: t.string, age: t.number }), - t.type({ city: t.string }), - ]), - ), - ); - // should default to the first value if the union does not contains null - expect(result).to.deep.equalInAnyOrder([{ name: '', age: null, city: '' }]); - }); - - it('should correctly build a default codec for an array of strings', () => { - const result = createDefaultCodec(t.array(t.string)); - // should default to the first value if the union does not contains null - expect(result).to.deep.equal([]); - }); - - it('should correctly build a default codec for an intersection', () => { - const result = createDefaultCodec( - t.intersection([ - t.type({ id: t.string, name: t.string }), - t.partial({ age: t.number }), - ]), - ); - // should default to the first value if the union does not contains null - expect(result).to.deep.equalInAnyOrder({ id: '', name: '', age: null }); - }); -}); diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts index 876bf284..1f7e65cf 100644 --- a/src/oneTrust/constants.ts +++ b/src/oneTrust/constants.ts @@ -1,4 +1,4 @@ -import { createDefaultCodec } from '../helpers'; +import { createDefaultCodec } from '@transcend-io/type-utils'; import { OneTrustCombinedAssessment } from './codecs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; diff --git a/yarn.lock b/yarn.lock index 670dcbf9..806d32ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -517,7 +517,7 @@ __metadata: "@transcend-io/persisted-state": ^1.0.4 "@transcend-io/privacy-types": ^4.105.0 "@transcend-io/secret-value": ^1.2.0 - "@transcend-io/type-utils": ^1.5.0 + "@transcend-io/type-utils": ^1.6.0 "@types/bluebird": ^3.5.38 "@types/chai": ^4.3.4 "@types/cli-progress": ^3.11.0 @@ -699,13 +699,13 @@ __metadata: languageName: node linkType: hard -"@transcend-io/type-utils@npm:^1.5.0": - version: 1.5.0 - resolution: "@transcend-io/type-utils@npm:1.5.0" +"@transcend-io/type-utils@npm:^1.6.0": + version: 1.6.0 + resolution: "@transcend-io/type-utils@npm:1.6.0" dependencies: fp-ts: ^2.16.1 io-ts: ^2.2.21 - checksum: 0d7d85e794254069663b277e7728a39fe2d7c6b96eef4e71e6a971cd44f2b1a1be20cb82d708603182ed5b7e9ad20535c845590df59e8302d9ab6f70c626464f + checksum: 4663edb42217641e03f9f82e0bf7606270a7e7f8048ba02d74d90f993aad4dd151aae2dba5e495168cb82523dc6aced5dcb3694aac26a206a983a9fcc1de9eb4 languageName: node linkType: hard From da299c30f2fc5e9300704ba37c2f1de88fce5eb4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:07:26 +0000 Subject: [PATCH 43/79] rename cli-pull-ot -> cli-sync-ot --- README.md | 14 +++++++------- package.json | 2 +- src/{cli-pull-ot.ts => cli-sync-ot.ts} | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) rename src/{cli-pull-ot.ts => cli-sync-ot.ts} (96%) diff --git a/README.md b/README.md index b411546b..c96597ab 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ - [Authentication](#authentication-34) - [Arguments](#arguments-33) - [Usage](#usage-34) - - [tr-pull-ot](#tr-pull-ot) + - [tr-sync-ot](#tr-sync-ot) - [Authentication](#authentication-35) - [Arguments](#arguments-34) - [Usage](#usage-35) @@ -176,7 +176,7 @@ yarn add -D @transcend-io/cli # cli commands available within package yarn tr-pull --auth=$TRANSCEND_API_KEY -yarn tr-pull-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=$ONE_TRUST_HOSTNAME --file=$ONE_TRUST_OUTPUT_FILE +yarn tr-sync-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=$ONE_TRUST_HOSTNAME --file=$ONE_TRUST_OUTPUT_FILE yarn tr-push --auth=$TRANSCEND_API_KEY yarn tr-scan-packages --auth=$TRANSCEND_API_KEY yarn tr-discover-silos --auth=$TRANSCEND_API_KEY @@ -217,7 +217,7 @@ npm i -D @transcend-io/cli # cli commands available within package tr-pull --auth=$TRANSCEND_API_KEY -tr-pull-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=$ONE_TRUST_HOSTNAME --file=$ONE_TRUST_OUTPUT_FILE +tr-sync-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=$ONE_TRUST_HOSTNAME --file=$ONE_TRUST_OUTPUT_FILE tr-push --auth=$TRANSCEND_API_KEY tr-scan-packages --auth=$TRANSCEND_API_KEY tr-discover-silos --auth=$TRANSCEND_API_KEY @@ -577,9 +577,9 @@ tr-pull --auth=./transcend-api-keys.json --resources=consentManager --file=./tra Note: This command will overwrite the existing transcend.yml file that you have locally. -### tr-pull-ot +### tr-sync-ot -Pulls resources from a OneTrust instance. For now, it only supports retrieving OneTrust Assessments. It sends a request to the [Get List of Assessments](https://developer.onetrust.com/onetrust/reference/getallassessmentbasicdetailsusingget) endpoint to fetch a list of all Assessments in your account. Then, it queries the [Get Assessment](https://developer.onetrust.com/onetrust/reference/exportassessmentusingget) and [Get Risk](https://developer.onetrust.com/onetrust/reference/getriskusingget) endpoints to enrich these assessments with more details such as respondents, approvers, assessment questions and responses, and assessment risks. Finally, it syncs the enriched resources to disk in the specified file and format. +Pulls resources from a OneTrust and syncs them to a Transcend instance. For now, it only supports retrieving OneTrust Assessments. It sends a request to the [Get List of Assessments](https://developer.onetrust.com/onetrust/reference/getallassessmentbasicdetailsusingget) endpoint to fetch a list of all Assessments in your account. Then, it queries the [Get Assessment](https://developer.onetrust.com/onetrust/reference/exportassessmentusingget) and [Get Risk](https://developer.onetrust.com/onetrust/reference/getriskusingget) endpoints to enrich these assessments with more details such as respondents, approvers, assessment questions and responses, and assessment risks. Finally, it syncs the enriched resources to disk in the specified file and format. This command can be helpful if you are looking to: @@ -603,7 +603,7 @@ To learn how to generate the token, see the [OAuth 2.0 Scopes](https://developer | auth | The OAuth access token with the scopes necessary to access the OneTrust Public APIs. | string | N/A | true | | hostname | The domain of the OneTrust environment from which to pull the resource (e.g. trial.onetrust.com). | string | N/A | true | | file | Path to the file to pull the resource into. Its format must match the fileFormat argument. | string | N/A | true | -| fileFormat | The format of the output file. | string | csv | false | +| fileFormat | The format of the output file. | string | csv | false | | resource | The resource to pull from OneTrust. For now, only assessments is supported. | string | assessments | false | | debug | Whether to print detailed logs in case of error. | boolean | false | false | @@ -611,7 +611,7 @@ To learn how to generate the token, see the [OAuth 2.0 Scopes](https://developer ```sh # Writes out file to ./oneTrustAssessments.json -tr-pull-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=trial.onetrust.com --file=./oneTrustAssessments.json +tr-sync-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=trial.onetrust.com --file=./oneTrustAssessments.json ``` ### tr-push diff --git a/package.json b/package.json index 5ba4cabf..a12d380a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "tr-pull-consent-metrics": "./build/cli-pull-consent-metrics.js", "tr-pull-consent-preferences": "./build/cli-pull-consent-preferences.js", "tr-pull-datapoints": "./build/cli-pull-datapoints.js", - "tr-pull-ot": "./build/cli-pull-ot.js", + "tr-sync-ot": "./build/cli-sync-ot.js", "tr-push": "./build/cli-push.js", "tr-request-approve": "./build/cli-request-approve.js", "tr-request-cancel": "./build/cli-request-cancel.js", diff --git a/src/cli-pull-ot.ts b/src/cli-sync-ot.ts similarity index 96% rename from src/cli-pull-ot.ts rename to src/cli-sync-ot.ts index 68f28d57..b62abcc7 100644 --- a/src/cli-pull-ot.ts +++ b/src/cli-sync-ot.ts @@ -22,10 +22,10 @@ import { * Pull configuration from OneTrust down locally to disk * * Dev Usage: - * yarn ts-node ./src/cli-pull-ot.ts --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json + * yarn ts-node ./src/cli-sync-ot.ts --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json * * Standard usage - * yarn cli-pull-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json + * yarn cli-sync-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json */ async function main(): Promise { const { file, fileFormat, hostname, auth, resource } = From 799e41cf7c98528101b7d1d22b64c3d776c9446d Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:13:36 +0000 Subject: [PATCH 44/79] add dryRun argument --- README.md | 1 + package.json | 2 +- src/cli-sync-ot.ts | 4 +- src/oneTrust/index.ts | 2 +- src/oneTrust/parseCliPullOtArguments.ts | 115 ------------------------ yarn.lock | 2 +- 6 files changed, 6 insertions(+), 120 deletions(-) delete mode 100644 src/oneTrust/parseCliPullOtArguments.ts diff --git a/README.md b/README.md index c96597ab..e233e3a0 100644 --- a/README.md +++ b/README.md @@ -606,6 +606,7 @@ To learn how to generate the token, see the [OAuth 2.0 Scopes](https://developer | fileFormat | The format of the output file. | string | csv | false | | resource | The resource to pull from OneTrust. For now, only assessments is supported. | string | assessments | false | | debug | Whether to print detailed logs in case of error. | boolean | false | false | +| dryRun | Whether to export the resource to a file rather than sync to Transcend. | boolean | false | false | #### Usage diff --git a/package.json b/package.json index a12d380a..c06e3f76 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "tr-pull-consent-metrics": "./build/cli-pull-consent-metrics.js", "tr-pull-consent-preferences": "./build/cli-pull-consent-preferences.js", "tr-pull-datapoints": "./build/cli-pull-datapoints.js", - "tr-sync-ot": "./build/cli-sync-ot.js", "tr-push": "./build/cli-push.js", "tr-request-approve": "./build/cli-request-approve.js", "tr-request-cancel": "./build/cli-request-cancel.js", @@ -42,6 +41,7 @@ "tr-retry-request-data-silos": "./build/cli-retry-request-data-silos.js", "tr-scan-packages": "./build/cli-scan-packages.js", "tr-skip-request-data-silos": "./build/cli-skip-request-data-silos.js", + "tr-sync-ot": "./build/cli-sync-ot.js", "tr-update-consent-manager": "./build/cli-update-consent-manager-to-latest.js", "tr-upload-consent-preferences": "./build/cli-upload-consent-preferences.js", "tr-upload-cookies-from-csv": "./build/cli-upload-cookies-from-csv.js", diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index b62abcc7..771418c3 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -5,7 +5,7 @@ import { getListOfOneTrustAssessments, getOneTrustAssessment, writeOneTrustAssessment, - parseCliPullOtArguments, + parseCliSyncOtArguments, createOneTrustGotInstance, getOneTrustRisk, } from './oneTrust'; @@ -29,7 +29,7 @@ import { */ async function main(): Promise { const { file, fileFormat, hostname, auth, resource } = - parseCliPullOtArguments(); + parseCliSyncOtArguments(); // try { // TODO: move to helper function diff --git a/src/oneTrust/index.ts b/src/oneTrust/index.ts index e98e34fe..5c921384 100644 --- a/src/oneTrust/index.ts +++ b/src/oneTrust/index.ts @@ -1,6 +1,6 @@ export * from './createOneTrustGotInstance'; export * from './getOneTrustAssessment'; export * from './writeOneTrustAssessment'; -export * from './parseCliPullOtArguments'; +export * from './parseCliSyncOtArguments'; export * from './getListOfOneTrustAssessments'; export * from './getOneTrustRisk'; diff --git a/src/oneTrust/parseCliPullOtArguments.ts b/src/oneTrust/parseCliPullOtArguments.ts deleted file mode 100644 index 4d31cbc3..00000000 --- a/src/oneTrust/parseCliPullOtArguments.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { logger } from '../logger'; -import colors from 'colors'; -import yargs from 'yargs-parser'; -import { OneTrustFileFormat, OneTrustPullResource } from '../enums'; - -const VALID_RESOURCES = Object.values(OneTrustPullResource); -const VALID_FILE_FORMATS = Object.values(OneTrustFileFormat); - -interface OneTrustCliArguments { - /** The name of the file to write the resources to */ - file: string; - /** The OneTrust hostname to send the requests to */ - hostname: string; - /** The OAuth Bearer token used to authenticate the requests */ - auth: string; - /** The resource to pull from OneTrust */ - resource: OneTrustPullResource; - /** Whether to enable debugging while reporting errors */ - debug: boolean; - /** The export format of the file where to save the resources */ - fileFormat: OneTrustFileFormat; -} - -/** - * Parse the command line arguments - * - * @returns the parsed arguments - */ -export const parseCliPullOtArguments = (): OneTrustCliArguments => { - const { file, hostname, auth, resource, debug, fileFormat } = yargs( - process.argv.slice(2), - { - string: ['file', 'hostname', 'auth', 'resource', 'fileFormat'], - boolean: ['debug'], - default: { - resource: OneTrustPullResource.Assessments, - fileFormat: OneTrustFileFormat.Csv, - debug: false, - }, - }, - ); - - if (!file) { - logger.error( - colors.red( - 'Missing required parameter "file". e.g. --file=./oneTrustAssessments.json', - ), - ); - return process.exit(1); - } - const splitFile = file.split('.'); - if (splitFile.length < 2) { - logger.error( - colors.red( - 'The "file" parameter has an invalid format. Expected a path with extensions. e.g. --file=./pathToFile.json.', - ), - ); - return process.exit(1); - } - if (splitFile.at(-1) !== fileFormat) { - logger.error( - colors.red( - `The "file" and "fileFormat" parameters must specify the same format! Got file=${file} and fileFormat=${fileFormat}`, - ), - ); - return process.exit(1); - } - - if (!hostname) { - logger.error( - colors.red( - 'Missing required parameter "hostname". e.g. --hostname=customer.my.onetrust.com', - ), - ); - return process.exit(1); - } - - if (!auth) { - logger.error( - colors.red( - 'Missing required parameter "auth". e.g. --auth=$ONE_TRUST_AUTH_TOKEN', - ), - ); - return process.exit(1); - } - if (!VALID_RESOURCES.includes(resource)) { - logger.error( - colors.red( - `Received invalid resource value: "${resource}". Allowed: ${VALID_RESOURCES.join( - ',', - )}`, - ), - ); - return process.exit(1); - } - if (!VALID_FILE_FORMATS.includes(fileFormat)) { - logger.error( - colors.red( - `Received invalid fileFormat value: "${fileFormat}". Allowed: ${VALID_FILE_FORMATS.join( - ',', - )}`, - ), - ); - return process.exit(1); - } - - return { - file, - hostname, - auth, - resource, - debug, - fileFormat, - }; -}; diff --git a/yarn.lock b/yarn.lock index 806d32ab..12dc0ff7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -593,7 +593,6 @@ __metadata: tr-pull-consent-metrics: ./build/cli-pull-consent-metrics.js tr-pull-consent-preferences: ./build/cli-pull-consent-preferences.js tr-pull-datapoints: ./build/cli-pull-datapoints.js - tr-pull-ot: ./build/cli-pull-ot.js tr-push: ./build/cli-push.js tr-request-approve: ./build/cli-request-approve.js tr-request-cancel: ./build/cli-request-cancel.js @@ -607,6 +606,7 @@ __metadata: tr-retry-request-data-silos: ./build/cli-retry-request-data-silos.js tr-scan-packages: ./build/cli-scan-packages.js tr-skip-request-data-silos: ./build/cli-skip-request-data-silos.js + tr-sync-ot: ./build/cli-sync-ot.js tr-update-consent-manager: ./build/cli-update-consent-manager-to-latest.js tr-upload-consent-preferences: ./build/cli-upload-consent-preferences.js tr-upload-cookies-from-csv: ./build/cli-upload-cookies-from-csv.js From 30f3a4cee522ed154f0d4dc8d3b187fc43d4c420 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:13:59 +0000 Subject: [PATCH 45/79] add dryRun argument --- src/oneTrust/parseCliSyncOtArguments.ts | 128 ++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/oneTrust/parseCliSyncOtArguments.ts diff --git a/src/oneTrust/parseCliSyncOtArguments.ts b/src/oneTrust/parseCliSyncOtArguments.ts new file mode 100644 index 00000000..6ab39a86 --- /dev/null +++ b/src/oneTrust/parseCliSyncOtArguments.ts @@ -0,0 +1,128 @@ +import { logger } from '../logger'; +import colors from 'colors'; +import yargs from 'yargs-parser'; +import { OneTrustFileFormat, OneTrustPullResource } from '../enums'; + +const VALID_RESOURCES = Object.values(OneTrustPullResource); +const VALID_FILE_FORMATS = Object.values(OneTrustFileFormat); + +interface OneTrustCliArguments { + /** The name of the file to write the resources to */ + file: string; + /** The OneTrust hostname to send the requests to */ + hostname: string; + /** The OAuth Bearer token used to authenticate the requests */ + auth: string; + /** The resource to pull from OneTrust */ + resource: OneTrustPullResource; + /** Whether to enable debugging while reporting errors */ + debug: boolean; + /** The export format of the file where to save the resources */ + fileFormat: OneTrustFileFormat; + /** Whether to export the resource into a file rather than push to transcend */ + dryRun: boolean; +} + +/** + * Parse the command line arguments + * + * @returns the parsed arguments + */ +export const parseCliSyncOtArguments = (): OneTrustCliArguments => { + const { file, hostname, auth, resource, debug, fileFormat, dryRun } = yargs( + process.argv.slice(2), + { + string: ['file', 'hostname', 'auth', 'resource', 'fileFormat', 'dryRun'], + boolean: ['debug', 'dryRun'], + default: { + resource: OneTrustPullResource.Assessments, + fileFormat: OneTrustFileFormat.Csv, + debug: false, + dryRun: false, + }, + }, + ); + + // Can only sync to Transcend via a CSV file format! + if (!dryRun && fileFormat !== OneTrustFileFormat.Csv) { + logger.error( + colors.red( + 'Must not set the "fileFormat" parameter when "dryRun" is "false".', + ), + ); + } + + if (!file) { + logger.error( + colors.red( + 'Missing required parameter "file". e.g. --file=./oneTrustAssessments.json', + ), + ); + return process.exit(1); + } + const splitFile = file.split('.'); + if (splitFile.length < 2) { + logger.error( + colors.red( + 'The "file" parameter has an invalid format. Expected a path with extensions. e.g. --file=./pathToFile.json.', + ), + ); + return process.exit(1); + } + if (splitFile.at(-1) !== fileFormat) { + logger.error( + colors.red( + `The "file" and "fileFormat" parameters must specify the same format! Got file=${file} and fileFormat=${fileFormat}`, + ), + ); + return process.exit(1); + } + + if (!hostname) { + logger.error( + colors.red( + 'Missing required parameter "hostname". e.g. --hostname=customer.my.onetrust.com', + ), + ); + return process.exit(1); + } + + if (!auth) { + logger.error( + colors.red( + 'Missing required parameter "auth". e.g. --auth=$ONE_TRUST_AUTH_TOKEN', + ), + ); + return process.exit(1); + } + if (!VALID_RESOURCES.includes(resource)) { + logger.error( + colors.red( + `Received invalid resource value: "${resource}". Allowed: ${VALID_RESOURCES.join( + ',', + )}`, + ), + ); + return process.exit(1); + } + if (!VALID_FILE_FORMATS.includes(fileFormat)) { + logger.error( + colors.red( + `Received invalid fileFormat value: "${fileFormat}". Allowed: ${VALID_FILE_FORMATS.join( + ',', + )}`, + ), + ); + return process.exit(1); + } + + return { + file, + hostname, + auth, + resource, + debug, + fileFormat, + dryRun, + }; +}; From 239a72d535072cb375d813bfcd4f4aabda27c62b Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:20:27 +0000 Subject: [PATCH 46/79] move some logic from writeOneTrustAssessment to cli-sync-ot --- src/cli-sync-ot.ts | 162 +++++++++++++++--------- src/oneTrust/writeOneTrustAssessment.ts | 57 +-------- 2 files changed, 107 insertions(+), 112 deletions(-) diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index 771418c3..da163e82 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -1,5 +1,7 @@ #!/usr/bin/env node import { logger } from './logger'; +import keyBy from 'lodash/keyBy'; + import colors from 'colors'; import { getListOfOneTrustAssessments, @@ -28,75 +30,117 @@ import { * yarn cli-sync-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json */ async function main(): Promise { - const { file, fileFormat, hostname, auth, resource } = + const { file, fileFormat, hostname, auth, resource, debug } = parseCliSyncOtArguments(); - // try { - // TODO: move to helper function - if (resource === OneTrustPullResource.Assessments) { - // use the hostname and auth token to instantiate a client to talk to OneTrust - const oneTrust = createOneTrustGotInstance({ hostname, auth }); - - // fetch the list of all assessments in the OneTrust organization - const assessments = await getListOfOneTrustAssessments({ oneTrust }); + try { + // TODO: move to helper function + if (resource === OneTrustPullResource.Assessments) { + // use the hostname and auth token to instantiate a client to talk to OneTrust + const oneTrust = createOneTrustGotInstance({ hostname, auth }); - // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory - await mapSeries(assessments, async (assessment, index) => { - logger.info( - `Fetching details about assessment ${index + 1} of ${ - assessments.length - }...`, - ); - const assessmentDetails = await getOneTrustAssessment({ - oneTrust, - assessmentId: assessment.assessmentId, - }); + // fetch the list of all assessments in the OneTrust organization + const assessments = await getListOfOneTrustAssessments({ oneTrust }); - // enrich assessments with risk information - let riskDetails: OneTrustGetRiskResponse[] = []; - const riskIds = uniq( - assessmentDetails.sections.flatMap((s: OneTrustAssessmentSection) => - s.questions.flatMap((q: OneTrustAssessmentQuestion) => - (q.risks ?? []).flatMap((r) => r.riskId), - ), - ), - ); - if (riskIds.length > 0) { + // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory + await mapSeries(assessments, async (assessment, index) => { logger.info( - `Fetching details about ${riskIds.length} risks for assessment ${ - index + 1 - } of ${assessments.length}...`, + `Fetching details about assessment ${index + 1} of ${ + assessments.length + }...`, ); - riskDetails = await map( - riskIds, - (riskId) => getOneTrustRisk({ oneTrust, riskId: riskId as string }), - { - concurrency: 5, - }, + const assessmentDetails = await getOneTrustAssessment({ + oneTrust, + assessmentId: assessment.assessmentId, + }); + + // enrich assessments with risk information + let riskDetails: OneTrustGetRiskResponse[] = []; + const riskIds = uniq( + assessmentDetails.sections.flatMap((s: OneTrustAssessmentSection) => + s.questions.flatMap((q: OneTrustAssessmentQuestion) => + (q.risks ?? []).flatMap((r) => r.riskId), + ), + ), ); - } + if (riskIds.length > 0) { + logger.info( + `Fetching details about ${riskIds.length} risks for assessment ${ + index + 1 + } of ${assessments.length}...`, + ); + riskDetails = await map( + riskIds, + (riskId) => getOneTrustRisk({ oneTrust, riskId: riskId as string }), + { + concurrency: 5, + }, + ); + } + + // enrich the sections with risk details + const riskDetailsById = keyBy(riskDetails, 'id'); + const { sections, ...restAssessmentDetails } = assessmentDetails; + const enrichedSections = sections.map((section) => { + const { questions, ...restSection } = section; + const enrichedQuestions = questions.map((question) => { + const { risks, ...restQuestion } = question; + const enrichedRisks = (risks ?? []).map((risk) => { + const details = riskDetailsById[risk.riskId]; + // TODO: missing the risk meta data and links to the assessment + return { + ...risk, + description: details.description, + name: details.name, + treatment: details.treatment, + treatmentStatus: details.treatmentStatus, + type: details.type, + state: details.state, + stage: details.stage, + result: details.result, + categories: details.categories, + }; + }); + return { + ...restQuestion, + risks: enrichedRisks, + }; + }); + return { + ...restSection, + questions: enrichedQuestions, + }; + }); - writeOneTrustAssessment({ - assessment, - assessmentDetails, - riskDetails, - index, - total: assessments.length, - file, - fileFormat, + // combine the two assessments into a single enriched result + const enrichedAssessment = { + ...restAssessmentDetails, + sections: enrichedSections, + }; + + writeOneTrustAssessment({ + assessment: { + ...assessment, + ...enrichedAssessment, + }, + riskDetails, + index, + total: assessments.length, + file, + fileFormat, + }); }); - }); + } + } catch (err) { + logger.error( + colors.red( + `An error occurred pulling the resource ${resource} from OneTrust: ${ + debug ? err.stack : err.message + }`, + ), + ); + process.exit(1); } - // } catch (err) { - // logger.error( - // colors.red( - // `An error occurred pulling the resource ${resource} from OneTrust: ${ - // debug ? err.stack : err.message - // }`, - // ), - // ); - // process.exit(1); - // } // Indicate success logger.info( diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index b674eade..eb7a08fc 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -1,5 +1,4 @@ import { logger } from '../logger'; -import keyBy from 'lodash/keyBy'; import colors from 'colors'; import { OneTrustFileFormat } from '../enums'; import fs from 'fs'; @@ -7,11 +6,10 @@ import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; import { decodeCodec } from '@transcend-io/type-utils'; import { - OneTrustAssessment, OneTrustAssessmentCsvRecord, - OneTrustGetAssessmentResponse, OneTrustGetRiskResponse, } from '@transcend-io/privacy-types'; +import { OneTrustCombinedAssessment } from './codecs'; /** * Write the assessment to disk at the specified file path. @@ -23,8 +21,6 @@ export const writeOneTrustAssessment = ({ file, fileFormat, assessment, - assessmentDetails, - riskDetails, index, total, }: { @@ -33,9 +29,7 @@ export const writeOneTrustAssessment = ({ /** The format of the output file */ fileFormat: OneTrustFileFormat; /** The basic assessment */ - assessment: OneTrustAssessment; - /** The assessment with details */ - assessmentDetails: OneTrustGetAssessmentResponse; + assessment: OneTrustCombinedAssessment; /** The details of risks found within the assessment */ riskDetails: OneTrustGetRiskResponse[]; /** The index of the assessment being written to the file */ @@ -51,46 +45,6 @@ export const writeOneTrustAssessment = ({ ), ); - // enrich the sections with risk details - const riskDetailsById = keyBy(riskDetails, 'id'); - const { sections, ...restAssessmentDetails } = assessmentDetails; - const enrichedSections = sections.map((section) => { - const { questions, ...restSection } = section; - const enrichedQuestions = questions.map((question) => { - const { risks, ...restQuestion } = question; - const enrichedRisks = (risks ?? []).map((risk) => { - const details = riskDetailsById[risk.riskId]; - // TODO: missing the risk meta data and links to the assessment - return { - ...risk, - description: details.description, - name: details.name, - treatment: details.treatment, - treatmentStatus: details.treatmentStatus, - type: details.type, - state: details.state, - stage: details.stage, - result: details.result, - categories: details.categories, - }; - }); - return { - ...restQuestion, - risks: enrichedRisks, - }; - }); - return { - ...restSection, - questions: enrichedQuestions, - }; - }); - - // combine the two assessments into a single enriched result - const enrichedAssessment = { - ...restAssessmentDetails, - sections: enrichedSections, - }; - // For json format if (fileFormat === OneTrustFileFormat.Json) { // start with an opening bracket @@ -98,7 +52,7 @@ export const writeOneTrustAssessment = ({ fs.writeFileSync(file, '[\n'); } - const stringifiedAssessment = JSON.stringify(enrichedAssessment, null, 2); + const stringifiedAssessment = JSON.stringify(assessment, null, 2); // Add comma for all items except the last one const comma = index < total - 1 ? ',' : ''; @@ -119,10 +73,7 @@ export const writeOneTrustAssessment = ({ } // flatten the assessment object so it does not have nested properties - const flatAssessment = flattenOneTrustAssessment({ - ...assessment, - ...enrichedAssessment, - }); + const flatAssessment = flattenOneTrustAssessment(assessment); // comment const flatAssessmentFull = Object.fromEntries( From ae5522f4bb27ed39d6424cf30fd1484e6b9e819b Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:23:09 +0000 Subject: [PATCH 47/79] ship more improvements to writeOneTrustAssessment --- src/cli-sync-ot.ts | 15 +++++++++------ src/oneTrust/codecs.ts | 6 +++--- src/oneTrust/constants.ts | 8 ++++---- src/oneTrust/flattenOneTrustAssessment.ts | 4 ++-- src/oneTrust/writeOneTrustAssessment.ts | 11 +++-------- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index da163e82..dd9b6ca1 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -42,7 +42,10 @@ async function main(): Promise { // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); - // fetch details about one assessment at a time and sync to disk right away to avoid running out of memory + /** + * fetch details about one assessment in series and push to transcend or write to disk + * (depending on the dryRun argument) right away to avoid running out of memory + */ await mapSeries(assessments, async (assessment, index) => { logger.info( `Fetching details about assessment ${index + 1} of ${ @@ -78,10 +81,11 @@ async function main(): Promise { ); } + // TODO: create a helper for this // enrich the sections with risk details const riskDetailsById = keyBy(riskDetails, 'id'); const { sections, ...restAssessmentDetails } = assessmentDetails; - const enrichedSections = sections.map((section) => { + const sectionsWithEnrichedRisk = sections.map((section) => { const { questions, ...restSection } = section; const enrichedQuestions = questions.map((question) => { const { risks, ...restQuestion } = question; @@ -113,17 +117,16 @@ async function main(): Promise { }); // combine the two assessments into a single enriched result - const enrichedAssessment = { + const assessmentWithEnrichedRisk = { ...restAssessmentDetails, - sections: enrichedSections, + sections: sectionsWithEnrichedRisk, }; writeOneTrustAssessment({ assessment: { ...assessment, - ...enrichedAssessment, + ...assessmentWithEnrichedRisk, }, - riskDetails, index, total: assessments.length, file, diff --git a/src/oneTrust/codecs.ts b/src/oneTrust/codecs.ts index 7be22bd4..3677d153 100644 --- a/src/oneTrust/codecs.ts +++ b/src/oneTrust/codecs.ts @@ -159,12 +159,12 @@ export type OneTrustEnrichedAssessmentResponse = t.TypeOf< // eslint-disable-next-line @typescript-eslint/no-unused-vars const { status, ...OneTrustAssessmentWithoutStatus } = OneTrustAssessment.props; -export const OneTrustCombinedAssessment = t.intersection([ +export const OneTrustEnrichedAssessment = t.intersection([ t.type(OneTrustAssessmentWithoutStatus), OneTrustEnrichedAssessmentResponse, ]); /** Type override */ -export type OneTrustCombinedAssessment = t.TypeOf< - typeof OneTrustCombinedAssessment +export type OneTrustEnrichedAssessment = t.TypeOf< + typeof OneTrustEnrichedAssessment >; diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts index 1f7e65cf..de4cf982 100644 --- a/src/oneTrust/constants.ts +++ b/src/oneTrust/constants.ts @@ -1,14 +1,14 @@ import { createDefaultCodec } from '@transcend-io/type-utils'; -import { OneTrustCombinedAssessment } from './codecs'; +import { OneTrustEnrichedAssessment } from './codecs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; /** - * An object with default values of type OneTrustCombinedAssessment. It's very + * An object with default values of type OneTrustEnrichedAssessment. It's very * valuable when converting assessments to CSV, as it contains all keys that * make up the CSV header in the expected order */ -const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustCombinedAssessment = - createDefaultCodec(OneTrustCombinedAssessment); +const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustEnrichedAssessment = + createDefaultCodec(OneTrustEnrichedAssessment); /** The header of the OneTrust ASsessment CSV file */ export const DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER = Object.keys( diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/flattenOneTrustAssessment.ts index ce31f9f5..9702b0fb 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/flattenOneTrustAssessment.ts @@ -9,7 +9,7 @@ import { } from '@transcend-io/privacy-types'; import { extractProperties } from '../helpers'; import { - OneTrustCombinedAssessment, + OneTrustEnrichedAssessment, OneTrustEnrichedAssessmentQuestion, OneTrustEnrichedAssessmentSection, OneTrustEnrichedRisk, @@ -238,7 +238,7 @@ const flattenOneTrustSections = ( // TODO: update type to be a Record export const flattenOneTrustAssessment = ( - combinedAssessment: OneTrustCombinedAssessment, + combinedAssessment: OneTrustEnrichedAssessment, ): Record => { const { approvers, diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts index eb7a08fc..56e29fa9 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -5,11 +5,8 @@ import fs from 'fs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; import { decodeCodec } from '@transcend-io/type-utils'; -import { - OneTrustAssessmentCsvRecord, - OneTrustGetRiskResponse, -} from '@transcend-io/privacy-types'; -import { OneTrustCombinedAssessment } from './codecs'; +import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; +import { OneTrustEnrichedAssessment } from './codecs'; /** * Write the assessment to disk at the specified file path. @@ -29,9 +26,7 @@ export const writeOneTrustAssessment = ({ /** The format of the output file */ fileFormat: OneTrustFileFormat; /** The basic assessment */ - assessment: OneTrustCombinedAssessment; - /** The details of risks found within the assessment */ - riskDetails: OneTrustGetRiskResponse[]; + assessment: OneTrustEnrichedAssessment; /** The index of the assessment being written to the file */ index: number; /** The total amount of assessments that we will write */ From 4b428dc626bf5d4fb5e367e445ded6e5a75a9136 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:27:53 +0000 Subject: [PATCH 48/79] reorganize folder structure --- src/oneTrust/constants.ts | 2 +- .../{ => endpoints}/getListOfOneTrustAssessments.ts | 2 +- src/oneTrust/{ => endpoints}/getOneTrustAssessment.ts | 0 src/oneTrust/{ => endpoints}/getOneTrustRisk.ts | 0 src/oneTrust/endpoints/index.ts | 3 +++ src/oneTrust/{ => helpers}/flattenOneTrustAssessment.ts | 4 ++-- src/oneTrust/helpers/index.ts | 3 +++ src/oneTrust/{ => helpers}/parseCliSyncOtArguments.ts | 4 ++-- src/oneTrust/{ => helpers}/writeOneTrustAssessment.ts | 8 ++++---- src/oneTrust/index.ts | 7 ++----- 10 files changed, 18 insertions(+), 15 deletions(-) rename src/oneTrust/{ => endpoints}/getListOfOneTrustAssessments.ts (97%) rename src/oneTrust/{ => endpoints}/getOneTrustAssessment.ts (100%) rename src/oneTrust/{ => endpoints}/getOneTrustRisk.ts (100%) create mode 100644 src/oneTrust/endpoints/index.ts rename src/oneTrust/{ => helpers}/flattenOneTrustAssessment.ts (99%) create mode 100644 src/oneTrust/helpers/index.ts rename src/oneTrust/{ => helpers}/parseCliSyncOtArguments.ts (96%) rename src/oneTrust/{ => helpers}/writeOneTrustAssessment.ts (92%) diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts index de4cf982..ad249a2a 100644 --- a/src/oneTrust/constants.ts +++ b/src/oneTrust/constants.ts @@ -1,6 +1,6 @@ import { createDefaultCodec } from '@transcend-io/type-utils'; import { OneTrustEnrichedAssessment } from './codecs'; -import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; +import { flattenOneTrustAssessment } from './helpers'; /** * An object with default values of type OneTrustEnrichedAssessment. It's very diff --git a/src/oneTrust/getListOfOneTrustAssessments.ts b/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts similarity index 97% rename from src/oneTrust/getListOfOneTrustAssessments.ts rename to src/oneTrust/endpoints/getListOfOneTrustAssessments.ts index 2ca9abea..5d848a30 100644 --- a/src/oneTrust/getListOfOneTrustAssessments.ts +++ b/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts @@ -1,5 +1,5 @@ import { Got } from 'got'; -import { logger } from '../logger'; +import { logger } from '../../logger'; import { decodeCodec } from '@transcend-io/type-utils'; import { OneTrustAssessment, diff --git a/src/oneTrust/getOneTrustAssessment.ts b/src/oneTrust/endpoints/getOneTrustAssessment.ts similarity index 100% rename from src/oneTrust/getOneTrustAssessment.ts rename to src/oneTrust/endpoints/getOneTrustAssessment.ts diff --git a/src/oneTrust/getOneTrustRisk.ts b/src/oneTrust/endpoints/getOneTrustRisk.ts similarity index 100% rename from src/oneTrust/getOneTrustRisk.ts rename to src/oneTrust/endpoints/getOneTrustRisk.ts diff --git a/src/oneTrust/endpoints/index.ts b/src/oneTrust/endpoints/index.ts new file mode 100644 index 00000000..7f84cfce --- /dev/null +++ b/src/oneTrust/endpoints/index.ts @@ -0,0 +1,3 @@ +export * from './getListOfOneTrustAssessments'; +export * from './getOneTrustAssessment'; +export * from './getOneTrustRisk'; diff --git a/src/oneTrust/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts similarity index 99% rename from src/oneTrust/flattenOneTrustAssessment.ts rename to src/oneTrust/helpers/flattenOneTrustAssessment.ts index 9702b0fb..89f4743d 100644 --- a/src/oneTrust/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -7,13 +7,13 @@ import { OneTrustAssessmentSectionHeader, OneTrustRiskCategories, } from '@transcend-io/privacy-types'; -import { extractProperties } from '../helpers'; +import { extractProperties } from '../../helpers'; import { OneTrustEnrichedAssessment, OneTrustEnrichedAssessmentQuestion, OneTrustEnrichedAssessmentSection, OneTrustEnrichedRisk, -} from './codecs'; +} from '../codecs'; // import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // TODO: will have to use something like csv-stringify diff --git a/src/oneTrust/helpers/index.ts b/src/oneTrust/helpers/index.ts new file mode 100644 index 00000000..504bb11e --- /dev/null +++ b/src/oneTrust/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './flattenOneTrustAssessment'; +export * from './parseCliSyncOtArguments'; +export * from './writeOneTrustAssessment'; diff --git a/src/oneTrust/parseCliSyncOtArguments.ts b/src/oneTrust/helpers/parseCliSyncOtArguments.ts similarity index 96% rename from src/oneTrust/parseCliSyncOtArguments.ts rename to src/oneTrust/helpers/parseCliSyncOtArguments.ts index 6ab39a86..feabaf44 100644 --- a/src/oneTrust/parseCliSyncOtArguments.ts +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -1,7 +1,7 @@ -import { logger } from '../logger'; +import { logger } from '../../logger'; import colors from 'colors'; import yargs from 'yargs-parser'; -import { OneTrustFileFormat, OneTrustPullResource } from '../enums'; +import { OneTrustFileFormat, OneTrustPullResource } from '../../enums'; const VALID_RESOURCES = Object.values(OneTrustPullResource); const VALID_FILE_FORMATS = Object.values(OneTrustFileFormat); diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/helpers/writeOneTrustAssessment.ts similarity index 92% rename from src/oneTrust/writeOneTrustAssessment.ts rename to src/oneTrust/helpers/writeOneTrustAssessment.ts index 56e29fa9..2867ab34 100644 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ b/src/oneTrust/helpers/writeOneTrustAssessment.ts @@ -1,12 +1,12 @@ -import { logger } from '../logger'; +import { logger } from '../../logger'; import colors from 'colors'; -import { OneTrustFileFormat } from '../enums'; +import { OneTrustFileFormat } from '../../enums'; import fs from 'fs'; import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; -import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; +import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from '../constants'; import { decodeCodec } from '@transcend-io/type-utils'; import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; -import { OneTrustEnrichedAssessment } from './codecs'; +import { OneTrustEnrichedAssessment } from '../codecs'; /** * Write the assessment to disk at the specified file path. diff --git a/src/oneTrust/index.ts b/src/oneTrust/index.ts index 5c921384..4f127531 100644 --- a/src/oneTrust/index.ts +++ b/src/oneTrust/index.ts @@ -1,6 +1,3 @@ export * from './createOneTrustGotInstance'; -export * from './getOneTrustAssessment'; -export * from './writeOneTrustAssessment'; -export * from './parseCliSyncOtArguments'; -export * from './getListOfOneTrustAssessments'; -export * from './getOneTrustRisk'; +export * from './helpers'; +export * from './endpoints'; From 13e80e9a5aa099ad3319d41683360fb0a99c3f49 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:45:53 +0000 Subject: [PATCH 49/79] create syncOneTrustAssessments and enrichOneTrustAssessment helpers --- src/cli-sync-ot.ts | 5 +- .../endpoints/getListOfOneTrustAssessments.ts | 1 - .../helpers/enrichOneTrustAssessment.ts | 67 +++++++++++++ .../helpers/parseCliSyncOtArguments.ts | 20 +++- .../helpers/syncOneTrustAssessments.ts | 94 +++++++++++++++++++ 5 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 src/oneTrust/helpers/enrichOneTrustAssessment.ts create mode 100644 src/oneTrust/helpers/syncOneTrustAssessments.ts diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index dd9b6ca1..bb552c58 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -32,13 +32,12 @@ import { async function main(): Promise { const { file, fileFormat, hostname, auth, resource, debug } = parseCliSyncOtArguments(); + // use the hostname and auth token to instantiate a client to talk to OneTrust + const oneTrust = createOneTrustGotInstance({ hostname, auth }); try { // TODO: move to helper function if (resource === OneTrustPullResource.Assessments) { - // use the hostname and auth token to instantiate a client to talk to OneTrust - const oneTrust = createOneTrustGotInstance({ hostname, auth }); - // fetch the list of all assessments in the OneTrust organization const assessments = await getListOfOneTrustAssessments({ oneTrust }); diff --git a/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts b/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts index 5d848a30..2d290e4f 100644 --- a/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts +++ b/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts @@ -25,7 +25,6 @@ export const getListOfOneTrustAssessments = async ({ const allAssessments: OneTrustAssessment[] = []; - logger.info('Getting list of all assessments from OneTrust...'); while (currentPage < totalPages) { // eslint-disable-next-line no-await-in-loop const { body } = await oneTrust.get( diff --git a/src/oneTrust/helpers/enrichOneTrustAssessment.ts b/src/oneTrust/helpers/enrichOneTrustAssessment.ts new file mode 100644 index 00000000..1d9022fe --- /dev/null +++ b/src/oneTrust/helpers/enrichOneTrustAssessment.ts @@ -0,0 +1,67 @@ +import { + OneTrustAssessment, + OneTrustGetAssessmentResponse, + OneTrustGetRiskResponse, +} from '@transcend-io/privacy-types'; +import keyBy from 'lodash/keyBy'; +import { OneTrustEnrichedAssessment } from '../codecs'; + +/** + * Merge the assessment, assessmentDetails, and riskDetails into one object. + * + * @param param - the assessment and risk information + * @returns the assessment enriched with details and risk information + */ +export const enrichOneTrustAssessment = ({ + assessment, + assessmentDetails, + riskDetails, +}: { + /** The OneTrust risk details */ + riskDetails: OneTrustGetRiskResponse[]; + /** The OneTrust assessment as returned from Get List of Assessments endpoint */ + assessment: OneTrustAssessment; + /** The OneTrust assessment details */ + assessmentDetails: OneTrustGetAssessmentResponse; +}): OneTrustEnrichedAssessment => { + const riskDetailsById = keyBy(riskDetails, 'id'); + const { sections, ...restAssessmentDetails } = assessmentDetails; + const sectionsWithEnrichedRisk = sections.map((section) => { + const { questions, ...restSection } = section; + const enrichedQuestions = questions.map((question) => { + const { risks, ...restQuestion } = question; + const enrichedRisks = (risks ?? []).map((risk) => { + const details = riskDetailsById[risk.riskId]; + // TODO: missing the risk meta data and links to the assessment + return { + ...risk, + description: details.description, + name: details.name, + treatment: details.treatment, + treatmentStatus: details.treatmentStatus, + type: details.type, + state: details.state, + stage: details.stage, + result: details.result, + categories: details.categories, + }; + }); + return { + ...restQuestion, + risks: enrichedRisks, + }; + }); + return { + ...restSection, + questions: enrichedQuestions, + }; + }); + + // combine the two assessments into a single enriched result + + return { + ...assessment, + ...restAssessmentDetails, + sections: sectionsWithEnrichedRisk, + }; +}; diff --git a/src/oneTrust/helpers/parseCliSyncOtArguments.ts b/src/oneTrust/helpers/parseCliSyncOtArguments.ts index feabaf44..2992aa2e 100644 --- a/src/oneTrust/helpers/parseCliSyncOtArguments.ts +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -47,19 +47,33 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { if (!dryRun && fileFormat !== OneTrustFileFormat.Csv) { logger.error( colors.red( - 'Must not set the "fileFormat" parameter when "dryRun" is "false".', + `The "fileFormat" parameter must equal ${OneTrustFileFormat.Csv} when "dryRun" is "false".`, ), ); } - if (!file) { + // If trying to sync to disk, must specify a file path + if (dryRun && !file) { logger.error( colors.red( - 'Missing required parameter "file". e.g. --file=./oneTrustAssessments.json', + 'Must set a "file" parameter when "dryRun" is "true". e.g. --file=./oneTrustAssessments.json', ), ); return process.exit(1); } + + // If trying to sync to disk, must specify a file format + if (dryRun && !fileFormat) { + logger.error( + colors.red( + `Must set a "fileFormat" parameter when "dryRun" is "true". e.g. --fileFormat=${ + OneTrustFileFormat.Json + }. Supported formats: ${VALID_FILE_FORMATS.join(',')}`, + ), + ); + return process.exit(1); + } + const splitFile = file.split('.'); if (splitFile.length < 2) { logger.error( diff --git a/src/oneTrust/helpers/syncOneTrustAssessments.ts b/src/oneTrust/helpers/syncOneTrustAssessments.ts new file mode 100644 index 00000000..998c2c1a --- /dev/null +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -0,0 +1,94 @@ +import { Got } from 'got/dist/source'; +import { + getListOfOneTrustAssessments, + getOneTrustAssessment, + getOneTrustRisk, +} from '../endpoints'; +import { map, mapSeries } from 'bluebird'; +import { logger } from '../../logger'; +import { + OneTrustAssessmentQuestion, + OneTrustAssessmentSection, + OneTrustGetRiskResponse, +} from '@transcend-io/privacy-types'; +import uniq from 'lodash/uniq'; +import { enrichOneTrustAssessment } from './enrichOneTrustAssessment'; +import { writeOneTrustAssessment } from './writeOneTrustAssessment'; +import { OneTrustFileFormat } from '../../enums'; + +export const syncOneTrustAssessments = async ({ + oneTrust, + file, + fileFormat, + dryRun, +}: { + /** the OneTrust client instance */ + oneTrust: Got; + /** Whether to write to file instead of syncing to Transcend */ + dryRun: boolean; + /** the path to the file in case dryRun is true */ + file?: string; + /** the format of the file in case dryRun is true */ + fileFormat?: OneTrustFileFormat; +}): Promise => { + // fetch the list of all assessments in the OneTrust organization + logger.info('Getting list of all assessments from OneTrust...'); + const assessments = await getListOfOneTrustAssessments({ oneTrust }); + + /** + * fetch details about one assessment in series and push to transcend or write to disk + * (depending on the dryRun argument) right away to avoid running out of memory + */ + await mapSeries(assessments, async (assessment, index) => { + logger.info( + `Fetching details about assessment ${index + 1} of ${ + assessments.length + }...`, + ); + const assessmentDetails = await getOneTrustAssessment({ + oneTrust, + assessmentId: assessment.assessmentId, + }); + + // enrich assessments with risk information + let riskDetails: OneTrustGetRiskResponse[] = []; + const riskIds = uniq( + assessmentDetails.sections.flatMap((s: OneTrustAssessmentSection) => + s.questions.flatMap((q: OneTrustAssessmentQuestion) => + (q.risks ?? []).flatMap((r) => r.riskId), + ), + ), + ); + if (riskIds.length > 0) { + logger.info( + `Fetching details about ${riskIds.length} risks for assessment ${ + index + 1 + } of ${assessments.length}...`, + ); + riskDetails = await map( + riskIds, + (riskId) => getOneTrustRisk({ oneTrust, riskId: riskId as string }), + { + concurrency: 5, + }, + ); + } + + // enrich the sections with risk details + const enrichedAssessment = enrichOneTrustAssessment({ + assessment, + assessmentDetails, + riskDetails, + }); + + if (dryRun && file && fileFormat) { + writeOneTrustAssessment({ + assessment: enrichedAssessment, + index, + total: assessments.length, + file, + fileFormat, + }); + } + }); +}; From d11609e675c8320a75f1776ed942215997f3b301 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:47:04 +0000 Subject: [PATCH 50/79] call syncOneTrustAssessments from cli-sync-ot --- src/cli-sync-ot.ts | 117 ++------------------------------------------- 1 file changed, 5 insertions(+), 112 deletions(-) diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index bb552c58..e01eae11 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -1,24 +1,10 @@ #!/usr/bin/env node import { logger } from './logger'; -import keyBy from 'lodash/keyBy'; import colors from 'colors'; -import { - getListOfOneTrustAssessments, - getOneTrustAssessment, - writeOneTrustAssessment, - parseCliSyncOtArguments, - createOneTrustGotInstance, - getOneTrustRisk, -} from './oneTrust'; +import { parseCliSyncOtArguments, createOneTrustGotInstance } from './oneTrust'; import { OneTrustPullResource } from './enums'; -import { mapSeries, map } from 'bluebird'; -import uniq from 'lodash/uniq'; -import { - OneTrustAssessmentQuestion, - OneTrustAssessmentSection, - OneTrustGetRiskResponse, -} from '@transcend-io/privacy-types'; +import { syncOneTrustAssessments } from './oneTrust/helpers/syncOneTrustAssessments'; /** * Pull configuration from OneTrust down locally to disk @@ -30,108 +16,15 @@ import { * yarn cli-sync-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json */ async function main(): Promise { - const { file, fileFormat, hostname, auth, resource, debug } = + const { file, fileFormat, hostname, auth, resource, debug, dryRun } = parseCliSyncOtArguments(); + // use the hostname and auth token to instantiate a client to talk to OneTrust const oneTrust = createOneTrustGotInstance({ hostname, auth }); try { - // TODO: move to helper function if (resource === OneTrustPullResource.Assessments) { - // fetch the list of all assessments in the OneTrust organization - const assessments = await getListOfOneTrustAssessments({ oneTrust }); - - /** - * fetch details about one assessment in series and push to transcend or write to disk - * (depending on the dryRun argument) right away to avoid running out of memory - */ - await mapSeries(assessments, async (assessment, index) => { - logger.info( - `Fetching details about assessment ${index + 1} of ${ - assessments.length - }...`, - ); - const assessmentDetails = await getOneTrustAssessment({ - oneTrust, - assessmentId: assessment.assessmentId, - }); - - // enrich assessments with risk information - let riskDetails: OneTrustGetRiskResponse[] = []; - const riskIds = uniq( - assessmentDetails.sections.flatMap((s: OneTrustAssessmentSection) => - s.questions.flatMap((q: OneTrustAssessmentQuestion) => - (q.risks ?? []).flatMap((r) => r.riskId), - ), - ), - ); - if (riskIds.length > 0) { - logger.info( - `Fetching details about ${riskIds.length} risks for assessment ${ - index + 1 - } of ${assessments.length}...`, - ); - riskDetails = await map( - riskIds, - (riskId) => getOneTrustRisk({ oneTrust, riskId: riskId as string }), - { - concurrency: 5, - }, - ); - } - - // TODO: create a helper for this - // enrich the sections with risk details - const riskDetailsById = keyBy(riskDetails, 'id'); - const { sections, ...restAssessmentDetails } = assessmentDetails; - const sectionsWithEnrichedRisk = sections.map((section) => { - const { questions, ...restSection } = section; - const enrichedQuestions = questions.map((question) => { - const { risks, ...restQuestion } = question; - const enrichedRisks = (risks ?? []).map((risk) => { - const details = riskDetailsById[risk.riskId]; - // TODO: missing the risk meta data and links to the assessment - return { - ...risk, - description: details.description, - name: details.name, - treatment: details.treatment, - treatmentStatus: details.treatmentStatus, - type: details.type, - state: details.state, - stage: details.stage, - result: details.result, - categories: details.categories, - }; - }); - return { - ...restQuestion, - risks: enrichedRisks, - }; - }); - return { - ...restSection, - questions: enrichedQuestions, - }; - }); - - // combine the two assessments into a single enriched result - const assessmentWithEnrichedRisk = { - ...restAssessmentDetails, - sections: sectionsWithEnrichedRisk, - }; - - writeOneTrustAssessment({ - assessment: { - ...assessment, - ...assessmentWithEnrichedRisk, - }, - index, - total: assessments.length, - file, - fileFormat, - }); - }); + await syncOneTrustAssessments({ oneTrust, file, fileFormat, dryRun }); } } catch (err) { logger.error( From 7647c02ea1f23b20e6533735ee113a9d5ef6ebe3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 02:59:27 +0000 Subject: [PATCH 51/79] create oneTrustAssessmentToJson helper --- .../helpers/oneTrustAssessmentToJson.ts | 41 +++++++++++++++++++ .../helpers/parseCliSyncOtArguments.ts | 1 + .../helpers/syncOneTrustAssessments.ts | 1 + .../helpers/writeOneTrustAssessment.ts | 24 ++++------- 4 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 src/oneTrust/helpers/oneTrustAssessmentToJson.ts diff --git a/src/oneTrust/helpers/oneTrustAssessmentToJson.ts b/src/oneTrust/helpers/oneTrustAssessmentToJson.ts new file mode 100644 index 00000000..0cc17db7 --- /dev/null +++ b/src/oneTrust/helpers/oneTrustAssessmentToJson.ts @@ -0,0 +1,41 @@ +import { OneTrustEnrichedAssessment } from '../codecs'; + +/** + * Converts the assessment into a json entry. + * + * @param param - information about the assessment and amount of entries + * @returns a stringified json entry ready to be appended to a file + */ +export const oneTrustAssessmentToJson = ({ + assessment, + index, + total, +}: { + /** The assessment to convert */ + assessment: OneTrustEnrichedAssessment; + /** The position of the assessment in the final Json object */ + index: number; + /** The total amount of the assessments in the final Json object */ + total: number; +}): string => { + let jsonEntry = ''; + // start with an opening bracket + if (index === 0) { + jsonEntry = '[\n'; + } + + const stringifiedAssessment = JSON.stringify(assessment, null, 2); + + // Add comma for all items except the last one + const comma = index < total - 1 ? ',' : ''; + + // write to file + jsonEntry = jsonEntry + stringifiedAssessment + comma; + + // end with closing bracket + if (index === total - 1) { + jsonEntry += ']'; + } + + return jsonEntry; +}; diff --git a/src/oneTrust/helpers/parseCliSyncOtArguments.ts b/src/oneTrust/helpers/parseCliSyncOtArguments.ts index 2992aa2e..d8ae3581 100644 --- a/src/oneTrust/helpers/parseCliSyncOtArguments.ts +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -50,6 +50,7 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { `The "fileFormat" parameter must equal ${OneTrustFileFormat.Csv} when "dryRun" is "false".`, ), ); + return process.exit(1); } // If trying to sync to disk, must specify a file path diff --git a/src/oneTrust/helpers/syncOneTrustAssessments.ts b/src/oneTrust/helpers/syncOneTrustAssessments.ts index 998c2c1a..cf7e4720 100644 --- a/src/oneTrust/helpers/syncOneTrustAssessments.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -81,6 +81,7 @@ export const syncOneTrustAssessments = async ({ riskDetails, }); + // sync to file if (dryRun && file && fileFormat) { writeOneTrustAssessment({ assessment: enrichedAssessment, diff --git a/src/oneTrust/helpers/writeOneTrustAssessment.ts b/src/oneTrust/helpers/writeOneTrustAssessment.ts index 2867ab34..173ae381 100644 --- a/src/oneTrust/helpers/writeOneTrustAssessment.ts +++ b/src/oneTrust/helpers/writeOneTrustAssessment.ts @@ -7,6 +7,7 @@ import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from '../constants'; import { decodeCodec } from '@transcend-io/type-utils'; import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; import { OneTrustEnrichedAssessment } from '../codecs'; +import { oneTrustAssessmentToJson } from './oneTrustAssessmentToJson'; /** * Write the assessment to disk at the specified file path. @@ -42,23 +43,12 @@ export const writeOneTrustAssessment = ({ // For json format if (fileFormat === OneTrustFileFormat.Json) { - // start with an opening bracket - if (index === 0) { - fs.writeFileSync(file, '[\n'); - } - - const stringifiedAssessment = JSON.stringify(assessment, null, 2); - - // Add comma for all items except the last one - const comma = index < total - 1 ? ',' : ''; - - // write to file - fs.appendFileSync(file, stringifiedAssessment + comma); - - // end with closing bracket - if (index === total - 1) { - fs.appendFileSync(file, ']'); - } + const jsonEntry = oneTrustAssessmentToJson({ + assessment, + index, + total, + }); + fs.appendFileSync(file, jsonEntry); } else if (fileFormat === OneTrustFileFormat.Csv) { const csvRows = []; From 5dae37dc315c772ef325bc513351529202f1cdd4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 03:05:48 +0000 Subject: [PATCH 52/79] create oneTrustAssessmentToCsv helper --- src/oneTrust/constants.ts | 16 ------ .../helpers/flattenOneTrustAssessment.ts | 1 - .../helpers/oneTrustAssessmentToCsv.ts | 55 +++++++++++++++++++ .../helpers/writeOneTrustAssessment.ts | 51 +++-------------- 4 files changed, 64 insertions(+), 59 deletions(-) delete mode 100644 src/oneTrust/constants.ts create mode 100644 src/oneTrust/helpers/oneTrustAssessmentToCsv.ts diff --git a/src/oneTrust/constants.ts b/src/oneTrust/constants.ts deleted file mode 100644 index ad249a2a..00000000 --- a/src/oneTrust/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createDefaultCodec } from '@transcend-io/type-utils'; -import { OneTrustEnrichedAssessment } from './codecs'; -import { flattenOneTrustAssessment } from './helpers'; - -/** - * An object with default values of type OneTrustEnrichedAssessment. It's very - * valuable when converting assessments to CSV, as it contains all keys that - * make up the CSV header in the expected order - */ -const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustEnrichedAssessment = - createDefaultCodec(OneTrustEnrichedAssessment); - -/** The header of the OneTrust ASsessment CSV file */ -export const DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER = Object.keys( - flattenOneTrustAssessment(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT), -); diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 89f4743d..5609add9 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -14,7 +14,6 @@ import { OneTrustEnrichedAssessmentSection, OneTrustEnrichedRisk, } from '../codecs'; -// import { DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT } from './constants'; // TODO: will have to use something like csv-stringify diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts new file mode 100644 index 00000000..05ac1176 --- /dev/null +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts @@ -0,0 +1,55 @@ +import { decodeCodec } from '@transcend-io/type-utils'; +import { OneTrustEnrichedAssessment } from '../codecs'; +import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; +import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; +import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; + +/** + * Converts the assessment into a csv entry. + * + * @param param - information about the assessment and amount of entries + * @returns a stringified csv entry ready to be appended to a file + */ +export const oneTrustAssessmentToCsv = ({ + assessment, + index, +}: { + /** The assessment to convert */ + assessment: OneTrustEnrichedAssessment; + /** The position of the assessment in the final Json object */ + index: number; +}): string => { + const csvRows = []; + + // write csv header at the beginning of the file + if (index === 0) { + csvRows.push(DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.join(',')); + } + + // flatten the assessment object so it does not have nested properties + const flatAssessment = flattenOneTrustAssessment(assessment); + + // comment + const flatAssessmentFull = Object.fromEntries( + DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map((header) => { + const value = flatAssessment[header] ?? ''; + const escapedValue = + typeof value === 'string' && + (value.includes(',') || value.includes('"')) + ? `"${value.replace(/"/g, '""')}"` + : value; + return [header, escapedValue]; + }), + ); + + // ensure the record has the expected type! + decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); + + // transform the flat assessment to have all CSV keys in the expected order + const assessmentRow = Object.values(flatAssessmentFull); + + // append the rows to the file + csvRows.push(`${assessmentRow.join(',')}\n`); + + return csvRows.join('\n'); +}; diff --git a/src/oneTrust/helpers/writeOneTrustAssessment.ts b/src/oneTrust/helpers/writeOneTrustAssessment.ts index 173ae381..af8e4ab7 100644 --- a/src/oneTrust/helpers/writeOneTrustAssessment.ts +++ b/src/oneTrust/helpers/writeOneTrustAssessment.ts @@ -2,12 +2,9 @@ import { logger } from '../../logger'; import colors from 'colors'; import { OneTrustFileFormat } from '../../enums'; import fs from 'fs'; -import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; -import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from '../constants'; -import { decodeCodec } from '@transcend-io/type-utils'; -import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; import { OneTrustEnrichedAssessment } from '../codecs'; import { oneTrustAssessmentToJson } from './oneTrustAssessmentToJson'; +import { oneTrustAssessmentToCsv } from './oneTrustAssessmentToCsv'; /** * Write the assessment to disk at the specified file path. @@ -41,46 +38,16 @@ export const writeOneTrustAssessment = ({ ), ); - // For json format if (fileFormat === OneTrustFileFormat.Json) { - const jsonEntry = oneTrustAssessmentToJson({ - assessment, - index, - total, - }); - fs.appendFileSync(file, jsonEntry); - } else if (fileFormat === OneTrustFileFormat.Csv) { - const csvRows = []; - - // write csv header at the beginning of the file - if (index === 0) { - csvRows.push(DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.join(',')); - } - - // flatten the assessment object so it does not have nested properties - const flatAssessment = flattenOneTrustAssessment(assessment); - - // comment - const flatAssessmentFull = Object.fromEntries( - DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map((header) => { - const value = flatAssessment[header] ?? ''; - const escapedValue = - typeof value === 'string' && - (value.includes(',') || value.includes('"')) - ? `"${value.replace(/"/g, '""')}"` - : value; - return [header, escapedValue]; + fs.appendFileSync( + file, + oneTrustAssessmentToJson({ + assessment, + index, + total, }), ); - - // ensure the record has the expected type! - decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); - - // transform the flat assessment to have all CSV keys in the expected order - const assessmentRow = Object.values(flatAssessmentFull); - - // append the rows to the file - csvRows.push(`${assessmentRow.join(',')}\n`); - fs.appendFileSync('./oneTrust.csv', csvRows.join('\n')); + } else if (fileFormat === OneTrustFileFormat.Csv) { + fs.appendFileSync(file, oneTrustAssessmentToCsv({ assessment, index })); } }; From 4256788ea592a3de99aaa787e91ce000614b2324 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 03:17:18 +0000 Subject: [PATCH 53/79] create oneTrustAssessmentToCsvRecord helper --- .../helpers/oneTrustAssessmentToCsv.ts | 38 ++++--------------- .../helpers/oneTrustAssessmentToCsvRecord.ts | 36 ++++++++++++++++++ .../helpers/syncOneTrustAssessments.ts | 5 ++- .../helpers/writeOneTrustAssessment.ts | 2 +- 4 files changed, 49 insertions(+), 32 deletions(-) create mode 100644 src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts index 05ac1176..4d46554c 100644 --- a/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts @@ -1,8 +1,5 @@ -import { decodeCodec } from '@transcend-io/type-utils'; import { OneTrustEnrichedAssessment } from '../codecs'; -import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; -import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; -import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; +import { oneTrustAssessmentToCsvRecord } from './oneTrustAssessmentToCsvRecord'; /** * Converts the assessment into a csv entry. @@ -19,37 +16,18 @@ export const oneTrustAssessmentToCsv = ({ /** The position of the assessment in the final Json object */ index: number; }): string => { - const csvRows = []; + const assessmentCsvRecord = oneTrustAssessmentToCsvRecord(assessment); // write csv header at the beginning of the file + const csvRows = []; if (index === 0) { - csvRows.push(DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.join(',')); + const header = Object.keys(assessmentCsvRecord).join(','); + csvRows.push(header); } - // flatten the assessment object so it does not have nested properties - const flatAssessment = flattenOneTrustAssessment(assessment); - - // comment - const flatAssessmentFull = Object.fromEntries( - DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map((header) => { - const value = flatAssessment[header] ?? ''; - const escapedValue = - typeof value === 'string' && - (value.includes(',') || value.includes('"')) - ? `"${value.replace(/"/g, '""')}"` - : value; - return [header, escapedValue]; - }), - ); - - // ensure the record has the expected type! - decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); - - // transform the flat assessment to have all CSV keys in the expected order - const assessmentRow = Object.values(flatAssessmentFull); - - // append the rows to the file - csvRows.push(`${assessmentRow.join(',')}\n`); + // append the row + const row = `${Object.values(assessmentCsvRecord).join(',')}\n`; + csvRows.push(row); return csvRows.join('\n'); }; diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts new file mode 100644 index 00000000..af164be2 --- /dev/null +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts @@ -0,0 +1,36 @@ +import { decodeCodec } from '@transcend-io/type-utils'; +import { OneTrustEnrichedAssessment } from '../codecs'; +import { DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER } from './constants'; +import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; +import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; + +/** + * Converts the assessment into a csv record (header + values). It always + * returns a record with every key in the same order. + * + * @param assessment - the assessment to convert to a csv record + * @returns a stringified csv entry ready to be appended to a file + */ +export const oneTrustAssessmentToCsvRecord = ( + /** The assessment to convert */ + assessment: OneTrustEnrichedAssessment, +): OneTrustAssessmentCsvRecord => { + // flatten the assessment object so it does not have nested properties + const flatAssessment = flattenOneTrustAssessment(assessment); + + // transform the flat assessment to have all CSV keys in the expected order + const flatAssessmentFull = Object.fromEntries( + DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map((header) => { + const value = flatAssessment[header] ?? ''; + const escapedValue = + typeof value === 'string' && + (value.includes(',') || value.includes('"')) + ? `"${value.replace(/"/g, '""')}"` + : value; + return [header, escapedValue]; + }), + ); + + // ensure the record has the expected type! + return decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); +}; diff --git a/src/oneTrust/helpers/syncOneTrustAssessments.ts b/src/oneTrust/helpers/syncOneTrustAssessments.ts index cf7e4720..0790d958 100644 --- a/src/oneTrust/helpers/syncOneTrustAssessments.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -81,8 +81,8 @@ export const syncOneTrustAssessments = async ({ riskDetails, }); - // sync to file if (dryRun && file && fileFormat) { + // sync to file writeOneTrustAssessment({ assessment: enrichedAssessment, index, @@ -90,6 +90,9 @@ export const syncOneTrustAssessments = async ({ file, fileFormat, }); + } else if (fileFormat === OneTrustFileFormat.Csv) { + // sync to transcend + // const csvEntry = oneTrustAssessmentToCsv({ assessment, index }); } }); }; diff --git a/src/oneTrust/helpers/writeOneTrustAssessment.ts b/src/oneTrust/helpers/writeOneTrustAssessment.ts index af8e4ab7..df7b320b 100644 --- a/src/oneTrust/helpers/writeOneTrustAssessment.ts +++ b/src/oneTrust/helpers/writeOneTrustAssessment.ts @@ -32,7 +32,7 @@ export const writeOneTrustAssessment = ({ }): void => { logger.info( colors.magenta( - `Syncing enriched assessment ${ + `Writing enriched assessment ${ index + 1 } of ${total} to file "${file}"...`, ), From 6ec35ba974934f5eba62739e93a9045fa5915aa7 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 03:31:00 +0000 Subject: [PATCH 54/79] update readme --- README.md | 35 +++++++--- .../helpers/parseCliSyncOtArguments.ts | 64 +++++++++++++------ .../helpers/syncOneTrustAssessments.ts | 3 +- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index e233e3a0..01cfa962 100644 --- a/README.md +++ b/README.md @@ -598,21 +598,36 @@ To learn how to generate the token, see the [OAuth 2.0 Scopes](https://developer #### Arguments -| Argument | Description | Type | Default | Required | -| ---------- | ------------------------------------------------------------------------------------------------- | ------- | ----------- | -------- | -| auth | The OAuth access token with the scopes necessary to access the OneTrust Public APIs. | string | N/A | true | -| hostname | The domain of the OneTrust environment from which to pull the resource (e.g. trial.onetrust.com). | string | N/A | true | -| file | Path to the file to pull the resource into. Its format must match the fileFormat argument. | string | N/A | true | -| fileFormat | The format of the output file. | string | csv | false | -| resource | The resource to pull from OneTrust. For now, only assessments is supported. | string | assessments | false | -| debug | Whether to print detailed logs in case of error. | boolean | false | false | -| dryRun | Whether to export the resource to a file rather than sync to Transcend. | boolean | false | false | +| Argument | Description | Type | Default | Required | +| ------------- | ------------------------------------------------------------------------------------------------- | ------- | ----------- | -------- | +| hostname | The domain of the OneTrust environment from which to pull the resource (e.g. trial.onetrust.com). | string | N/A | true | +| oneTrustAuth | The OAuth access token with the scopes necessary to access the OneTrust Public APIs. | string | N/A | true | +| transcendAuth | The Transcend API Key to with the scopes necessary to access Transcend's Public APIs. | string | N/A | false | +| file | Path to the file to pull the resource into. Its format must match the fileFormat argument. | string | N/A | false | +| fileFormat | The format of the output file. | string | csv | false | +| resource | The resource to pull from OneTrust. For now, only assessments is supported. | string | assessments | false | +| dryRun | Whether to export the resource to a file rather than sync to Transcend. | boolean | false | false | +| debug | Whether to print detailed logs in case of error. | boolean | false | false | #### Usage +```sh +# Syncs all assessments from the OneTrust instance to Transcend +tr-sync-ot --hostname=trial.onetrust.com --oneTrustAuth=$ONE_TRUST_OAUTH_TOKEN --transcendAuth=$TRANSCEND_API_KEY +``` + +Alternatively, you can set dryRun to true and sync the resource to disk: + +```sh +# Writes out file to ./oneTrustAssessments.csv +tr-sync-ot --hostname=trial.onetrust.com --oneTrustAuth=$ONE_TRUST_OAUTH_TOKEN --dryRun=true --file=./oneTrustAssessments.csv +``` + +You can also sync to disk in json format: + ```sh # Writes out file to ./oneTrustAssessments.json -tr-sync-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=trial.onetrust.com --file=./oneTrustAssessments.json +tr-sync-ot --hostname=trial.onetrust.com --oneTrustAuth=$ONE_TRUST_OAUTH_TOKEN --dryRun=true --fileFormat=json --file=./oneTrustAssessments.json ``` ### tr-push diff --git a/src/oneTrust/helpers/parseCliSyncOtArguments.ts b/src/oneTrust/helpers/parseCliSyncOtArguments.ts index d8ae3581..6d726cf1 100644 --- a/src/oneTrust/helpers/parseCliSyncOtArguments.ts +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -11,8 +11,10 @@ interface OneTrustCliArguments { file: string; /** The OneTrust hostname to send the requests to */ hostname: string; - /** The OAuth Bearer token used to authenticate the requests */ - auth: string; + /** The OAuth Bearer token used to authenticate the requests to OneTrust */ + oneTrustAuth: string; + /** The Transcend API key to authenticate the requests to Transcend */ + transcendAuth: string; /** The resource to pull from OneTrust */ resource: OneTrustPullResource; /** Whether to enable debugging while reporting errors */ @@ -29,25 +31,50 @@ interface OneTrustCliArguments { * @returns the parsed arguments */ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { - const { file, hostname, auth, resource, debug, fileFormat, dryRun } = yargs( - process.argv.slice(2), - { - string: ['file', 'hostname', 'auth', 'resource', 'fileFormat', 'dryRun'], - boolean: ['debug', 'dryRun'], - default: { - resource: OneTrustPullResource.Assessments, - fileFormat: OneTrustFileFormat.Csv, - debug: false, - dryRun: false, - }, + const { + file, + hostname, + oneTrustAuth, + resource, + debug, + fileFormat, + dryRun, + transcendAuth, + } = yargs(process.argv.slice(2), { + string: [ + 'file', + 'hostname', + 'oneTrustAuth', + 'resource', + 'fileFormat', + 'dryRun', + 'transcendAuth', + ], + boolean: ['debug', 'dryRun'], + default: { + resource: OneTrustPullResource.Assessments, + fileFormat: OneTrustFileFormat.Csv, + debug: false, + dryRun: false, }, - ); + }); + + // Can only sync to Transcend via a CSV file format! + if (!dryRun && !transcendAuth) { + logger.error( + colors.red( + // eslint-disable-next-line no-template-curly-in-string + 'Must specify a "transcendAuth" parameter to sync resources to Transcend. e.g. --transcendAuth=${TRANSCEND_API_KEY}', + ), + ); + return process.exit(1); + } // Can only sync to Transcend via a CSV file format! if (!dryRun && fileFormat !== OneTrustFileFormat.Csv) { logger.error( colors.red( - `The "fileFormat" parameter must equal ${OneTrustFileFormat.Csv} when "dryRun" is "false".`, + `The "fileFormat" parameter must equal ${OneTrustFileFormat.Csv} to sync resources to Transcend.`, ), ); return process.exit(1); @@ -102,10 +129,10 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { return process.exit(1); } - if (!auth) { + if (!oneTrustAuth) { logger.error( colors.red( - 'Missing required parameter "auth". e.g. --auth=$ONE_TRUST_AUTH_TOKEN', + 'Missing required parameter "oneTrustAuth". e.g. --oneTrustAuth=$ONE_TRUST_AUTH_TOKEN', ), ); return process.exit(1); @@ -134,10 +161,11 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { return { file, hostname, - auth, + oneTrustAuth, resource, debug, fileFormat, dryRun, + transcendAuth, }; }; diff --git a/src/oneTrust/helpers/syncOneTrustAssessments.ts b/src/oneTrust/helpers/syncOneTrustAssessments.ts index 0790d958..2ebb4fc9 100644 --- a/src/oneTrust/helpers/syncOneTrustAssessments.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -15,6 +15,7 @@ import uniq from 'lodash/uniq'; import { enrichOneTrustAssessment } from './enrichOneTrustAssessment'; import { writeOneTrustAssessment } from './writeOneTrustAssessment'; import { OneTrustFileFormat } from '../../enums'; +import { oneTrustAssessmentToCsvRecord } from './oneTrustAssessmentToCsvRecord'; export const syncOneTrustAssessments = async ({ oneTrust, @@ -92,7 +93,7 @@ export const syncOneTrustAssessments = async ({ }); } else if (fileFormat === OneTrustFileFormat.Csv) { // sync to transcend - // const csvEntry = oneTrustAssessmentToCsv({ assessment, index }); + // const csvEntry = oneTrustAssessmentToCsvRecord(enrichedAssessment); } }); }; From 5f94a17c1fdee059168c4180714d98edfa73e7f9 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 03:31:40 +0000 Subject: [PATCH 55/79] move constants to heleprs --- src/oneTrust/helpers/constants.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/oneTrust/helpers/constants.ts diff --git a/src/oneTrust/helpers/constants.ts b/src/oneTrust/helpers/constants.ts new file mode 100644 index 00000000..3b74a416 --- /dev/null +++ b/src/oneTrust/helpers/constants.ts @@ -0,0 +1,16 @@ +import { createDefaultCodec } from '@transcend-io/type-utils'; +import { OneTrustEnrichedAssessment } from '../codecs'; +import { flattenOneTrustAssessment } from '.'; + +/** + * An object with default values of type OneTrustEnrichedAssessment. It's very + * valuable when converting assessments to CSV, as it contains all keys that + * make up the CSV header in the expected order + */ +const DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT: OneTrustEnrichedAssessment = + createDefaultCodec(OneTrustEnrichedAssessment); + +/** The header of the OneTrust ASsessment CSV file */ +export const DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER = Object.keys( + flattenOneTrustAssessment(DEFAULT_ONE_TRUST_COMBINED_ASSESSMENT), +); From df76064d1ffd5f4e7595ecc1ebab48b76ade0a9a Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 03:34:21 +0000 Subject: [PATCH 56/79] add transcendUrl to the list of arguments --- README.md | 21 ++++++++++--------- .../helpers/parseCliSyncOtArguments.ts | 14 ++++++++++++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 01cfa962..3df3aca9 100644 --- a/README.md +++ b/README.md @@ -598,16 +598,17 @@ To learn how to generate the token, see the [OAuth 2.0 Scopes](https://developer #### Arguments -| Argument | Description | Type | Default | Required | -| ------------- | ------------------------------------------------------------------------------------------------- | ------- | ----------- | -------- | -| hostname | The domain of the OneTrust environment from which to pull the resource (e.g. trial.onetrust.com). | string | N/A | true | -| oneTrustAuth | The OAuth access token with the scopes necessary to access the OneTrust Public APIs. | string | N/A | true | -| transcendAuth | The Transcend API Key to with the scopes necessary to access Transcend's Public APIs. | string | N/A | false | -| file | Path to the file to pull the resource into. Its format must match the fileFormat argument. | string | N/A | false | -| fileFormat | The format of the output file. | string | csv | false | -| resource | The resource to pull from OneTrust. For now, only assessments is supported. | string | assessments | false | -| dryRun | Whether to export the resource to a file rather than sync to Transcend. | boolean | false | false | -| debug | Whether to print detailed logs in case of error. | boolean | false | false | +| Argument | Description | Type | Default | Required | +| ------------- | ------------------------------------------------------------------------------------------------- | ------------ | ------------------------ | -------- | +| hostname | The domain of the OneTrust environment from which to pull the resource (e.g. trial.onetrust.com). | string | N/A | true | +| oneTrustAuth | The OAuth access token with the scopes necessary to access the OneTrust Public APIs. | string | N/A | true | +| transcendAuth | The Transcend API Key to with the scopes necessary to access Transcend's Public APIs. | string | N/A | false | +| transcendUrl | URL of the Transcend backend. Use https://api.us.transcend.io for US hosting. | string - URL | https://api.transcend.io | false | +| file | Path to the file to pull the resource into. Its format must match the fileFormat argument. | string | N/A | false | +| fileFormat | The format of the output file. | string | csv | false | +| resource | The resource to pull from OneTrust. For now, only assessments is supported. | string | assessments | false | +| dryRun | Whether to export the resource to a file rather than sync to Transcend. | boolean | false | false | +| debug | Whether to print detailed logs in case of error. | boolean | false | false | #### Usage diff --git a/src/oneTrust/helpers/parseCliSyncOtArguments.ts b/src/oneTrust/helpers/parseCliSyncOtArguments.ts index 6d726cf1..b7a59a6c 100644 --- a/src/oneTrust/helpers/parseCliSyncOtArguments.ts +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -40,6 +40,7 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { fileFormat, dryRun, transcendAuth, + transcendUrl, } = yargs(process.argv.slice(2), { string: [ 'file', @@ -49,6 +50,7 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { 'fileFormat', 'dryRun', 'transcendAuth', + 'transcendUrl', ], boolean: ['debug', 'dryRun'], default: { @@ -56,10 +58,11 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { fileFormat: OneTrustFileFormat.Csv, debug: false, dryRun: false, + transcendUrl: 'https://api.transcend.io', }, }); - // Can only sync to Transcend via a CSV file format! + // Must be able to authenticate to transcend to sync resources to it if (!dryRun && !transcendAuth) { logger.error( colors.red( @@ -69,6 +72,15 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { ); return process.exit(1); } + if (!dryRun && !transcendUrl) { + logger.error( + colors.red( + // eslint-disable-next-line max-len + 'Must specify a "transcendUrl" parameter to sync resources to Transcend. e.g. --transcendUrl=https://api.transcend.io', + ), + ); + return process.exit(1); + } // Can only sync to Transcend via a CSV file format! if (!dryRun && fileFormat !== OneTrustFileFormat.Csv) { From b424c03be0254da26c5c06e41bb9725945411224 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:19:20 +0000 Subject: [PATCH 57/79] add ability to sync to Transcend --- src/cli-sync-ot.ts | 31 +++++++-- src/codecs.ts | 28 +++++++++ src/graphql/gqls/assessment.ts | 13 ++++ src/oneTrust/helpers/index.ts | 2 +- .../helpers/parseCliSyncOtArguments.ts | 38 ++++++----- ...ent.ts => syncOneTrustAssessmentToDisk.ts} | 2 +- .../syncOneTrustAssessmentToTranscend.ts | 63 +++++++++++++++++++ .../helpers/syncOneTrustAssessments.ts | 24 +++++-- 8 files changed, 174 insertions(+), 27 deletions(-) rename src/oneTrust/helpers/{writeOneTrustAssessment.ts => syncOneTrustAssessmentToDisk.ts} (96%) create mode 100644 src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index e01eae11..751981ac 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -5,6 +5,7 @@ import colors from 'colors'; import { parseCliSyncOtArguments, createOneTrustGotInstance } from './oneTrust'; import { OneTrustPullResource } from './enums'; import { syncOneTrustAssessments } from './oneTrust/helpers/syncOneTrustAssessments'; +import { buildTranscendGraphQLClient } from './graphql'; /** * Pull configuration from OneTrust down locally to disk @@ -16,15 +17,37 @@ import { syncOneTrustAssessments } from './oneTrust/helpers/syncOneTrustAssessme * yarn cli-sync-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json */ async function main(): Promise { - const { file, fileFormat, hostname, auth, resource, debug, dryRun } = - parseCliSyncOtArguments(); + const { + file, + fileFormat, + hostname, + oneTrustAuth, + transcendAuth, + transcendUrl, + resource, + debug, + dryRun, + } = parseCliSyncOtArguments(); // use the hostname and auth token to instantiate a client to talk to OneTrust - const oneTrust = createOneTrustGotInstance({ hostname, auth }); + const oneTrust = createOneTrustGotInstance({ hostname, auth: oneTrustAuth }); try { if (resource === OneTrustPullResource.Assessments) { - await syncOneTrustAssessments({ oneTrust, file, fileFormat, dryRun }); + await syncOneTrustAssessments({ + oneTrust, + file, + fileFormat, + dryRun, + ...(transcendAuth && transcendUrl + ? { + transcend: buildTranscendGraphQLClient( + transcendUrl, + transcendAuth, + ), + } + : {}), + }); } } catch (err) { logger.error( diff --git a/src/codecs.ts b/src/codecs.ts index ae052b36..ef63a602 100644 --- a/src/codecs.ts +++ b/src/codecs.ts @@ -2073,3 +2073,31 @@ export const PathfinderPromptRunMetadata = t.partial({ export type PathfinderPromptRunMetadata = t.TypeOf< typeof PathfinderPromptRunMetadata >; + +/** The columns of a row of a OneTrust Assessment form to import into Transcend. */ +const OneTrustAssessmentColumnInput = t.intersection([ + t.type({ + /** The title of the column */ + title: t.string, + }), + t.partial({ + /** The optional value of the column */ + value: t.string, + }), +]); + +/** A row with information of the OneTrust assessment form to import into Transcend */ +const OneTrustAssessmentRowInput = t.type({ + /** A list of columns within this row. */ + columns: t.array(OneTrustAssessmentColumnInput), +}); + +/** Input for importing multiple OneTrust assessment forms into Transcend */ +export const ImportOnetrustAssessmentsInput = t.type({ + /** 'The rows of the CSV file.' */ + rows: t.array(OneTrustAssessmentRowInput), +}); +/** Type override */ +export type ImportOnetrustAssessmentsInput = t.TypeOf< + typeof ImportOnetrustAssessmentsInput +>; diff --git a/src/graphql/gqls/assessment.ts b/src/graphql/gqls/assessment.ts index 6b8121fc..d0fe9313 100644 --- a/src/graphql/gqls/assessment.ts +++ b/src/graphql/gqls/assessment.ts @@ -283,3 +283,16 @@ export const ASSESSMENTS = gql` } } `; + +export const IMPORT_ONE_TRUST_ASSESSMENT_FORMS = gql` + mutation TranscendCliImportOneTrustAssessmentForms( + $input: ImportOnetrustAssessmentsInput! + ) { + importOneTrustAssessmentForms(input: $input) { + assessmentForms { + id + title + } + } + } +`; diff --git a/src/oneTrust/helpers/index.ts b/src/oneTrust/helpers/index.ts index 504bb11e..cb7534f6 100644 --- a/src/oneTrust/helpers/index.ts +++ b/src/oneTrust/helpers/index.ts @@ -1,3 +1,3 @@ export * from './flattenOneTrustAssessment'; export * from './parseCliSyncOtArguments'; -export * from './writeOneTrustAssessment'; +export * from './syncOneTrustAssessmentToDisk'; diff --git a/src/oneTrust/helpers/parseCliSyncOtArguments.ts b/src/oneTrust/helpers/parseCliSyncOtArguments.ts index b7a59a6c..f6c8463c 100644 --- a/src/oneTrust/helpers/parseCliSyncOtArguments.ts +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -15,6 +15,8 @@ interface OneTrustCliArguments { oneTrustAuth: string; /** The Transcend API key to authenticate the requests to Transcend */ transcendAuth: string; + /** The Transcend URL where to forward requests */ + transcendUrl: string; /** The resource to pull from OneTrust */ resource: OneTrustPullResource; /** Whether to enable debugging while reporting errors */ @@ -114,22 +116,25 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { return process.exit(1); } - const splitFile = file.split('.'); - if (splitFile.length < 2) { - logger.error( - colors.red( - 'The "file" parameter has an invalid format. Expected a path with extensions. e.g. --file=./pathToFile.json.', - ), - ); - return process.exit(1); - } - if (splitFile.at(-1) !== fileFormat) { - logger.error( - colors.red( - `The "file" and "fileFormat" parameters must specify the same format! Got file=${file} and fileFormat=${fileFormat}`, - ), - ); - return process.exit(1); + if (file) { + const splitFile = file.split('.'); + if (splitFile.length < 2) { + logger.error( + colors.red( + 'The "file" parameter has an invalid format. Expected a path with extensions. e.g. --file=./pathToFile.json.', + ), + ); + return process.exit(1); + } + if (splitFile.at(-1) !== fileFormat) { + logger.error( + colors.red( + // eslint-disable-next-line max-len + `The "file" and "fileFormat" parameters must specify the same format! Got file=${file} and fileFormat=${fileFormat}`, + ), + ); + return process.exit(1); + } } if (!hostname) { @@ -179,5 +184,6 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { fileFormat, dryRun, transcendAuth, + transcendUrl, }; }; diff --git a/src/oneTrust/helpers/writeOneTrustAssessment.ts b/src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts similarity index 96% rename from src/oneTrust/helpers/writeOneTrustAssessment.ts rename to src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts index df7b320b..504515e9 100644 --- a/src/oneTrust/helpers/writeOneTrustAssessment.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts @@ -12,7 +12,7 @@ import { oneTrustAssessmentToCsv } from './oneTrustAssessmentToCsv'; * * @param param - information about the assessment to write */ -export const writeOneTrustAssessment = ({ +export const syncOneTrustAssessmentToDisk = ({ file, fileFormat, assessment, diff --git a/src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts b/src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts new file mode 100644 index 00000000..f728ad58 --- /dev/null +++ b/src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts @@ -0,0 +1,63 @@ +import { logger } from '../../logger'; +import { oneTrustAssessmentToCsvRecord } from './oneTrustAssessmentToCsvRecord'; +import { GraphQLClient } from 'graphql-request'; +import { + IMPORT_ONE_TRUST_ASSESSMENT_FORMS, + makeGraphQLRequest, +} from '../../graphql'; +import { ImportOnetrustAssessmentsInput } from '../../codecs'; +import { OneTrustEnrichedAssessment } from '../codecs'; + +export interface AssessmentForm { + /** ID of Assessment Form */ + id: string; + /** Title of Assessment Form */ + name: string; +} + +/** + * Write the assessment to a Transcend instance. + * + * + * @param param - information about the assessment and Transcend instance to write to + */ +export const syncOneTrustAssessmentToTranscend = async ({ + transcend, + assessment, +}: { + /** the Transcend client instance */ + transcend: GraphQLClient; + /** the assessment to sync to Transcend */ + assessment: OneTrustEnrichedAssessment; +}): Promise => { + logger.info(); + + // convert the OneTrust assessment object into a CSV Record (a map from the csv header to values) + const csvRecord = oneTrustAssessmentToCsvRecord(assessment); + + // transform the csv record into a valid input to the mutation + const input: ImportOnetrustAssessmentsInput = { + rows: [ + { + columns: Object.entries(csvRecord).map(([key, value]) => ({ + title: key, + value: value.toString(), + })), + }, + ], + }; + + const { + importOneTrustAssessmentForms: { assessmentForms }, + } = await makeGraphQLRequest<{ + /** the importOneTrustAssessmentForms mutation */ + importOneTrustAssessmentForms: { + /** Created Assessment Forms */ + assessmentForms: AssessmentForm[]; + }; + }>(transcend, IMPORT_ONE_TRUST_ASSESSMENT_FORMS, { + input, + }); + + return assessmentForms[0]; +}; diff --git a/src/oneTrust/helpers/syncOneTrustAssessments.ts b/src/oneTrust/helpers/syncOneTrustAssessments.ts index 2ebb4fc9..b4894cdd 100644 --- a/src/oneTrust/helpers/syncOneTrustAssessments.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -13,18 +13,29 @@ import { } from '@transcend-io/privacy-types'; import uniq from 'lodash/uniq'; import { enrichOneTrustAssessment } from './enrichOneTrustAssessment'; -import { writeOneTrustAssessment } from './writeOneTrustAssessment'; +import { syncOneTrustAssessmentToDisk } from './syncOneTrustAssessmentToDisk'; import { OneTrustFileFormat } from '../../enums'; -import { oneTrustAssessmentToCsvRecord } from './oneTrustAssessmentToCsvRecord'; +import { GraphQLClient } from 'graphql-request'; +import { syncOneTrustAssessmentToTranscend } from './syncOneTrustAssessmentToTranscend'; + +export interface AssessmentForm { + /** ID of Assessment Form */ + id: string; + /** Title of Assessment Form */ + name: string; +} export const syncOneTrustAssessments = async ({ oneTrust, file, fileFormat, dryRun, + transcend, }: { /** the OneTrust client instance */ oneTrust: Got; + /** the Transcend client instance */ + transcend?: GraphQLClient; /** Whether to write to file instead of syncing to Transcend */ dryRun: boolean; /** the path to the file in case dryRun is true */ @@ -84,16 +95,19 @@ export const syncOneTrustAssessments = async ({ if (dryRun && file && fileFormat) { // sync to file - writeOneTrustAssessment({ + syncOneTrustAssessmentToDisk({ assessment: enrichedAssessment, index, total: assessments.length, file, fileFormat, }); - } else if (fileFormat === OneTrustFileFormat.Csv) { + } else if (fileFormat === OneTrustFileFormat.Csv && transcend) { // sync to transcend - // const csvEntry = oneTrustAssessmentToCsvRecord(enrichedAssessment); + await syncOneTrustAssessmentToTranscend({ + assessment: enrichedAssessment, + transcend, + }); } }); }; From 414e84e8530ff9767128037784fc7af97cb7e02e Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:21:32 +0000 Subject: [PATCH 58/79] update error messages --- src/cli-sync-ot.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index 751981ac..e7981ccd 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -52,7 +52,7 @@ async function main(): Promise { } catch (err) { logger.error( colors.red( - `An error occurred pulling the resource ${resource} from OneTrust: ${ + `An error occurred syncing the resource ${resource} from OneTrust: ${ debug ? err.stack : err.message }`, ), @@ -63,7 +63,9 @@ async function main(): Promise { // Indicate success logger.info( colors.green( - `Successfully synced OneTrust ${resource} to disk at "${file}"!`, + `Successfully synced OneTrust ${resource} to ${ + dryRun ? `disk at "${file}"` : 'Transcend' + }!`, ), ); } From 70956b62595b703a77bac136bf7167aaf5b5742e Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:24:45 +0000 Subject: [PATCH 59/79] update extractProperties documentation --- src/helpers/extractProperties.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/extractProperties.ts b/src/helpers/extractProperties.ts index 54a26e77..e5ad7452 100644 --- a/src/helpers/extractProperties.ts +++ b/src/helpers/extractProperties.ts @@ -50,7 +50,7 @@ type ExtractedArrayProperties = { * { id: 2, name: 'Jane', age: 30, city: 'LA' } * ] * const result = extractProperties(items, ['id', 'name']); - * // Returns: { id: number[], name: string[], rest: {age: number, city: string}[] } + * // Returns: { id: [1, 2], name: ['John', 'Jane'], rest: [{age: 25, city: 'NY'}, {age: 30, city: 'LA'}] } */ export const extractProperties = ( items: T[], From a8500c662d341cb47c6f3c4473847108089d6946 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:25:17 +0000 Subject: [PATCH 60/79] add fixme comment --- src/helpers/extractProperties.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/extractProperties.ts b/src/helpers/extractProperties.ts index e5ad7452..dfa6bc7d 100644 --- a/src/helpers/extractProperties.ts +++ b/src/helpers/extractProperties.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable @typescript-eslint/no-explicit-any */ +// FIXME: move to @transcend/type-utils /** * Type that represents the extracted properties from an object type T. * For each property K from T, creates an array of that property's type. From ae821ae490b6b8ee8400963170dcdf1ed71e50f0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:40:44 +0000 Subject: [PATCH 61/79] write docs for aggregateObjects --- .../helpers/flattenOneTrustAssessment.ts | 33 ++++++++++++++----- .../helpers/oneTrustAssessmentToCsvRecord.ts | 4 +-- .../helpers/syncOneTrustAssessments.ts | 4 +-- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 5609add9..ae24381a 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -15,9 +15,7 @@ import { OneTrustEnrichedRisk, } from '../codecs'; -// TODO: will have to use something like csv-stringify - -// TODO: test what happens when a value is null -> it should convert to '' +// FIXME: move to @transcend/type-utils, document and write tests const flattenObject = (obj: any, prefix = ''): any => Object.keys(obj ?? []).reduce((acc, key) => { const newKey = prefix ? `${prefix}_${key}` : key; @@ -42,22 +40,42 @@ const flattenObject = (obj: any, prefix = ''): any => return acc; }, {} as Record); -// TODO: move to helpers +// FIXME: move to @transcend/type-utils +/** + * Aggregates multiple objects into a single object by combining values of matching keys. + * For each key present in any of the input objects, creates a comma-separated string + * of values from all objects. + * + * @param param - the objects to aggregate and the aggregation method + * @returns a single object containing all unique keys with aggregated values + * @example + * const obj1 = { name: 'John', age: 30 }; + * const obj2 = { name: 'Jane', city: 'NY' }; + * const obj3 = { name: 'Bob', age: 25 }; + * + * // Without wrap + * aggregateObjects({ objs: [obj1, obj2, obj3] }) + * // Returns: { name: 'John,Jane,Bob', age: '30,,25', city: ',NY,' } + * + * // With wrap + * aggregateObjects({ objs: [obj1, obj2, obj3], wrap: true }) + * // Returns: { name: '[John],[Jane],[Bob]', age: '[30],[],[25]', city: '[],[NY],[]' } + */ const aggregateObjects = ({ objs, wrap = false, }: { /** the objects to aggregate in a single one */ objs: any[]; - /** whether to wrap the values in a [] */ + /** whether to wrap the concatenated values in a [] */ wrap?: boolean; }): any => { const allKeys = Array.from(new Set(objs.flatMap((a) => Object.keys(a)))); - // build a single object where all the keys contain the respective values of objs + // Reduce into a single object, where each key contains concatenated values from all input objects return allKeys.reduce((acc, key) => { const values = objs - .map((a) => (wrap ? `[${a[key] ?? ''}]` : a[key] ?? '')) + .map((o) => (wrap ? `[${o[key] ?? ''}]` : o[key] ?? '')) .join(','); acc[key] = values; return acc; @@ -80,7 +98,6 @@ const flattenOneTrustNestedQuestions = ( questions: OneTrustAssessmentNestedQuestion[], prefix: string, ): any => { - // TODO: how do extract properties handle null const { options: allOptions, rest: restQuestions } = extractProperties( questions, ['options'], diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts index af164be2..ee704528 100644 --- a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts @@ -5,8 +5,8 @@ import { flattenOneTrustAssessment } from './flattenOneTrustAssessment'; import { OneTrustAssessmentCsvRecord } from '@transcend-io/privacy-types'; /** - * Converts the assessment into a csv record (header + values). It always - * returns a record with every key in the same order. + * Converts the assessment into a csv record (i.e. a map from the csv header + * to values). It always returns a record with every key in the same order. * * @param assessment - the assessment to convert to a csv record * @returns a stringified csv entry ready to be appended to a file diff --git a/src/oneTrust/helpers/syncOneTrustAssessments.ts b/src/oneTrust/helpers/syncOneTrustAssessments.ts index b4894cdd..8ee4e88a 100644 --- a/src/oneTrust/helpers/syncOneTrustAssessments.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -48,7 +48,7 @@ export const syncOneTrustAssessments = async ({ const assessments = await getListOfOneTrustAssessments({ oneTrust }); /** - * fetch details about one assessment in series and push to transcend or write to disk + * fetch details about each assessment in series and write to transcend or to disk * (depending on the dryRun argument) right away to avoid running out of memory */ await mapSeries(assessments, async (assessment, index) => { @@ -86,7 +86,7 @@ export const syncOneTrustAssessments = async ({ ); } - // enrich the sections with risk details + // enrich the assessments with risk and details const enrichedAssessment = enrichOneTrustAssessment({ assessment, assessmentDetails, From ca7f66b99d1031a9ff23b2062cfba15c798e1572 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:43:35 +0000 Subject: [PATCH 62/79] document flattenObject --- .../helpers/flattenOneTrustAssessment.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index ae24381a..c5c1f5bc 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -15,7 +15,34 @@ import { OneTrustEnrichedRisk, } from '../codecs'; -// FIXME: move to @transcend/type-utils, document and write tests +/** + * FIXME: move to @transcend/type-utils + * + * Flattens a nested object into a single-level object with concatenated key names. + * + * @param obj - The object to flatten + * @param prefix - The prefix to prepend to keys (used in recursion) + * @returns A flattened object where nested keys are joined with underscores + * @example + * const nested = { + * user: { + * name: 'John', + * address: { + * city: 'NY', + * zip: 10001 + * }, + * hobbies: ['reading', 'gaming'] + * } + * }; + * + * flattenObject(nested) + * // Returns: { + * // user_name: 'John', + * // user_address_city: 'NY', + * // user_address_zip: 10001, + * // user_hobbies: 'reading,gaming' + * // } + */ const flattenObject = (obj: any, prefix = ''): any => Object.keys(obj ?? []).reduce((acc, key) => { const newKey = prefix ? `${prefix}_${key}` : key; @@ -40,8 +67,9 @@ const flattenObject = (obj: any, prefix = ''): any => return acc; }, {} as Record); -// FIXME: move to @transcend/type-utils /** + * FIXME: move to @transcend/type-utils + * * Aggregates multiple objects into a single object by combining values of matching keys. * For each key present in any of the input objects, creates a comma-separated string * of values from all objects. From 7692cd4aba42e79dcbc6438a6d98c4fa935bdc5c Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:45:11 +0000 Subject: [PATCH 63/79] remove some TODOs --- src/oneTrust/helpers/flattenOneTrustAssessment.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index c5c1f5bc..4d138eb8 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -184,7 +184,6 @@ const flattenOneTrustRisks = ( allRisks: (OneTrustEnrichedRisk[] | null)[], prefix: string, ): any => { - // TODO: extract categories and other nested properties const allRisksFlat = (allRisks ?? []).map((risks) => { const { categories, rest: restRisks } = extractProperties(risks ?? [], [ 'categories', @@ -206,7 +205,6 @@ const flattenOneTrustQuestions = ( ): any => { const allSectionQuestionsFlat = allSectionQuestions.map( (sectionQuestions) => { - // extract nested properties (TODO: try to make a helper for this!!!) const { rest: restSectionQuestions, question: questions, @@ -258,7 +256,6 @@ const flattenOneTrustSectionHeaders = ( }; }; -// TODO: update type to be const flattenOneTrustSections = ( sections: OneTrustEnrichedAssessmentSection[], prefix: string, @@ -280,7 +277,6 @@ const flattenOneTrustSections = ( return { ...sectionsFlat, ...headersFlat, ...questionsFlat }; }; -// TODO: update type to be a Record export const flattenOneTrustAssessment = ( combinedAssessment: OneTrustEnrichedAssessment, ): Record => { From eda427b950d7c72bca833cc125de9cd33b8c1c64 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:49:03 +0000 Subject: [PATCH 64/79] add TODOs --- src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts index ee704528..5d4085f4 100644 --- a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts @@ -31,6 +31,13 @@ export const oneTrustAssessmentToCsvRecord = ( }), ); + /** + * TODO: test that this is actually doing something. + * For example + * - does it fail if flatAssessmentFull has extra properties + * - does it fail if flatAssessmentFull is missing properties + * + */ // ensure the record has the expected type! return decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); }; From f8ef92ee490c5371655a92c44379a3f1f76f7b96 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:50:44 +0000 Subject: [PATCH 65/79] remove TODOs --- src/oneTrust/helpers/flattenOneTrustAssessment.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 4d138eb8..f748a105 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -290,7 +290,6 @@ export const flattenOneTrustAssessment = ( ...rest } = combinedAssessment; - // TODO: extract approver from approvers, otherwise it won't agree with the codec const flatApprovers = approvers.map((approver) => flattenObject(approver, 'approvers'), ); From e4176bea25881cb97927957b4444b15633af130a Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:51:36 +0000 Subject: [PATCH 66/79] add a fixme --- src/oneTrust/helpers/enrichOneTrustAssessment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oneTrust/helpers/enrichOneTrustAssessment.ts b/src/oneTrust/helpers/enrichOneTrustAssessment.ts index 1d9022fe..d943c4a5 100644 --- a/src/oneTrust/helpers/enrichOneTrustAssessment.ts +++ b/src/oneTrust/helpers/enrichOneTrustAssessment.ts @@ -32,7 +32,7 @@ export const enrichOneTrustAssessment = ({ const { risks, ...restQuestion } = question; const enrichedRisks = (risks ?? []).map((risk) => { const details = riskDetailsById[risk.riskId]; - // TODO: missing the risk meta data and links to the assessment + // FIXME: missing the risk meta data and links to the assessment return { ...risk, description: details.description, From 66bfc711bc9e0283f85d84db52ec0179ca434841 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 17:14:18 +0000 Subject: [PATCH 67/79] simplify flattenOneTrustAssessment --- .../helpers/flattenOneTrustAssessment.ts | 148 ++++-------------- 1 file changed, 28 insertions(+), 120 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index f748a105..86d38db7 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -14,108 +14,17 @@ import { OneTrustEnrichedAssessmentSection, OneTrustEnrichedRisk, } from '../codecs'; - -/** - * FIXME: move to @transcend/type-utils - * - * Flattens a nested object into a single-level object with concatenated key names. - * - * @param obj - The object to flatten - * @param prefix - The prefix to prepend to keys (used in recursion) - * @returns A flattened object where nested keys are joined with underscores - * @example - * const nested = { - * user: { - * name: 'John', - * address: { - * city: 'NY', - * zip: 10001 - * }, - * hobbies: ['reading', 'gaming'] - * } - * }; - * - * flattenObject(nested) - * // Returns: { - * // user_name: 'John', - * // user_address_city: 'NY', - * // user_address_zip: 10001, - * // user_hobbies: 'reading,gaming' - * // } - */ -const flattenObject = (obj: any, prefix = ''): any => - Object.keys(obj ?? []).reduce((acc, key) => { - const newKey = prefix ? `${prefix}_${key}` : key; - - const entry = obj[key]; - if (typeof entry === 'object' && entry !== null && !Array.isArray(entry)) { - Object.assign(acc, flattenObject(entry, newKey)); - } else { - acc[newKey] = Array.isArray(entry) - ? entry - .map((e) => { - if (typeof e === 'string') { - return e.replaceAll(',', ''); - } - return e ?? ''; - }) - .join(',') - : typeof entry === 'string' - ? entry.replaceAll(',', '') - : entry ?? ''; - } - return acc; - }, {} as Record); - -/** - * FIXME: move to @transcend/type-utils - * - * Aggregates multiple objects into a single object by combining values of matching keys. - * For each key present in any of the input objects, creates a comma-separated string - * of values from all objects. - * - * @param param - the objects to aggregate and the aggregation method - * @returns a single object containing all unique keys with aggregated values - * @example - * const obj1 = { name: 'John', age: 30 }; - * const obj2 = { name: 'Jane', city: 'NY' }; - * const obj3 = { name: 'Bob', age: 25 }; - * - * // Without wrap - * aggregateObjects({ objs: [obj1, obj2, obj3] }) - * // Returns: { name: 'John,Jane,Bob', age: '30,,25', city: ',NY,' } - * - * // With wrap - * aggregateObjects({ objs: [obj1, obj2, obj3], wrap: true }) - * // Returns: { name: '[John],[Jane],[Bob]', age: '[30],[],[25]', city: '[],[NY],[]' } - */ -const aggregateObjects = ({ - objs, - wrap = false, -}: { - /** the objects to aggregate in a single one */ - objs: any[]; - /** whether to wrap the concatenated values in a [] */ - wrap?: boolean; -}): any => { - const allKeys = Array.from(new Set(objs.flatMap((a) => Object.keys(a)))); - - // Reduce into a single object, where each key contains concatenated values from all input objects - return allKeys.reduce((acc, key) => { - const values = objs - .map((o) => (wrap ? `[${o[key] ?? ''}]` : o[key] ?? '')) - .join(','); - acc[key] = values; - return acc; - }, {} as Record); -}; +import { flattenObject } from './flattenObject'; +import { aggregateObjects } from './aggregateObjects'; const flattenOneTrustNestedQuestionsOptions = ( allOptions: (OneTrustAssessmentQuestionOption[] | null)[], prefix: string, ): any => { const allOptionsFlat = allOptions.map((options) => { - const flatOptions = (options ?? []).map((o) => flattenObject(o, prefix)); + const flatOptions = (options ?? []).map((o) => + flattenObject({ obj: o, prefix }), + ); return aggregateObjects({ objs: flatOptions }); }); @@ -131,7 +40,9 @@ const flattenOneTrustNestedQuestions = ( ['options'], ); - const restQuestionsFlat = restQuestions.map((r) => flattenObject(r, prefix)); + const restQuestionsFlat = restQuestions.map((r) => + flattenObject({ obj: r, prefix }), + ); return { ...aggregateObjects({ objs: restQuestionsFlat }), ...flattenOneTrustNestedQuestionsOptions(allOptions, `${prefix}_options`), @@ -155,10 +66,10 @@ const flattenOneTrustQuestionResponses = ( ); const responsesFlat = (responses ?? []).map((r) => - flattenObject(r, prefix), + flattenObject({ obj: r, prefix }), ); const restQuestionResponsesFlat = (restQuestionResponses ?? []).map((q) => - flattenObject(q, prefix), + flattenObject({ obj: q, prefix }), ); return { ...aggregateObjects({ objs: responsesFlat }), @@ -174,7 +85,9 @@ const flattenOneTrustRiskCategories = ( prefix: string, ): any => { const allCategoriesFlat = (allCategories ?? []).map((categories) => { - const flatCategories = categories.map((c) => flattenObject(c, prefix)); + const flatCategories = categories.map((c) => + flattenObject({ obj: c, prefix }), + ); return aggregateObjects({ objs: flatCategories }); }); return aggregateObjects({ objs: allCategoriesFlat, wrap: true }); @@ -189,7 +102,9 @@ const flattenOneTrustRisks = ( 'categories', ]); - const flatRisks = (restRisks ?? []).map((r) => flattenObject(r, prefix)); + const flatRisks = (restRisks ?? []).map((r) => + flattenObject({ obj: r, prefix }), + ); return { ...aggregateObjects({ objs: flatRisks }), ...flattenOneTrustRiskCategories(categories, `${prefix}_categories`), @@ -217,7 +132,7 @@ const flattenOneTrustQuestions = ( ]); const restSectionQuestionsFlat = restSectionQuestions.map((q) => - flattenObject(q, prefix), + flattenObject({ obj: q, prefix }), ); return { @@ -246,9 +161,11 @@ const flattenOneTrustSectionHeaders = ( 'riskStatistics', ]); - const flatFlatHeaders = restHeaders.map((h) => flattenObject(h, prefix)); + const flatFlatHeaders = restHeaders.map((h) => + flattenObject({ obj: h, prefix }), + ); const flatRiskStatistics = riskStatistics.map((r) => - flattenObject(r, `${prefix}_riskStatistics`), + flattenObject({ obj: r, prefix: `${prefix}_riskStatistics` }), ); return { ...aggregateObjects({ objs: flatFlatHeaders }), @@ -266,7 +183,9 @@ const flattenOneTrustSections = ( rest: restSections, } = extractProperties(sections, ['questions', 'header']); - const restSectionsFlat = restSections.map((s) => flattenObject(s, prefix)); + const restSectionsFlat = restSections.map((s) => + flattenObject({ obj: s, prefix }), + ); const sectionsFlat = aggregateObjects({ objs: restSectionsFlat }); const headersFlat = flattenOneTrustSectionHeaders(headers, prefix); const questionsFlat = flattenOneTrustQuestions( @@ -290,22 +209,11 @@ export const flattenOneTrustAssessment = ( ...rest } = combinedAssessment; - const flatApprovers = approvers.map((approver) => - flattenObject(approver, 'approvers'), - ); - const flatRespondents = respondents.map((respondent) => - flattenObject(respondent, 'respondents'), - ); - const flatPrimaryEntityDetails = primaryEntityDetails.map( - (primaryEntityDetail) => - flattenObject(primaryEntityDetail, 'primaryEntityDetails'), - ); - return { - ...flattenObject(rest), - ...aggregateObjects({ objs: flatApprovers }), - ...aggregateObjects({ objs: flatRespondents }), - ...aggregateObjects({ objs: flatPrimaryEntityDetails }), + ...flattenObject({ obj: rest }), + ...flattenObject({ obj: { approvers } }), + ...flattenObject({ obj: { respondents } }), + ...flattenObject({ obj: { primaryEntityDetails } }), ...flattenOneTrustSections(sections, 'sections'), }; }; From b34e3f990be1aa9bcce4c1f4d34ac208a613d297 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 17:17:03 +0000 Subject: [PATCH 68/79] improve flattenOneTrustSections --- src/oneTrust/helpers/flattenOneTrustAssessment.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 86d38db7..04275935 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -183,10 +183,7 @@ const flattenOneTrustSections = ( rest: restSections, } = extractProperties(sections, ['questions', 'header']); - const restSectionsFlat = restSections.map((s) => - flattenObject({ obj: s, prefix }), - ); - const sectionsFlat = aggregateObjects({ objs: restSectionsFlat }); + const sectionsFlat = flattenObject({ obj: { sections: restSections } }); const headersFlat = flattenOneTrustSectionHeaders(headers, prefix); const questionsFlat = flattenOneTrustQuestions( allQuestions, From b65036b6c8c35c053b00a4b62ecd59f58166a728 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 17:22:46 +0000 Subject: [PATCH 69/79] improve flattenOneTrustSectionHeaders --- src/oneTrust/helpers/flattenOneTrustAssessment.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 04275935..8775687d 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -164,12 +164,9 @@ const flattenOneTrustSectionHeaders = ( const flatFlatHeaders = restHeaders.map((h) => flattenObject({ obj: h, prefix }), ); - const flatRiskStatistics = riskStatistics.map((r) => - flattenObject({ obj: r, prefix: `${prefix}_riskStatistics` }), - ); return { ...aggregateObjects({ objs: flatFlatHeaders }), - ...aggregateObjects({ objs: flatRiskStatistics }), + ...flattenObject({ obj: { riskStatistics }, prefix }), }; }; From bd5e882efe4239ed24e046bb13fcc38cbe3cf3b3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 17:46:12 +0000 Subject: [PATCH 70/79] simplify flatten functions --- .../helpers/flattenOneTrustAssessment.ts | 85 +++++++------------ .../helpers/oneTrustAssessmentToCsvRecord.ts | 7 -- 2 files changed, 33 insertions(+), 59 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 8775687d..2e0b1022 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -21,13 +21,9 @@ const flattenOneTrustNestedQuestionsOptions = ( allOptions: (OneTrustAssessmentQuestionOption[] | null)[], prefix: string, ): any => { - const allOptionsFlat = allOptions.map((options) => { - const flatOptions = (options ?? []).map((o) => - flattenObject({ obj: o, prefix }), - ); - return aggregateObjects({ objs: flatOptions }); - }); - + const allOptionsFlat = allOptions.map((options) => + flattenObject({ obj: { options }, prefix }), + ); return aggregateObjects({ objs: allOptionsFlat, wrap: true }); }; @@ -40,12 +36,9 @@ const flattenOneTrustNestedQuestions = ( ['options'], ); - const restQuestionsFlat = restQuestions.map((r) => - flattenObject({ obj: r, prefix }), - ); return { - ...aggregateObjects({ objs: restQuestionsFlat }), - ...flattenOneTrustNestedQuestionsOptions(allOptions, `${prefix}_options`), + ...flattenObject({ obj: { questions: restQuestions }, prefix }), + ...flattenOneTrustNestedQuestionsOptions(allOptions, `${prefix}_questions`), }; }; @@ -54,29 +47,24 @@ const flattenOneTrustQuestionResponses = ( allQuestionResponses: OneTrustAssessmentQuestionResponses[], prefix: string, ): any => { - const allQuestionResponsesFlat = allQuestionResponses.map( - (questionResponses) => { - const { responses, rest: restQuestionResponses } = extractProperties( - questionResponses.map((q) => ({ - ...q, - // there is always just one response within responses - responses: q.responses[0], - })), - ['responses'], - ); + const allQuestionResponsesFlat = allQuestionResponses.map((qrs) => { + const { responses, rest: questionResponses } = extractProperties( + qrs.map((q) => ({ + ...q, + // there is always just one response within responses + responses: q.responses[0], + })), + ['responses'], + ); - const responsesFlat = (responses ?? []).map((r) => - flattenObject({ obj: r, prefix }), - ); - const restQuestionResponsesFlat = (restQuestionResponses ?? []).map((q) => - flattenObject({ obj: q, prefix }), - ); - return { - ...aggregateObjects({ objs: responsesFlat }), - ...aggregateObjects({ objs: restQuestionResponsesFlat }), - }; - }, - ); + const responsesFlat = (responses ?? []).map((r) => + flattenObject({ obj: r, prefix: `${prefix}_questionResponses` }), + ); + return { + ...aggregateObjects({ objs: responsesFlat }), + ...flattenObject({ obj: { questionResponses }, prefix }), + }; + }); return aggregateObjects({ objs: allQuestionResponsesFlat, wrap: true }); }; @@ -84,12 +72,9 @@ const flattenOneTrustRiskCategories = ( allCategories: OneTrustRiskCategories[], prefix: string, ): any => { - const allCategoriesFlat = (allCategories ?? []).map((categories) => { - const flatCategories = categories.map((c) => - flattenObject({ obj: c, prefix }), - ); - return aggregateObjects({ objs: flatCategories }); - }); + const allCategoriesFlat = (allCategories ?? []).map((categories) => + flattenObject({ obj: { categories }, prefix }), + ); return aggregateObjects({ objs: allCategoriesFlat, wrap: true }); }; @@ -103,11 +88,11 @@ const flattenOneTrustRisks = ( ]); const flatRisks = (restRisks ?? []).map((r) => - flattenObject({ obj: r, prefix }), + flattenObject({ obj: r, prefix: `${prefix}_risks` }), ); return { ...aggregateObjects({ objs: flatRisks }), - ...flattenOneTrustRiskCategories(categories, `${prefix}_categories`), + ...flattenOneTrustRiskCategories(categories, `${prefix}_risks`), }; }); @@ -132,16 +117,16 @@ const flattenOneTrustQuestions = ( ]); const restSectionQuestionsFlat = restSectionQuestions.map((q) => - flattenObject({ obj: q, prefix }), + flattenObject({ obj: q, prefix: `${prefix}_questions` }), ); return { ...aggregateObjects({ objs: restSectionQuestionsFlat }), ...flattenOneTrustNestedQuestions(questions, prefix), - ...flattenOneTrustRisks(allRisks, `${prefix}_risks`), + ...flattenOneTrustRisks(allRisks, `${prefix}_questions`), ...flattenOneTrustQuestionResponses( allQuestionResponses, - `${prefix}_questionResponses`, + `${prefix}_questions`, ), }; }, @@ -172,7 +157,6 @@ const flattenOneTrustSectionHeaders = ( const flattenOneTrustSections = ( sections: OneTrustEnrichedAssessmentSection[], - prefix: string, ): any => { const { questions: allQuestions, @@ -181,11 +165,8 @@ const flattenOneTrustSections = ( } = extractProperties(sections, ['questions', 'header']); const sectionsFlat = flattenObject({ obj: { sections: restSections } }); - const headersFlat = flattenOneTrustSectionHeaders(headers, prefix); - const questionsFlat = flattenOneTrustQuestions( - allQuestions, - `${prefix}_questions`, - ); + const headersFlat = flattenOneTrustSectionHeaders(headers, 'sections'); + const questionsFlat = flattenOneTrustQuestions(allQuestions, 'sections'); return { ...sectionsFlat, ...headersFlat, ...questionsFlat }; }; @@ -208,6 +189,6 @@ export const flattenOneTrustAssessment = ( ...flattenObject({ obj: { approvers } }), ...flattenObject({ obj: { respondents } }), ...flattenObject({ obj: { primaryEntityDetails } }), - ...flattenOneTrustSections(sections, 'sections'), + ...flattenOneTrustSections(sections), }; }; diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts index 5d4085f4..ee704528 100644 --- a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts @@ -31,13 +31,6 @@ export const oneTrustAssessmentToCsvRecord = ( }), ); - /** - * TODO: test that this is actually doing something. - * For example - * - does it fail if flatAssessmentFull has extra properties - * - does it fail if flatAssessmentFull is missing properties - * - */ // ensure the record has the expected type! return decodeCodec(OneTrustAssessmentCsvRecord, flatAssessmentFull); }; From 7b4ee5b19ae797bc0dcf713daf83ee5b79972c9c Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 17:49:17 +0000 Subject: [PATCH 71/79] import aggregateObjects and flattenObject from @transcend/type-utils --- .pnp.cjs | 10 +++++----- package.json | 2 +- src/oneTrust/helpers/flattenOneTrustAssessment.ts | 3 +-- yarn.lock | 10 +++++----- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.pnp.cjs b/.pnp.cjs index e5888643..077f6d81 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -34,7 +34,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/persisted-state", "npm:1.0.4"],\ ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ - ["@transcend-io/type-utils", "npm:1.6.0"],\ + ["@transcend-io/type-utils", "npm:1.7.0"],\ ["@types/bluebird", "npm:3.5.38"],\ ["@types/chai", "npm:4.3.4"],\ ["@types/cli-progress", "npm:3.11.0"],\ @@ -686,7 +686,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/persisted-state", "npm:1.0.4"],\ ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ - ["@transcend-io/type-utils", "npm:1.6.0"],\ + ["@transcend-io/type-utils", "npm:1.7.0"],\ ["@types/bluebird", "npm:3.5.38"],\ ["@types/chai", "npm:4.3.4"],\ ["@types/cli-progress", "npm:3.11.0"],\ @@ -836,10 +836,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["npm:1.6.0", {\ - "packageLocation": "./.yarn/cache/@transcend-io-type-utils-npm-1.6.0-6359184828-4663edb422.zip/node_modules/@transcend-io/type-utils/",\ + ["npm:1.7.0", {\ + "packageLocation": "./.yarn/cache/@transcend-io-type-utils-npm-1.7.0-fc53de2630-00c709d5d5.zip/node_modules/@transcend-io/type-utils/",\ "packageDependencies": [\ - ["@transcend-io/type-utils", "npm:1.6.0"],\ + ["@transcend-io/type-utils", "npm:1.7.0"],\ ["fp-ts", "npm:2.16.1"],\ ["io-ts", "virtual:a57afaf9d13087a7202de8c93ac4854c9e2828bad7709250829ec4c7bc9dc95ecc2858c25612aa1774c986aedc232c76957076a1da3156fd2ab63ae5551b086f#npm:2.2.21"]\ ],\ diff --git a/package.json b/package.json index c06e3f76..141730ce 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@transcend-io/persisted-state": "^1.0.4", "@transcend-io/privacy-types": "^4.105.0", "@transcend-io/secret-value": "^1.2.0", - "@transcend-io/type-utils": "^1.6.0", + "@transcend-io/type-utils": "^1.7.1", "bluebird": "^3.7.2", "cli-progress": "^3.11.2", "colors": "^1.4.0", diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 2e0b1022..b2213747 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -14,8 +14,7 @@ import { OneTrustEnrichedAssessmentSection, OneTrustEnrichedRisk, } from '../codecs'; -import { flattenObject } from './flattenObject'; -import { aggregateObjects } from './aggregateObjects'; +import { flattenObject, aggregateObjects } from '@transcend-io/type-utils'; const flattenOneTrustNestedQuestionsOptions = ( allOptions: (OneTrustAssessmentQuestionOption[] | null)[], diff --git a/yarn.lock b/yarn.lock index 12dc0ff7..7fc9caa8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -517,7 +517,7 @@ __metadata: "@transcend-io/persisted-state": ^1.0.4 "@transcend-io/privacy-types": ^4.105.0 "@transcend-io/secret-value": ^1.2.0 - "@transcend-io/type-utils": ^1.6.0 + "@transcend-io/type-utils": ^1.7.0 "@types/bluebird": ^3.5.38 "@types/chai": ^4.3.4 "@types/cli-progress": ^3.11.0 @@ -699,13 +699,13 @@ __metadata: languageName: node linkType: hard -"@transcend-io/type-utils@npm:^1.6.0": - version: 1.6.0 - resolution: "@transcend-io/type-utils@npm:1.6.0" +"@transcend-io/type-utils@npm:^1.7.0": + version: 1.7.0 + resolution: "@transcend-io/type-utils@npm:1.7.0" dependencies: fp-ts: ^2.16.1 io-ts: ^2.2.21 - checksum: 4663edb42217641e03f9f82e0bf7606270a7e7f8048ba02d74d90f993aad4dd151aae2dba5e495168cb82523dc6aced5dcb3694aac26a206a983a9fcc1de9eb4 + checksum: 00c709d5d5f0b5cb0d0c83e83d2c67cf8ff660f76741436db906e9e4a0e03ff2d1c8ea144db4d04d035af537688c33d798053767aac09e41a01fc68a270e9509 languageName: node linkType: hard From dedf1b807d9245ea297442349e6c60cbcb32737d Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 19:08:20 +0000 Subject: [PATCH 72/79] use transposeObjectArray from @transcend/type-utils --- .pnp.cjs | 10 +-- package.json | 2 +- src/helpers/extractProperties.ts | 78 ------------------- src/helpers/index.ts | 1 - .../helpers/flattenOneTrustAssessment.ts | 29 +++---- yarn.lock | 10 +-- 6 files changed, 27 insertions(+), 103 deletions(-) delete mode 100644 src/helpers/extractProperties.ts diff --git a/.pnp.cjs b/.pnp.cjs index 077f6d81..9162708d 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -34,7 +34,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/persisted-state", "npm:1.0.4"],\ ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ - ["@transcend-io/type-utils", "npm:1.7.0"],\ + ["@transcend-io/type-utils", "npm:1.8.0"],\ ["@types/bluebird", "npm:3.5.38"],\ ["@types/chai", "npm:4.3.4"],\ ["@types/cli-progress", "npm:3.11.0"],\ @@ -686,7 +686,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/persisted-state", "npm:1.0.4"],\ ["@transcend-io/privacy-types", "npm:4.105.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ - ["@transcend-io/type-utils", "npm:1.7.0"],\ + ["@transcend-io/type-utils", "npm:1.8.0"],\ ["@types/bluebird", "npm:3.5.38"],\ ["@types/chai", "npm:4.3.4"],\ ["@types/cli-progress", "npm:3.11.0"],\ @@ -836,10 +836,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["npm:1.7.0", {\ - "packageLocation": "./.yarn/cache/@transcend-io-type-utils-npm-1.7.0-fc53de2630-00c709d5d5.zip/node_modules/@transcend-io/type-utils/",\ + ["npm:1.8.0", {\ + "packageLocation": "./.yarn/cache/@transcend-io-type-utils-npm-1.8.0-4099be8224-e4a3784e93.zip/node_modules/@transcend-io/type-utils/",\ "packageDependencies": [\ - ["@transcend-io/type-utils", "npm:1.7.0"],\ + ["@transcend-io/type-utils", "npm:1.8.0"],\ ["fp-ts", "npm:2.16.1"],\ ["io-ts", "virtual:a57afaf9d13087a7202de8c93ac4854c9e2828bad7709250829ec4c7bc9dc95ecc2858c25612aa1774c986aedc232c76957076a1da3156fd2ab63ae5551b086f#npm:2.2.21"]\ ],\ diff --git a/package.json b/package.json index 141730ce..fc4eea8f 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@transcend-io/persisted-state": "^1.0.4", "@transcend-io/privacy-types": "^4.105.0", "@transcend-io/secret-value": "^1.2.0", - "@transcend-io/type-utils": "^1.7.1", + "@transcend-io/type-utils": "^1.8.0", "bluebird": "^3.7.2", "cli-progress": "^3.11.2", "colors": "^1.4.0", diff --git a/src/helpers/extractProperties.ts b/src/helpers/extractProperties.ts deleted file mode 100644 index dfa6bc7d..00000000 --- a/src/helpers/extractProperties.ts +++ /dev/null @@ -1,78 +0,0 @@ -// eslint-disable-next-line eslint-comments/disable-enable-pair -/* eslint-disable @typescript-eslint/no-explicit-any */ -// FIXME: move to @transcend/type-utils -/** - * Type that represents the extracted properties from an object type T. - * For each property K from T, creates an array of that property's type. - * Also includes a 'rest' property containing an array of objects with all non-extracted properties. - * - * - * @template T - The source object type - * @template K - The keys to extract from T - * @example - * // Given an array of objects: - * const items = [ - * { id: 1, name: 'John', age: 25, city: 'NY' }, - * { id: 2, name: 'Jane', age: 30, city: 'LA' } - * ]; - * - * // And extracting 'id' and 'name': - * type Result = ExtractedArrayProperties; - * - * // Result will be typed as: - * { - * id: number[]; // [1, 2] - * name: string[]; // ['John', 'Jane'] - * rest: Array<{ // [{ age: 25, city: 'NY' }, { age: 30, city: 'LA' }] - * age: number; - * city: string; - * }>; - * } - */ -type ExtractedArrayProperties = { - [P in K]: Array; -} & { - /** The array of non-extracted properties */ - rest: Array>; -}; - -/** - * Extracts specified properties from an array of objects into separate arrays. - * Also collects all non-extracted properties into a 'rest' array. - * - * @template T - The type of objects in the input array - * @template K - The keys of properties to extract - * @param items - Array of objects to extract properties from - * @param properties - Array of property keys to extract - * @returns An object containing arrays of extracted properties and a rest array - * @example - * const items = [ - * { id: 1, name: 'John', age: 25, city: 'NY' }, - * { id: 2, name: 'Jane', age: 30, city: 'LA' } - * ] - * const result = extractProperties(items, ['id', 'name']); - * // Returns: { id: [1, 2], name: ['John', 'Jane'], rest: [{age: 25, city: 'NY'}, {age: 30, city: 'LA'}] } - */ -export const extractProperties = ( - items: T[], - properties: K[], -): ExtractedArrayProperties => - items.reduce((acc, item) => { - const result = { ...acc } as ExtractedArrayProperties; - - properties.forEach((prop) => { - const currentArray = (acc[prop] || []) as T[K][]; - result[prop] = [...currentArray, item[prop]] as any; - }); - - const restObject = {} as Omit; - Object.entries(item).forEach(([key, value]) => { - if (!properties.includes(key as K)) { - (restObject as any)[key] = value; - } - }); - - result.rest = [...(acc.rest || []), restObject]; - - return result; - }, {} as ExtractedArrayProperties); diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 07a8fd6a..73d30623 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -2,4 +2,3 @@ export * from './buildAIIntegrationType'; export * from './buildEnabledRouteType'; export * from './inquirer'; export * from './parseVariablesFromString'; -export * from './extractProperties'; diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index b2213747..261e7786 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -7,14 +7,17 @@ import { OneTrustAssessmentSectionHeader, OneTrustRiskCategories, } from '@transcend-io/privacy-types'; -import { extractProperties } from '../../helpers'; import { OneTrustEnrichedAssessment, OneTrustEnrichedAssessmentQuestion, OneTrustEnrichedAssessmentSection, OneTrustEnrichedRisk, } from '../codecs'; -import { flattenObject, aggregateObjects } from '@transcend-io/type-utils'; +import { + flattenObject, + aggregateObjects, + transposeObjectArray, +} from '@transcend-io/type-utils'; const flattenOneTrustNestedQuestionsOptions = ( allOptions: (OneTrustAssessmentQuestionOption[] | null)[], @@ -30,7 +33,7 @@ const flattenOneTrustNestedQuestions = ( questions: OneTrustAssessmentNestedQuestion[], prefix: string, ): any => { - const { options: allOptions, rest: restQuestions } = extractProperties( + const { options: allOptions, rest: restQuestions } = transposeObjectArray( questions, ['options'], ); @@ -47,7 +50,7 @@ const flattenOneTrustQuestionResponses = ( prefix: string, ): any => { const allQuestionResponsesFlat = allQuestionResponses.map((qrs) => { - const { responses, rest: questionResponses } = extractProperties( + const { responses, rest: questionResponses } = transposeObjectArray( qrs.map((q) => ({ ...q, // there is always just one response within responses @@ -82,7 +85,7 @@ const flattenOneTrustRisks = ( prefix: string, ): any => { const allRisksFlat = (allRisks ?? []).map((risks) => { - const { categories, rest: restRisks } = extractProperties(risks ?? [], [ + const { categories, rest: restRisks } = transposeObjectArray(risks ?? [], [ 'categories', ]); @@ -109,7 +112,7 @@ const flattenOneTrustQuestions = ( question: questions, questionResponses: allQuestionResponses, risks: allRisks, - } = extractProperties(sectionQuestions, [ + } = transposeObjectArray(sectionQuestions, [ 'question', 'questionResponses', 'risks', @@ -141,7 +144,7 @@ const flattenOneTrustSectionHeaders = ( headers: OneTrustAssessmentSectionHeader[], prefix: string, ): any => { - const { riskStatistics, rest: restHeaders } = extractProperties(headers, [ + const { riskStatistics, rest: restHeaders } = transposeObjectArray(headers, [ 'riskStatistics', ]); @@ -161,13 +164,13 @@ const flattenOneTrustSections = ( questions: allQuestions, header: headers, rest: restSections, - } = extractProperties(sections, ['questions', 'header']); - - const sectionsFlat = flattenObject({ obj: { sections: restSections } }); - const headersFlat = flattenOneTrustSectionHeaders(headers, 'sections'); - const questionsFlat = flattenOneTrustQuestions(allQuestions, 'sections'); + } = transposeObjectArray(sections, ['questions', 'header']); - return { ...sectionsFlat, ...headersFlat, ...questionsFlat }; + return { + ...flattenObject({ obj: { sections: restSections } }), + ...flattenOneTrustSectionHeaders(headers, 'sections'), + ...flattenOneTrustQuestions(allQuestions, 'sections'), + }; }; export const flattenOneTrustAssessment = ( diff --git a/yarn.lock b/yarn.lock index 7fc9caa8..31b7c4e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -517,7 +517,7 @@ __metadata: "@transcend-io/persisted-state": ^1.0.4 "@transcend-io/privacy-types": ^4.105.0 "@transcend-io/secret-value": ^1.2.0 - "@transcend-io/type-utils": ^1.7.0 + "@transcend-io/type-utils": ^1.8.0 "@types/bluebird": ^3.5.38 "@types/chai": ^4.3.4 "@types/cli-progress": ^3.11.0 @@ -699,13 +699,13 @@ __metadata: languageName: node linkType: hard -"@transcend-io/type-utils@npm:^1.7.0": - version: 1.7.0 - resolution: "@transcend-io/type-utils@npm:1.7.0" +"@transcend-io/type-utils@npm:^1.8.0": + version: 1.8.0 + resolution: "@transcend-io/type-utils@npm:1.8.0" dependencies: fp-ts: ^2.16.1 io-ts: ^2.2.21 - checksum: 00c709d5d5f0b5cb0d0c83e83d2c67cf8ff660f76741436db906e9e4a0e03ff2d1c8ea144db4d04d035af537688c33d798053767aac09e41a01fc68a270e9509 + checksum: e4a3784e932cbdd9499c3d5d1246a5b2951063a3a8d5ee3be740775b92b7dd10ce1eed21eec85a508e7f98354e0c80a002f89218b541378f9efd1e7fc1b5f155 languageName: node linkType: hard From 7a8389192718aca09965fb4f86a50813fb31518c Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 19:19:20 +0000 Subject: [PATCH 73/79] improve flattenOneTrustAssessment --- .../helpers/flattenOneTrustAssessment.ts | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 261e7786..b8f55d47 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -58,12 +58,8 @@ const flattenOneTrustQuestionResponses = ( })), ['responses'], ); - - const responsesFlat = (responses ?? []).map((r) => - flattenObject({ obj: r, prefix: `${prefix}_questionResponses` }), - ); return { - ...aggregateObjects({ objs: responsesFlat }), + ...flattenObject({ obj: { questionResponses: responses }, prefix }), ...flattenObject({ obj: { questionResponses }, prefix }), }; }); @@ -84,16 +80,12 @@ const flattenOneTrustRisks = ( allRisks: (OneTrustEnrichedRisk[] | null)[], prefix: string, ): any => { - const allRisksFlat = (allRisks ?? []).map((risks) => { - const { categories, rest: restRisks } = transposeObjectArray(risks ?? [], [ + const allRisksFlat = (allRisks ?? []).map((ars) => { + const { categories, rest: risks } = transposeObjectArray(ars ?? [], [ 'categories', ]); - - const flatRisks = (restRisks ?? []).map((r) => - flattenObject({ obj: r, prefix: `${prefix}_risks` }), - ); return { - ...aggregateObjects({ objs: flatRisks }), + ...flattenObject({ obj: { risks }, prefix }), ...flattenOneTrustRiskCategories(categories, `${prefix}_risks`), }; }); @@ -108,8 +100,8 @@ const flattenOneTrustQuestions = ( const allSectionQuestionsFlat = allSectionQuestions.map( (sectionQuestions) => { const { - rest: restSectionQuestions, - question: questions, + rest: questions, + question: nestedQuestions, questionResponses: allQuestionResponses, risks: allRisks, } = transposeObjectArray(sectionQuestions, [ @@ -118,13 +110,9 @@ const flattenOneTrustQuestions = ( 'risks', ]); - const restSectionQuestionsFlat = restSectionQuestions.map((q) => - flattenObject({ obj: q, prefix: `${prefix}_questions` }), - ); - return { - ...aggregateObjects({ objs: restSectionQuestionsFlat }), - ...flattenOneTrustNestedQuestions(questions, prefix), + ...flattenObject({ obj: { questions }, prefix }), + ...flattenOneTrustNestedQuestions(nestedQuestions, prefix), ...flattenOneTrustRisks(allRisks, `${prefix}_questions`), ...flattenOneTrustQuestionResponses( allQuestionResponses, From 148b0e4c69b7b5b69585aa1602c526de062c3477 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 19:23:35 +0000 Subject: [PATCH 74/79] update Readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index c03cad1c..468b447a 100644 --- a/README.md +++ b/README.md @@ -597,6 +597,14 @@ In order to use this command, you will need to generate a OneTrust OAuth Token w To learn how to generate the token, see the [OAuth 2.0 Scopes](https://developer.onetrust.com/onetrust/reference/oauth-20-scopes) and [Generate Access Token](https://developer.onetrust.com/onetrust/reference/getoauthtoken) pages. +If syncing the resources to Transcend, you will also need to generate an API key on the Transcend Admin Dashboard (https://app.transcend.io/infrastructure/api-keys). + +The API key needs the following scopes when pushing the various resource types: + +| Resource | Scope | +| ----------- | ------------------ | +| assessments | Manage Assessments | + #### Arguments | Argument | Description | Type | Default | Required | From de53f5e5330d94115c7c7fc79d7fcfe35c3626a9 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 19:27:28 +0000 Subject: [PATCH 75/79] update cli-sync-ot docs --- src/cli-sync-ot.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index e7981ccd..0d5deb21 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -11,10 +11,10 @@ import { buildTranscendGraphQLClient } from './graphql'; * Pull configuration from OneTrust down locally to disk * * Dev Usage: - * yarn ts-node ./src/cli-sync-ot.ts --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json + * yarn ts-node ./src/cli-sync-ot.ts --hostname=customer.my.onetrust.com --oneTrustAuth=$ONE_TRUST_OAUTH_TOKEN --transcendAuth=$TRANSCEND_API_KEY * * Standard usage - * yarn cli-sync-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json + * yarn cli-sync-ot --hostname=customer.my.onetrust.com --oneTrustAuth=$ONE_TRUST_OAUTH_TOKEN --transcendAuth=$TRANSCEND_API_KEY */ async function main(): Promise { const { From 34de61b410d5282100692b19082a91ae5e7ff36a Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 19:29:07 +0000 Subject: [PATCH 76/79] update package.version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc4eea8f..1c22c3b6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Transcend Inc.", "name": "@transcend-io/cli", "description": "Small package containing useful typescript utilities.", - "version": "6.13.0", + "version": "6.14.0", "homepage": "https://github.com/transcend-io/cli", "repository": { "type": "git", From 4deaa2d7c1a2c58de007eaf29bea7f127f8de5fc Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Jan 2025 01:24:52 +0000 Subject: [PATCH 77/79] update commments --- src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts index ee704528..0eb76da6 100644 --- a/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts @@ -18,7 +18,11 @@ export const oneTrustAssessmentToCsvRecord = ( // flatten the assessment object so it does not have nested properties const flatAssessment = flattenOneTrustAssessment(assessment); - // transform the flat assessment to have all CSV keys in the expected order + /** + * transform the flat assessment to: + * 1. have every possible header. This is how the backend can tell it's a CLI, not Dashboard, import. + * 2. have the headers in the same order. This is fundamental for constructing a CSV file. + */ const flatAssessmentFull = Object.fromEntries( DEFAULT_ONE_TRUST_ASSESSMENT_CSV_HEADER.map((header) => { const value = flatAssessment[header] ?? ''; From cf23df8ec16cd126cd79e07815168c2c653cd3d2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 16 Jan 2025 02:33:30 +0000 Subject: [PATCH 78/79] remove potential bugs from flattenOneTrustAssessment --- .../helpers/flattenOneTrustAssessment.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index b8f55d47..07c881f6 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -85,8 +85,9 @@ const flattenOneTrustRisks = ( 'categories', ]); return { - ...flattenObject({ obj: { risks }, prefix }), - ...flattenOneTrustRiskCategories(categories, `${prefix}_risks`), + ...(risks && flattenObject({ obj: { risks }, prefix })), + ...(categories && + flattenOneTrustRiskCategories(categories, `${prefix}_risks`)), }; }); @@ -111,13 +112,15 @@ const flattenOneTrustQuestions = ( ]); return { - ...flattenObject({ obj: { questions }, prefix }), - ...flattenOneTrustNestedQuestions(nestedQuestions, prefix), - ...flattenOneTrustRisks(allRisks, `${prefix}_questions`), - ...flattenOneTrustQuestionResponses( - allQuestionResponses, - `${prefix}_questions`, - ), + ...(questions && flattenObject({ obj: { questions }, prefix })), + ...(nestedQuestions && + flattenOneTrustNestedQuestions(nestedQuestions, prefix)), + ...(allRisks && flattenOneTrustRisks(allRisks, `${prefix}_questions`)), + ...(allQuestionResponses && + flattenOneTrustQuestionResponses( + allQuestionResponses, + `${prefix}_questions`, + )), }; }, ); @@ -136,28 +139,30 @@ const flattenOneTrustSectionHeaders = ( 'riskStatistics', ]); - const flatFlatHeaders = restHeaders.map((h) => + const flatFlatHeaders = (restHeaders ?? []).map((h) => flattenObject({ obj: h, prefix }), ); return { ...aggregateObjects({ objs: flatFlatHeaders }), - ...flattenObject({ obj: { riskStatistics }, prefix }), + ...(riskStatistics && flattenObject({ obj: { riskStatistics }, prefix })), }; }; const flattenOneTrustSections = ( sections: OneTrustEnrichedAssessmentSection[], ): any => { + // filter out sections without questions (shouldn't happen, but just to be safe) + const sectionsWithQuestions = sections.filter((s) => s.questions.length > 0); const { questions: allQuestions, header: headers, rest: restSections, - } = transposeObjectArray(sections, ['questions', 'header']); + } = transposeObjectArray(sectionsWithQuestions, ['questions', 'header']); return { - ...flattenObject({ obj: { sections: restSections } }), - ...flattenOneTrustSectionHeaders(headers, 'sections'), - ...flattenOneTrustQuestions(allQuestions, 'sections'), + ...(restSections && flattenObject({ obj: { sections: restSections } })), + ...(headers && flattenOneTrustSectionHeaders(headers, 'sections')), + ...(allQuestions && flattenOneTrustQuestions(allQuestions, 'sections')), }; }; From fa17027ce172c48ab0c4a0532962e6a07a717466 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 17 Jan 2025 06:37:40 +0000 Subject: [PATCH 79/79] add fixmes --- src/oneTrust/helpers/flattenOneTrustAssessment.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/oneTrust/helpers/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts index 07c881f6..721a1310 100644 --- a/src/oneTrust/helpers/flattenOneTrustAssessment.ts +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -44,6 +44,9 @@ const flattenOneTrustNestedQuestions = ( }; }; +// FIXME: there is a bug here. My current guess is that if no question in a section has +// responses, the output is string[], instead of string[][] (question responses per question in the section) +// writing test cases may help catch this! // flatten questionResponses of every question within a section const flattenOneTrustQuestionResponses = ( allQuestionResponses: OneTrustAssessmentQuestionResponses[],