diff --git a/README.md b/README.md index f15b1b2d..1b8deb87 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,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-push --auth=$TRANSCEND_API_KEY yarn tr-scan-packages --auth=$TRANSCEND_API_KEY yarn tr-discover-silos --auth=$TRANSCEND_API_KEY @@ -212,6 +213,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-push --auth=$TRANSCEND_API_KEY tr-scan-packages --auth=$TRANSCEND_API_KEY tr-discover-silos --auth=$TRANSCEND_API_KEY @@ -571,6 +573,43 @@ 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 + +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. + +This command can be helpful if you are looking to: + +- Pull resources from your OneTrust account. +- Migrate your resources from your OneTrust account to Transcend. + +#### Authentication + +In order to use this command, you will need to generate a OneTrust OAuth Token with scope for accessing the following endpoints: + +- [GET /v2/assessments](https://developer.onetrust.com/onetrust/reference/getallassessmentbasicdetailsusingget) +- [GET /v2/assessments/{assessmentId}/export](https://developer.onetrust.com/onetrust/reference/exportassessmentusingget) +- [GET /risks/{riskId}](https://developer.onetrust.com/onetrust/reference/getriskusingget) + +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. + +#### 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 | + +#### Usage + +```sh +# Writes out file to ./oneTrustAssessments.json +tr-pull-ot --auth=$ONE_TRUST_OAUTH_TOKEN --hostname=trial.onetrust.com --file=./oneTrustAssessments.json +``` + ### tr-push Given a transcend.yml file, sync the contents up to your connected services view (https://app.transcend.io/privacy-requests/connected-services). diff --git a/package.json b/package.json index de0cec7e..e2c927e9 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.12.1", + "version": "6.13.0", "homepage": "https://github.com/transcend-io/cli", "repository": { "type": "git", @@ -28,6 +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-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-pull-ot.ts new file mode 100644 index 00000000..1ea67f36 --- /dev/null +++ b/src/cli-pull-ot.ts @@ -0,0 +1,76 @@ +#!/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/enums.ts b/src/enums.ts index 1655b8a3..b5aca7de 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -1,5 +1,18 @@ import { makeEnum } from '@transcend-io/type-utils'; +/** Accepted file formats for exporting resources from OneTrust */ +export enum OneTrustFileFormat { + Json = 'json', + Csv = 'csv', +} + +/** + * Resources that can be pulled in from OneTrust + */ +export enum OneTrustPullResource { + Assessments = 'assessments', +} + /** * Resources that can be pulled in */ @@ -50,7 +63,7 @@ export const PathfinderPolicyName = makeEnum({ * Type override */ export type PathfinderPolicyName = - typeof PathfinderPolicyName[keyof typeof PathfinderPolicyName]; + (typeof PathfinderPolicyName)[keyof typeof PathfinderPolicyName]; /** * The names of the OpenAI routes that we support setting policies for @@ -76,4 +89,4 @@ export const OpenAIRouteName = makeEnum({ * Type override */ export type OpenAIRouteName = - typeof OpenAIRouteName[keyof typeof OpenAIRouteName]; + (typeof OpenAIRouteName)[keyof typeof OpenAIRouteName]; diff --git a/src/oneTrust/createOneTrustGotInstance.ts b/src/oneTrust/createOneTrustGotInstance.ts new file mode 100644 index 00000000..04dd72fd --- /dev/null +++ b/src/oneTrust/createOneTrustGotInstance.ts @@ -0,0 +1,25 @@ +import got, { Got } from 'got'; + +/** + * Instantiate an instance of got that is capable of making requests to OneTrust + * + * @param param - information about the OneTrust URL + * @returns The instance of got that is capable of making requests to the customer ingress + */ +export const createOneTrustGotInstance = ({ + hostname, + auth, +}: { + /** Hostname of the OneTrust API */ + hostname: string; + /** The OAuth access token */ + auth: string; +}): Got => + got.extend({ + prefixUrl: `https://${hostname}`, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + authorization: `Bearer ${auth}`, + }, + }); diff --git a/src/oneTrust/getAssessment.ts b/src/oneTrust/getAssessment.ts new file mode 100644 index 00000000..2b81b028 --- /dev/null +++ b/src/oneTrust/getAssessment.ts @@ -0,0 +1,24 @@ +import { Got } from 'got'; +import { OneTrustGetAssessmentResponse } from './types'; + +/** + * Retrieve details about a particular assessment. + * + * @param param - the information about the OneTrust client and assessment to retrieve + * @returns details about the assessment + */ +export const getAssessment = async ({ + oneTrust, + assessmentId, +}: { + /** The OneTrust client instance */ + oneTrust: Got; + /** The ID of the assessment to retrieve */ + assessmentId: string; +}): Promise => { + const { body } = await oneTrust.get( + `api/assessment/v2/assessments/${assessmentId}/export?ExcludeSkippedQuestions=false`, + ); + + return JSON.parse(body) as OneTrustGetAssessmentResponse; +}; diff --git a/src/oneTrust/getListOfAssessments.ts b/src/oneTrust/getListOfAssessments.ts new file mode 100644 index 00000000..926d786c --- /dev/null +++ b/src/oneTrust/getListOfAssessments.ts @@ -0,0 +1,49 @@ +import { Got } from 'got'; +import { logger } from '../logger'; +import { + OneTrustAssessment, + OneTrustGetListOfAssessmentsResponse, +} from './types'; + +/** + * Fetch a list of all assessments from the OneTrust client. + * + * @param param - the information about the OneTrust client + * @returns a list of OneTrustAssessment + */ +export const getListOfAssessments = async ({ + oneTrust, +}: { + /** The OneTrust client instance */ + oneTrust: Got; +}): Promise => { + let currentPage = 0; + let totalPages = 1; + let totalElements = 0; + + 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( + body, + ) as OneTrustGetListOfAssessmentsResponse; + allAssessments.push(...(content ?? [])); + if (currentPage === 0) { + totalPages = page?.totalPages ?? 0; + totalElements = page?.totalElements ?? 0; + } + currentPage += 1; + + // log progress + logger.info( + `Fetched ${allAssessments.length} of ${totalElements} assessments.`, + ); + } + + return allAssessments; +}; diff --git a/src/oneTrust/index.ts b/src/oneTrust/index.ts new file mode 100644 index 00000000..24486cc7 --- /dev/null +++ b/src/oneTrust/index.ts @@ -0,0 +1,5 @@ +export * from './getListOfAssessments'; +export * from './createOneTrustGotInstance'; +export * from './getAssessment'; +export * from './writeOneTrustAssessment'; +export * from './parseCliPullOtArguments'; diff --git a/src/oneTrust/parseCliPullOtArguments.ts b/src/oneTrust/parseCliPullOtArguments.ts new file mode 100644 index 00000000..d1b42032 --- /dev/null +++ b/src/oneTrust/parseCliPullOtArguments.ts @@ -0,0 +1,115 @@ +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 new file mode 100644 index 00000000..fa0d3c8a --- /dev/null +++ b/src/oneTrust/types.ts @@ -0,0 +1,436 @@ +/* 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 new file mode 100644 index 00000000..3e816317 --- /dev/null +++ b/src/oneTrust/writeOneTrustAssessment.ts @@ -0,0 +1,65 @@ +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/yarn.lock b/yarn.lock index b320739b..9e62ba0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -593,6 +593,7 @@ __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