diff --git a/.pnp.cjs b/.pnp.cjs index 9695ff8c..9162708d 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -32,9 +32,9 @@ 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"],\ + ["@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"],\ @@ -684,9 +684,9 @@ 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"],\ + ["@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"],\ @@ -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"]\ @@ -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.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.5.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/.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 ea860d21..00000000 Binary files a/.yarn/cache/@transcend-io-privacy-types-npm-4.103.0-b8d1864632-4661368b34.zip and /dev/null differ 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 557ba49a..00000000 Binary files a/.yarn/cache/@transcend-io-type-utils-npm-1.5.0-125f1a01fb-0d7d85e794.zip and /dev/null differ diff --git a/README.md b/README.md index 11674d59..468b447a 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ - [Authentication](#authentication-34) - [Arguments](#arguments-33) - [Usage](#usage-34) + - [tr-sync-ot](#tr-sync-ot) - [tr-build-xdi-sync-endpoint](#tr-build-xdi-sync-endpoint) - [Authentication](#authentication-35) - [Arguments](#arguments-34) @@ -176,7 +177,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 +218,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 +578,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: @@ -596,22 +597,47 @@ 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 | -| ---------- | ------------------------------------------------------------------------------------------------- | ------- | ----------- | -------- | -| 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 | -| 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 | +| 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 +```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-pull-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/package.json b/package.json index e2c927e9..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", @@ -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-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", @@ -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", @@ -68,9 +68,9 @@ "@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", + "@transcend-io/type-utils": "^1.8.0", "bluebird": "^3.7.2", "cli-progress": "^3.11.2", "colors": "^1.4.0", diff --git a/src/cli-pull-ot.ts b/src/cli-pull-ot.ts deleted file mode 100644 index 1ea67f36..00000000 --- a/src/cli-pull-ot.ts +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env node -import { logger } from './logger'; -import colors from 'colors'; -import { - getListOfAssessments, - getAssessment, - writeOneTrustAssessment, - parseCliPullOtArguments, - createOneTrustGotInstance, -} from './oneTrust'; -import { OneTrustPullResource } from './enums'; -import { mapSeries } from 'bluebird'; - -/** - * 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 - * - * Standard usage - * 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 } = - parseCliPullOtArguments(); - - try { - 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 getListOfAssessments({ 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 getAssessment({ - oneTrust, - assessmentId: assessment.assessmentId, - }); - - writeOneTrustAssessment({ - assessment, - assessmentDetails, - 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); - } - - // Indicate success - logger.info( - colors.green( - `Successfully synced OneTrust ${resource} to disk at "${file}"!`, - ), - ); -} - -main(); diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts new file mode 100644 index 00000000..0d5deb21 --- /dev/null +++ b/src/cli-sync-ot.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env node +import { logger } from './logger'; + +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 + * + * Dev Usage: + * 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 --oneTrustAuth=$ONE_TRUST_OAUTH_TOKEN --transcendAuth=$TRANSCEND_API_KEY + */ +async function main(): Promise { + 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: oneTrustAuth }); + + try { + if (resource === OneTrustPullResource.Assessments) { + await syncOneTrustAssessments({ + oneTrust, + file, + fileFormat, + dryRun, + ...(transcendAuth && transcendUrl + ? { + transcend: buildTranscendGraphQLClient( + transcendUrl, + transcendAuth, + ), + } + : {}), + }); + } + } catch (err) { + logger.error( + colors.red( + `An error occurred syncing the resource ${resource} from OneTrust: ${ + debug ? err.stack : err.message + }`, + ), + ); + process.exit(1); + } + + // Indicate success + logger.info( + colors.green( + `Successfully synced OneTrust ${resource} to ${ + dryRun ? `disk at "${file}"` : 'Transcend' + }!`, + ), + ); +} + +main(); 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/codecs.ts b/src/oneTrust/codecs.ts new file mode 100644 index 00000000..3677d153 --- /dev/null +++ b/src/oneTrust/codecs.ts @@ -0,0 +1,170 @@ +import { + OneTrustAssessment, + OneTrustAssessmentNestedQuestion, + OneTrustAssessmentQuestion, + OneTrustAssessmentQuestionRisk, + OneTrustAssessmentSection, + OneTrustAssessmentSectionHeader, + OneTrustGetAssessmentResponse, + OneTrustGetRiskResponse, +} from '@transcend-io/privacy-types'; +import * as t from 'io-ts'; + +/** 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: + OneTrustAssessmentNestedQuestion.props.prePopulateResponse, + linkAssessmentToInventory: + OneTrustAssessmentNestedQuestion.props.linkAssessmentToInventory, + valid: OneTrustAssessmentNestedQuestion.props.valid, + type: OneTrustAssessmentNestedQuestion.props.type, + allowMultiSelect: OneTrustAssessmentNestedQuestion.props.allowMultiSelect, + content: OneTrustAssessmentNestedQuestion.props.content, + requireJustification: + OneTrustAssessmentNestedQuestion.props.requireJustification, +}); + +/** Type override */ +export type OneTrustAssessmentNestedQuestionFlat = t.TypeOf< + typeof OneTrustAssessmentNestedQuestionFlat +>; + +// 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: + OneTrustAssessmentQuestion.types[0].props.hasNavigationRules, + rootRequestInformationIds: + OneTrustAssessmentQuestion.types[0].props.rootRequestInformationIds, + totalAttachments: OneTrustAssessmentQuestion.types[0].props.totalAttachments, + attachmentIds: OneTrustAssessmentQuestion.types[0].props.attachmentIds, +}); + +/** Type override */ +export type OneTrustAssessmentQuestionFlat = t.TypeOf< + typeof OneTrustAssessmentQuestionFlat +>; + +/** 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: + OneTrustAssessmentSectionHeader.types[0].props.invalidQuestionIds, + requiredUnansweredQuestionIds: + OneTrustAssessmentSectionHeader.types[0].props + .requiredUnansweredQuestionIds, + requiredQuestionIds: + OneTrustAssessmentSectionHeader.types[0].props.requiredQuestionIds, + unansweredQuestionIds: + OneTrustAssessmentSectionHeader.types[0].props.unansweredQuestionIds, + effectivenessQuestionIds: + OneTrustAssessmentSectionHeader.types[0].props.effectivenessQuestionIds, + submitted: OneTrustAssessmentSectionHeader.types[0].props.submitted, +}); +/** Type override */ +export type OneTrustAssessmentSectionFlatHeader = t.TypeOf< + typeof OneTrustAssessmentSectionFlatHeader +>; + +/** 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, +}); + +/** Type override */ +export type OneTrustFlatAssessmentSection = t.TypeOf< + typeof OneTrustFlatAssessmentSection +>; + +export const OneTrustEnrichedRisk = t.intersection([ + OneTrustAssessmentQuestionRisk, + t.type({ + 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 OneTrustEnrichedRisk = t.TypeOf; + +export const OneTrustEnrichedRisks = t.union([ + t.array(OneTrustEnrichedRisk), + t.null, +]); +/** Type override */ +export type OneTrustEnrichedRisks = t.TypeOf; + +export const OneTrustEnrichedAssessmentQuestion = t.intersection([ + t.type({ + ...OneTrustAssessmentQuestion.types[0].props, + risks: OneTrustEnrichedRisks, + }), + t.partial({ ...OneTrustAssessmentQuestion.types[1].props }), +]); + +/** Type override */ +export type OneTrustEnrichedAssessmentQuestion = t.TypeOf< + typeof OneTrustEnrichedAssessmentQuestion +>; + +export const OneTrustEnrichedAssessmentSection = t.type({ + ...OneTrustAssessmentSection.props, + questions: t.array(OneTrustEnrichedAssessmentQuestion), +}); +/** Type override */ +export type OneTrustEnrichedAssessmentSection = t.TypeOf< + typeof OneTrustEnrichedAssessmentSection +>; + +export const OneTrustEnrichedAssessmentResponse = t.type({ + ...OneTrustGetAssessmentResponse.props, + sections: t.array(OneTrustEnrichedAssessmentSection), +}); +/** Type override */ +export type OneTrustEnrichedAssessmentResponse = t.TypeOf< + typeof OneTrustEnrichedAssessmentResponse +>; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { status, ...OneTrustAssessmentWithoutStatus } = OneTrustAssessment.props; +export const OneTrustEnrichedAssessment = t.intersection([ + t.type(OneTrustAssessmentWithoutStatus), + OneTrustEnrichedAssessmentResponse, +]); + +/** Type override */ +export type OneTrustEnrichedAssessment = t.TypeOf< + typeof OneTrustEnrichedAssessment +>; diff --git a/src/oneTrust/getListOfAssessments.ts b/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts similarity index 73% rename from src/oneTrust/getListOfAssessments.ts rename to src/oneTrust/endpoints/getListOfOneTrustAssessments.ts index 926d786c..2d290e4f 100644 --- a/src/oneTrust/getListOfAssessments.ts +++ b/src/oneTrust/endpoints/getListOfOneTrustAssessments.ts @@ -1,17 +1,19 @@ import { Got } from 'got'; -import { logger } from '../logger'; +import { logger } from '../../logger'; +import { decodeCodec } from '@transcend-io/type-utils'; import { OneTrustAssessment, OneTrustGetListOfAssessmentsResponse, -} from './types'; +} from '@transcend-io/privacy-types'; /** * 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 */ @@ -23,15 +25,16 @@ export const getListOfAssessments = 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( `api/assessment/v2/assessments?page=${currentPage}&size=2000`, ); - const { page, content } = JSON.parse( + + const { page, content } = decodeCodec( + OneTrustGetListOfAssessmentsResponse, body, - ) as OneTrustGetListOfAssessmentsResponse; + ); allAssessments.push(...(content ?? [])); if (currentPage === 0) { totalPages = page?.totalPages ?? 0; diff --git a/src/oneTrust/getAssessment.ts b/src/oneTrust/endpoints/getOneTrustAssessment.ts similarity index 63% rename from src/oneTrust/getAssessment.ts rename to src/oneTrust/endpoints/getOneTrustAssessment.ts index 2b81b028..6a02226e 100644 --- a/src/oneTrust/getAssessment.ts +++ b/src/oneTrust/endpoints/getOneTrustAssessment.ts @@ -1,13 +1,15 @@ import { Got } from 'got'; -import { OneTrustGetAssessmentResponse } from './types'; +import { decodeCodec } from '@transcend-io/type-utils'; +import { OneTrustGetAssessmentResponse } from '@transcend-io/privacy-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, }: { @@ -20,5 +22,5 @@ export const getAssessment = async ({ `api/assessment/v2/assessments/${assessmentId}/export?ExcludeSkippedQuestions=false`, ); - return JSON.parse(body) as OneTrustGetAssessmentResponse; + return decodeCodec(OneTrustGetAssessmentResponse, body); }; diff --git a/src/oneTrust/endpoints/getOneTrustRisk.ts b/src/oneTrust/endpoints/getOneTrustRisk.ts new file mode 100644 index 00000000..78184b02 --- /dev/null +++ b/src/oneTrust/endpoints/getOneTrustRisk.ts @@ -0,0 +1,24 @@ +import { Got } from 'got'; +import { decodeCodec } from '@transcend-io/type-utils'; +import { OneTrustGetRiskResponse } from '@transcend-io/privacy-types'; + +/** + * Retrieve details about a particular risk. + * ref: https://developer.onetrust.com/onetrust/reference/getriskusingget + * + * @param param - the information about the OneTrust client and risk to retrieve + * @returns the OneTrust risk + */ +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 decodeCodec(OneTrustGetRiskResponse, body); +}; 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/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), +); diff --git a/src/oneTrust/helpers/enrichOneTrustAssessment.ts b/src/oneTrust/helpers/enrichOneTrustAssessment.ts new file mode 100644 index 00000000..d943c4a5 --- /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]; + // FIXME: 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/flattenOneTrustAssessment.ts b/src/oneTrust/helpers/flattenOneTrustAssessment.ts new file mode 100644 index 00000000..721a1310 --- /dev/null +++ b/src/oneTrust/helpers/flattenOneTrustAssessment.ts @@ -0,0 +1,192 @@ +// 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 { + OneTrustEnrichedAssessment, + OneTrustEnrichedAssessmentQuestion, + OneTrustEnrichedAssessmentSection, + OneTrustEnrichedRisk, +} from '../codecs'; +import { + flattenObject, + aggregateObjects, + transposeObjectArray, +} from '@transcend-io/type-utils'; + +const flattenOneTrustNestedQuestionsOptions = ( + allOptions: (OneTrustAssessmentQuestionOption[] | null)[], + prefix: string, +): any => { + const allOptionsFlat = allOptions.map((options) => + flattenObject({ obj: { options }, prefix }), + ); + return aggregateObjects({ objs: allOptionsFlat, wrap: true }); +}; + +const flattenOneTrustNestedQuestions = ( + questions: OneTrustAssessmentNestedQuestion[], + prefix: string, +): any => { + const { options: allOptions, rest: restQuestions } = transposeObjectArray( + questions, + ['options'], + ); + + return { + ...flattenObject({ obj: { questions: restQuestions }, prefix }), + ...flattenOneTrustNestedQuestionsOptions(allOptions, `${prefix}_questions`), + }; +}; + +// 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[], + prefix: string, +): any => { + const allQuestionResponsesFlat = allQuestionResponses.map((qrs) => { + const { responses, rest: questionResponses } = transposeObjectArray( + qrs.map((q) => ({ + ...q, + // there is always just one response within responses + responses: q.responses[0], + })), + ['responses'], + ); + return { + ...flattenObject({ obj: { questionResponses: responses }, prefix }), + ...flattenObject({ obj: { questionResponses }, prefix }), + }; + }); + return aggregateObjects({ objs: allQuestionResponsesFlat, wrap: true }); +}; + +const flattenOneTrustRiskCategories = ( + allCategories: OneTrustRiskCategories[], + prefix: string, +): any => { + const allCategoriesFlat = (allCategories ?? []).map((categories) => + flattenObject({ obj: { categories }, prefix }), + ); + return aggregateObjects({ objs: allCategoriesFlat, wrap: true }); +}; + +const flattenOneTrustRisks = ( + allRisks: (OneTrustEnrichedRisk[] | null)[], + prefix: string, +): any => { + const allRisksFlat = (allRisks ?? []).map((ars) => { + const { categories, rest: risks } = transposeObjectArray(ars ?? [], [ + 'categories', + ]); + return { + ...(risks && flattenObject({ obj: { risks }, prefix })), + ...(categories && + flattenOneTrustRiskCategories(categories, `${prefix}_risks`)), + }; + }); + + return aggregateObjects({ objs: allRisksFlat, wrap: true }); +}; + +const flattenOneTrustQuestions = ( + allSectionQuestions: OneTrustEnrichedAssessmentQuestion[][], + prefix: string, +): any => { + const allSectionQuestionsFlat = allSectionQuestions.map( + (sectionQuestions) => { + const { + rest: questions, + question: nestedQuestions, + questionResponses: allQuestionResponses, + risks: allRisks, + } = transposeObjectArray(sectionQuestions, [ + 'question', + 'questionResponses', + 'risks', + ]); + + return { + ...(questions && flattenObject({ obj: { questions }, prefix })), + ...(nestedQuestions && + flattenOneTrustNestedQuestions(nestedQuestions, prefix)), + ...(allRisks && flattenOneTrustRisks(allRisks, `${prefix}_questions`)), + ...(allQuestionResponses && + flattenOneTrustQuestionResponses( + allQuestionResponses, + `${prefix}_questions`, + )), + }; + }, + ); + + return aggregateObjects({ + objs: allSectionQuestionsFlat, + wrap: true, + }); +}; + +const flattenOneTrustSectionHeaders = ( + headers: OneTrustAssessmentSectionHeader[], + prefix: string, +): any => { + const { riskStatistics, rest: restHeaders } = transposeObjectArray(headers, [ + 'riskStatistics', + ]); + + const flatFlatHeaders = (restHeaders ?? []).map((h) => + flattenObject({ obj: h, prefix }), + ); + return { + ...aggregateObjects({ objs: flatFlatHeaders }), + ...(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(sectionsWithQuestions, ['questions', 'header']); + + return { + ...(restSections && flattenObject({ obj: { sections: restSections } })), + ...(headers && flattenOneTrustSectionHeaders(headers, 'sections')), + ...(allQuestions && flattenOneTrustQuestions(allQuestions, 'sections')), + }; +}; + +export const flattenOneTrustAssessment = ( + combinedAssessment: OneTrustEnrichedAssessment, +): Record => { + const { + approvers, + primaryEntityDetails, + respondents, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + respondent, + sections, + ...rest + } = combinedAssessment; + + return { + ...flattenObject({ obj: rest }), + ...flattenObject({ obj: { approvers } }), + ...flattenObject({ obj: { respondents } }), + ...flattenObject({ obj: { primaryEntityDetails } }), + ...flattenOneTrustSections(sections), + }; +}; diff --git a/src/oneTrust/helpers/index.ts b/src/oneTrust/helpers/index.ts new file mode 100644 index 00000000..cb7534f6 --- /dev/null +++ b/src/oneTrust/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './flattenOneTrustAssessment'; +export * from './parseCliSyncOtArguments'; +export * from './syncOneTrustAssessmentToDisk'; diff --git a/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts b/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts new file mode 100644 index 00000000..4d46554c --- /dev/null +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsv.ts @@ -0,0 +1,33 @@ +import { OneTrustEnrichedAssessment } from '../codecs'; +import { oneTrustAssessmentToCsvRecord } from './oneTrustAssessmentToCsvRecord'; + +/** + * 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 assessmentCsvRecord = oneTrustAssessmentToCsvRecord(assessment); + + // write csv header at the beginning of the file + const csvRows = []; + if (index === 0) { + const header = Object.keys(assessmentCsvRecord).join(','); + csvRows.push(header); + } + + // 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..0eb76da6 --- /dev/null +++ b/src/oneTrust/helpers/oneTrustAssessmentToCsvRecord.ts @@ -0,0 +1,40 @@ +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 (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 + */ +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: + * 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] ?? ''; + 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/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 new file mode 100644 index 00000000..f6c8463c --- /dev/null +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -0,0 +1,189 @@ +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 to OneTrust */ + 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 */ + 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, + oneTrustAuth, + resource, + debug, + fileFormat, + dryRun, + transcendAuth, + transcendUrl, + } = yargs(process.argv.slice(2), { + string: [ + 'file', + 'hostname', + 'oneTrustAuth', + 'resource', + 'fileFormat', + 'dryRun', + 'transcendAuth', + 'transcendUrl', + ], + boolean: ['debug', 'dryRun'], + default: { + resource: OneTrustPullResource.Assessments, + fileFormat: OneTrustFileFormat.Csv, + debug: false, + dryRun: false, + transcendUrl: 'https://api.transcend.io', + }, + }); + + // Must be able to authenticate to transcend to sync resources to it + 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); + } + 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) { + logger.error( + colors.red( + `The "fileFormat" parameter must equal ${OneTrustFileFormat.Csv} to sync resources to Transcend.`, + ), + ); + return process.exit(1); + } + + // If trying to sync to disk, must specify a file path + if (dryRun && !file) { + logger.error( + colors.red( + '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); + } + + 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) { + logger.error( + colors.red( + 'Missing required parameter "hostname". e.g. --hostname=customer.my.onetrust.com', + ), + ); + return process.exit(1); + } + + if (!oneTrustAuth) { + logger.error( + colors.red( + 'Missing required parameter "oneTrustAuth". e.g. --oneTrustAuth=$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, + oneTrustAuth, + resource, + debug, + fileFormat, + dryRun, + transcendAuth, + transcendUrl, + }; +}; diff --git a/src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts b/src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts new file mode 100644 index 00000000..504515e9 --- /dev/null +++ b/src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts @@ -0,0 +1,53 @@ +import { logger } from '../../logger'; +import colors from 'colors'; +import { OneTrustFileFormat } from '../../enums'; +import fs from 'fs'; +import { OneTrustEnrichedAssessment } from '../codecs'; +import { oneTrustAssessmentToJson } from './oneTrustAssessmentToJson'; +import { oneTrustAssessmentToCsv } from './oneTrustAssessmentToCsv'; + +/** + * Write the assessment to disk at the specified file path. + * + * + * @param param - information about the assessment to write + */ +export const syncOneTrustAssessmentToDisk = ({ + file, + fileFormat, + assessment, + index, + total, +}: { + /** The file path to write the assessment to */ + file: string; + /** The format of the output file */ + fileFormat: OneTrustFileFormat; + /** The basic assessment */ + assessment: OneTrustEnrichedAssessment; + /** The index of the assessment being written to the file */ + index: number; + /** The total amount of assessments that we will write */ + total: number; +}): void => { + logger.info( + colors.magenta( + `Writing enriched assessment ${ + index + 1 + } of ${total} to file "${file}"...`, + ), + ); + + if (fileFormat === OneTrustFileFormat.Json) { + fs.appendFileSync( + file, + oneTrustAssessmentToJson({ + assessment, + index, + total, + }), + ); + } else if (fileFormat === OneTrustFileFormat.Csv) { + fs.appendFileSync(file, oneTrustAssessmentToCsv({ assessment, index })); + } +}; 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 new file mode 100644 index 00000000..8ee4e88a --- /dev/null +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -0,0 +1,113 @@ +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 { syncOneTrustAssessmentToDisk } from './syncOneTrustAssessmentToDisk'; +import { OneTrustFileFormat } from '../../enums'; +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 */ + 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 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) => { + 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 assessments with risk and details + const enrichedAssessment = enrichOneTrustAssessment({ + assessment, + assessmentDetails, + riskDetails, + }); + + if (dryRun && file && fileFormat) { + // sync to file + syncOneTrustAssessmentToDisk({ + assessment: enrichedAssessment, + index, + total: assessments.length, + file, + fileFormat, + }); + } else if (fileFormat === OneTrustFileFormat.Csv && transcend) { + // sync to transcend + await syncOneTrustAssessmentToTranscend({ + assessment: enrichedAssessment, + transcend, + }); + } + }); +}; diff --git a/src/oneTrust/index.ts b/src/oneTrust/index.ts index 24486cc7..4f127531 100644 --- a/src/oneTrust/index.ts +++ b/src/oneTrust/index.ts @@ -1,5 +1,3 @@ -export * from './getListOfAssessments'; export * from './createOneTrustGotInstance'; -export * from './getAssessment'; -export * from './writeOneTrustAssessment'; -export * from './parseCliPullOtArguments'; +export * from './helpers'; +export * from './endpoints'; diff --git a/src/oneTrust/parseCliPullOtArguments.ts b/src/oneTrust/parseCliPullOtArguments.ts deleted file mode 100644 index d1b42032..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.Json, - 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/src/oneTrust/types.ts b/src/oneTrust/types.ts deleted file mode 100644 index fa0d3c8a..00000000 --- a/src/oneTrust/types.ts +++ /dev/null @@ -1,436 +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: string; - /** 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; -} - -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; -} - -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[]; -} - -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 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; -} - -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: string; - /** 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; - /** 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: string | 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; -} -/* eslint-enable max-lines */ diff --git a/src/oneTrust/writeOneTrustAssessment.ts b/src/oneTrust/writeOneTrustAssessment.ts deleted file mode 100644 index 3e816317..00000000 --- a/src/oneTrust/writeOneTrustAssessment.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { logger } from '../logger'; -import colors from 'colors'; -import { OneTrustFileFormat } from '../enums'; -import { OneTrustAssessment, OneTrustGetAssessmentResponse } from './types'; -import fs from 'fs'; - -/** - * Write the assessment to disk at the specified file path. - * - * - * @param param - information about the assessment to write - */ -export const writeOneTrustAssessment = ({ - file, - // TODO: https://transcend.height.app/T-41372 - support converting to CSV - // fileFormat, - assessment, - assessmentDetails, - index, - total, -}: { - /** The file path to write the assessment to */ - file: string; - /** The format of the output file */ - fileFormat: OneTrustFileFormat; - /** The basic assessment */ - assessment: OneTrustAssessment; - /** The assessment with details */ - assessmentDetails: OneTrustGetAssessmentResponse; - /** The index of the assessment being written to the file */ - index: number; - /** The total amount of assessments that we will write */ - total: number; -}): void => { - logger.info( - colors.magenta( - `Syncing enriched assessment ${ - index + 1 - } of ${total} to file "${file}"...`, - ), - ); - - // Start with an opening bracket - if (index === 0) { - fs.writeFileSync(file, '[\n'); - } - - // combine the two assessments into a single stringified 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 ? ',' : ''; - - // write to file - fs.appendFileSync(file, stringifiedAssessment + comma); - - // End with closing bracket - if (index === total - 1) { - fs.appendFileSync(file, ']'); - } -}; 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({ diff --git a/yarn.lock b/yarn.lock index 9e62ba0d..31b7c4e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -515,9 +515,9 @@ __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 + "@transcend-io/type-utils": ^1.8.0 "@types/bluebird": ^3.5.38 "@types/chai": ^4.3.4 "@types/cli-progress": ^3.11.0 @@ -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 @@ -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 @@ -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.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: 0d7d85e794254069663b277e7728a39fe2d7c6b96eef4e71e6a971cd44f2b1a1be20cb82d708603182ed5b7e9ad20535c845590df59e8302d9ab6f70c626464f + checksum: e4a3784e932cbdd9499c3d5d1246a5b2951063a3a8d5ee3be740775b92b7dd10ce1eed21eec85a508e7f98354e0c80a002f89218b541378f9efd1e7fc1b5f155 languageName: node linkType: hard