Link SOC 2 documents to entity
This guide explains how to use a Workflow to streamline the process of linking SOC 2 documents to an entity.
Create a Workflow to link SOC 2 documents to an entity
Prerequisites
Before getting started:
You must have the
Edit Workflowspermission to create the Workflow, and theExecute Workflow runspermission to run it.
Step 1: Create the Workflow
You can create a Workflow in the Cortex UI or via the Cortex CLI.
Add the Workflow via CLI
You can use the Cortex CLI to add the example Workflow to your workspace. This allows you to quickly set up the example configuration then iterate on it for your own use case. Expand the tile below to learn more:
Import the Workflow via CLI
Save the Workflow example YAML file below:
name: Link SOC2 Documents to Entity
tag: link-soc2-documents-to-an-entity
description: null
isDraft: true
filter:
entityFilter:
typeFilter:
types:
- service
entityGroupFilter: null
ownershipScope: ALL
type: ENTITY
runResponseTemplate: null
failedRunResponseTemplate: null
restrictActionCompletionToRunnerUser: false
actions:
- name: User input
slug: user-input
schema:
inputs:
- name: Link Type
description: null
key: link-type
required: false
options:
- pen-test
- data-retention-policy
optionsLabels:
- Pen Test
- Data Retention Policy
defaultValue: null
placeholder: null
allowAdditionalOptions: false
type: SELECT_FIELD
- name: Link URL
description: null
key: link-url
required: false
defaultValue: null
placeholder: null
validationRegex: null
type: INPUT_FIELD
inputOverrides: []
jsValidatorScript: ""
type: USER_INPUT
outgoingActions:
- retrieve-entity-descriptor
isRootAction: true
- name: Retrieve entity descriptor
slug: retrieve-entity-descriptor
schema:
inputs:
entityId: "{{context.entity.tag}}"
integrationAlias: null
actionIdentifier: cortex.retrieveEntityDescriptor
type: ADVANCED_HTTP_REQUEST
outgoingActions:
- javascript
isRootAction: false
- name: JavaScript
slug: javascript
schema:
script: |+
const descriptor = actions['retrieve-entity-descriptor']?.outputs?.descriptor;
const linkType = actions['user-input']?.outputs?.['link-type'];
const linkUrl = actions['user-input']?.outputs?.['link-url'];
if (!descriptor || typeof descriptor !== 'string') {
throw new Error('Descriptor must be a YAML string.');
}
const rawType = String(linkType || '').trim();
const rawUrl = String(linkUrl || '').trim();
if (!rawType || !rawUrl) {
throw new Error('Both link-type and link-url are required.');
}
// Normalize input for mapping
const normalize = s => s.toLowerCase().replace(/[_\s-]+/g, ' ').trim();
const normType = normalize(rawType);
let finalType, finalName;
if (normType === 'pen test' || normType === 'pentest') {
finalType = 'pen-test';
finalName = 'Pen Test';
} else if (normType === 'data retention policy') {
finalType = 'documentation';
finalName = rawType; // keep exactly what the user selected
} else {
finalType = rawType;
finalName = rawType;
}
// Parse YAML
let obj;
try {
if (typeof YAML.load === 'function') {
obj = YAML.load(descriptor) || {};
} else if (typeof YAML.parse === 'function') {
obj = YAML.parse(descriptor) || {};
} else {
throw new Error('YAML parser not available in this runtime.');
}
} catch (err) {
throw new Error('Failed to parse descriptor YAML: ' + err.message);
}
// Ensure x-cortex-link exists
if (!obj.info) obj.info = {};
const linkKey = 'x-cortex-link';
const existing = obj.info[linkKey];
obj.info[linkKey] = Array.isArray(existing) ? existing : (existing ? [existing] : []);
// Normalize existing links
const current = obj.info[linkKey]
.filter(Boolean)
.map(l => ({
url: l.url ? String(l.url).trim() : '',
name: l.name ? String(l.name).trim() : '',
type: l.type ? String(l.type).trim() : '',
}))
.filter(l => l.url && l.name && l.type);
// Add new link if not duplicate
const incoming = { url: rawUrl, name: finalName, type: finalType };
const sig = l => `${l.url}::${l.type}::${l.name}`;
const seen = new Set(current.map(sig));
if (!seen.has(sig(incoming))) current.push(incoming);
obj.info[linkKey] = current.map(l => ({ url: l.url, name: l.name, type: l.type }));
// Dump back to YAML
let updatedDescriptor;
try {
if (typeof YAML.dump === 'function') {
updatedDescriptor = YAML.dump(obj, { indent: 2, lineWidth: 120 });
} else if (typeof YAML.stringify === 'function') {
updatedDescriptor = YAML.stringify(obj, { indent: 2 });
} else {
throw new Error('YAML serializer not available.');
}
} catch (err) {
throw new Error('Failed to serialize descriptor YAML: ' + err.message);
}
// ✅ Return updatedDescriptor (not wrapped in {body=...})
return { updatedDescriptor };
type: JAVASCRIPT
outgoingActions:
- create-or-update-entity
isRootAction: false
- name: Create or update entity
slug: create-or-update-entity
schema:
inputs:
body: "{{{actions.javascript.outputs.result.updatedDescriptor}}}"
dryRun: false
integrationAlias: null
actionIdentifier: cortex.createOrUpdateEntity
type: ADVANCED_HTTP_REQUEST
outgoingActions: []
isRootAction: false
runRestrictionPolicies: []
iconTag: SecurityCamera
variables: []Use the Cortex CLI to run this command, using the path to your Workflow YAML file:
cortex workflows create -f <path-to-your-workflow.yaml>
Step 1.1: Create the Workflow and configure basic settings
In Cortex, navigate to Workflows. In the upper right corner, click +Create workflow. Choose a blank Workflow.
Follow the documentation to configure the basic settings for your Workflow.
Step 1.2: Add blocks to the Workflow
Add the following blocks to your Workflow:
User input
Add a User Input block to collect the link type, link URL, and which entity to update.
Click + in the center of the page. In the block library modal, choose User input.
In the block configuration side panel, enter a name and unique slug for this block.
In this example, we use the name
User inputand the sluguser-input.
Click +Add user input. Add the following:
Name: Link type
Key: link-type
Type: Select
Data source: Manual
Options: Click +Add option to add options for
Pen TestandData Retention Policy.Click Add input.
Click +Add user input. Add the following:
Name: Link URL
Key: link-url
Type: Text
Click Add input.
At the bottom of the side panel, click Save.
Retrieve entity descriptor
This block retrieves the entity descriptor.
Click + in the center of the page. In the block library modal, select the Cortex > Retrieve entity descriptor block.
In the side panel, enter a name and unique slug for the block.
In this example, we use the name
Retrieve entity descriptorand the slugretrieve-entity-descriptor.
Configure the block:
Entity ID or tag:
{{context.entity.tag}}
At the bottom of the side panel, click Save.
JavaScript
This step retrieves the entity tag, validates required inputs, normalizes the data, and returns the updated link object in YAML format for use in the next Workflow block.
Click + in the center of the page. In the block library modal, select the Cortex > Retrieve entity descriptor block.
In the side panel, enter a name and unique slug for the block.
In this example, we use the name
JavaScriptand the slugjavascript.
In the text editor under the JavaScript header, enter the following:
const descriptor = actions['retrieve-entity-descriptor']?.outputs?.descriptor;
const linkType = actions['user-input']?.outputs?.['link-type'];
const linkUrl = actions['user-input']?.outputs?.['link-url'];
if (!descriptor || typeof descriptor !== 'string') {
throw new Error('Descriptor must be a YAML string.');
}
const rawType = String(linkType || '').trim();
const rawUrl = String(linkUrl || '').trim();
if (!rawType || !rawUrl) {
throw new Error('Both link-type and link-url are required.');
}
// Normalize input for mapping
const normalize = s => s.toLowerCase().replace(/[_\s-]+/g, ' ').trim();
const normType = normalize(rawType);
let finalType, finalName;
if (normType === 'pen test' || normType === 'pentest') {
finalType = 'pen-test';
finalName = 'Pen Test';
} else if (normType === 'data retention policy') {
finalType = 'documentation';
finalName = rawType; // keep exactly what the user selected
} else {
finalType = rawType;
finalName = rawType;
}
// Parse YAML
let obj;
try {
if (typeof YAML.load === 'function') {
obj = YAML.load(descriptor) || {};
} else if (typeof YAML.parse === 'function') {
obj = YAML.parse(descriptor) || {};
} else {
throw new Error('YAML parser not available in this runtime.');
}
} catch (err) {
throw new Error('Failed to parse descriptor YAML: ' + err.message);
}
// Ensure x-cortex-link exists
if (!obj.info) obj.info = {};
const linkKey = 'x-cortex-link';
const existing = obj.info[linkKey];
obj.info[linkKey] = Array.isArray(existing) ? existing : (existing ? [existing] : []);
// Normalize existing links
const current = obj.info[linkKey]
.filter(Boolean)
.map(l => ({
url: l.url ? String(l.url).trim() : '',
name: l.name ? String(l.name).trim() : '',
type: l.type ? String(l.type).trim() : '',
}))
.filter(l => l.url && l.name && l.type);
// Add new link if not duplicate
const incoming = { url: rawUrl, name: finalName, type: finalType };
const sig = l => `${l.url}::${l.type}::${l.name}`;
const seen = new Set(current.map(sig));
if (!seen.has(sig(incoming))) current.push(incoming);
obj.info[linkKey] = current.map(l => ({ url: l.url, name: l.name, type: l.type }));
// Dump back to YAML
let updatedDescriptor;
try {
if (typeof YAML.dump === 'function') {
updatedDescriptor = YAML.dump(obj, { indent: 2, lineWidth: 120 });
} else if (typeof YAML.stringify === 'function') {
updatedDescriptor = YAML.stringify(obj, { indent: 2 });
} else {
throw new Error('YAML serializer not available.');
}
} catch (err) {
throw new Error('Failed to serialize descriptor YAML: ' + err.message);
}
// ✅ Return updatedDescriptor (not wrapped in {body=...})
return { updatedDescriptor };At the bottom of the side panel, click Save.
Create or update entity
This block takes the object returned in the previous block and uses it to update the entity.
Click + in the center of the page. In the block library modal, select the Cortex > Create or update entity block.
In the side panel, enter a name and unique slug for the block.
In this example, we use the name
Create or update entityand the slugcreate-or-update-entity.
Configure the block:
Descriptor:
{{{actions.javascript.outputs.result.updatedDescriptor}}}
At the bottom of the side panel, click Save.
When you are finished adding blocks, click Save workflow in the upper right corner of the page.
Step 2: Run the Workflow
In the list of Workflows, locate the "Link SOC 2 Documents to Entity" Workflow and click Run.
When you run the Workflow, the following events happen:
The Workflow pauses to collect a response from the user during the User Input block. The user selects an entity, then enters a link type and link URL.
The next step runs, retrieving the entity descriptor.
The JavaScript block runs, which does the following:
It retrieves the entity descriptor from the previous block, and it retrieves the link type and URL from the initial block. It validates that the required inputs exist.
It normalizes the link type. It parses the descriptor string into a JavaScript object.
It creates a new link object with url, name, and type, and it only adds the new link if it doesn't already exist.
It converts the updated object back to YAML format, and returns
{ updatedDescriptor }for use in subsequent Workflow blocks.
The "Create or update entity" block runs, updating the entity with the
updatedDescriptoroutput of the previous block.
Last updated
Was this helpful?