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.

Prerequisites

Before getting started:

  • You must have the Edit Workflows permission to create the Workflow, and the Execute Workflow runs permission 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
  1. 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: []
  1. 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 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:

  1. 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.

  2. The next step runs, retrieving the entity descriptor.

  3. The JavaScript block runs, which does the following:

    1. 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.

    2. It normalizes the link type. It parses the descriptor string into a JavaScript object.

    3. It creates a new link object with url, name, and type, and it only adds the new link if it doesn't already exist.

    4. It converts the updated object back to YAML format, and returns { updatedDescriptor } for use in subsequent Workflow blocks.

  4. The "Create or update entity" block runs, updating the entity with the updatedDescriptor output of the previous block.

Last updated

Was this helpful?