LogoLogo
  • AttackForge Support
  • Release Notes
    • 2025
    • 2024
    • 2023
    • 2022
    • 2021
    • 2020
    • 2019
  • Core & Enterprise
    • Getting Started
      • How It Works
      • Requesting A Project
      • Creating & Updating Projects
      • Invite User To Project
      • View Project Team
      • Project Scope
      • Project Workspace
      • Project Notes
      • Project Pages
      • Test Cases
      • Creating Vulnerabilities
      • Updating Vulnerabilities
      • Review & QA
      • Attack Chains
      • Reporting
      • Retesting & Remediation
      • Notifications
      • Custom Fields & Forms
      • Vulnerability SLAs
      • User Settings
      • Login Redirects
    • Modules
      • Dashboard
      • Analytics
      • Vulnerabilities
      • Projects
      • Scheduling
      • Portfolios
      • Groups
      • Attack Chains
      • Assets
      • Writeups
      • Test Suites
      • Report Templates
        • Overview
        • Tutorial
        • Tips & Tricks
        • Troubleshooting
        • Template - Report Templates
        • Template - Tags
        • Template - Options
        • Template - Functions
        • Template - Filters
        • Template - Styles
        • Template - Tables
        • Template - Charts
        • Template - Conditions
        • ReportGen CLI
        • ReportGen Library
      • Users
      • Administration
      • Flows
      • Self-Service RESTful API
        • GETTING STARTED
        • EXPORTING TO CSV
        • ADVANCED QUERY FILTER
        • ActivateUser
        • AddProjectMembershipAdministrators
        • AddTestcaseToTestsuite
        • AddTestcasesToTestsuite
        • AddUserToGroup
        • ApproveProjectRequestById
        • ArchivePortfolio
        • ArchiveProject
        • CancelProjectRetestRound
        • CloneProject
        • CompleteProjectRetestRound
        • CreateAssetInLibrary
        • CreateGroup
        • CreatePortfolio
        • CreateProject
        • CreateProjectNote
        • CreateProjectRequest
        • CreateProjectTestCase
        • CreateProjectWorkspaceNote
        • CreateRemediationNote
        • CreateScope
        • CreateTestcaseNote
        • CreateTestsuite
        • CreateUser
        • CreateUsers
        • CreateVulnerability
        • CreateVulnerabilityBulk
        • CreateVulnerabilityLibraryIssue
        • CreateVulnerabilityWithLibrary
        • DeactivateUser
        • DownloadProjectTestCaseFile
        • DownloadProjectTestCaseNoteFile
        • DownloadProjectTestCaseWorkspaceNoteFile
        • DownloadVulnerabilityEvidence
        • DownloadVulnerabilityLibraryFile
        • DownloadWorkspaceFile
        • GetApplicationAuditLogs
        • GetAssets
        • GetAssetsByGroup
        • GetAssetInLibrary
        • GetAssetsInLibrary
        • GetCustomFieldsConfig
        • GetFormConfig
        • GetGroup
        • GetGroups
        • GetMostCommonVulnerabilities
        • GetMostFailedTestcases
        • GetMostVulnerableAssets
        • GetPortfolio
        • GetPortfolios
        • GetPortfolioStream
        • GetProjectAuditLogs
        • GetProjectById
        • GetProjects
        • GetProjectsAndVulnerabilities
        • GetProjectsByGroup
        • GetProjectMembershipAdministrators
        • GetProjectNotes
        • GetProjectReport
        • GetProjectReportData
        • GetProjectRequests
        • GetProjectRequestById
        • GetProjectTestcasesById
        • GetProjectVulnerabilitiesById
        • GetProjectWorkspace
        • GetTestsuiteById
        • GetTestsuites
        • GetUserByEmail
        • GetUserById
        • GetUserByUsername
        • GetUserAuditLogs
        • GetUserGroups
        • GetUserLoginHistory
        • GetUserProjects
        • GetUsers
        • GetVulnerabilityById
        • GetVulnerabilities
        • GetVulnerabilitiesByAssetName
        • GetVulnerabilitiesByGroup
        • GetVulnerabilityLibraryIssues
        • GetVulnerabilityRevisionHistory
        • InviteUserToProject
        • InviteUsersToProjectTeam
        • RejectProjectRequestById
        • RegenerateAPIKey
        • RemoveProjectMembershipAdministrators
        • RemoveProjectTeamMembers
        • RequestNewProjectRetest
        • RestoreProject
        • SendEmail
        • SendDailyCommencementEmail
        • SendDailyCompletionEmail
        • UpdateAssetInLibrary
        • UpdateCustomFieldsConfig
        • UpdateExecSummaryNotes
        • UpdateFormConfig
        • UpdateGroup
        • UpdatePortfolio
        • UpdateProjectById
        • UpdateProjectMembershipAdministrators
        • UpdateProjectNote
        • UpdateProjectRequestById
        • UpdateProjectRetestRound
        • UpdateProjectWorkspaceNote
        • UpdateScope
        • UpdateTestcase
        • UpdateTestcaseOnTestsuite
        • UpdateTestsuite
        • UpdateUserAccessOnGroup
        • UpdateUserAccessOnProject
        • UpdateUser
        • UpdateVulnerabilityById
        • UpdateVulnerabilityLibraryIssue
        • UpdateVulnerabilitySLAs
        • UpdateVulnerabilityWithLibrary
        • UploadTestcaseFile
        • UploadVulnerabilityEvidence
        • UploadVulnerabilityLibraryFile
        • UploadWorkspaceFile
      • Self-Service Events API
        • GETTING STARTED
        • Project Created
        • Project Updated
        • Project Request Created
        • Project Request Updated
        • Project Retest Requested
        • Project Retest Completed
        • Project Retest Cancelled
        • Vulnerability Created
        • Vulnerability Updated
        • Vulnerability Evidence Created
        • Vulnerability Evidence Updated
        • Vulnerability Remediation Note Created
        • Vulnerability Remediation Note Updated
    • AFScript
    • Access Control Matrix
    • Raising Support Tickets
    • Security
  • Contact
Powered by GitBook

Check YouTube for more tutorials: https://youtube.com/@attackforge

On this page
  • Overview
  • Getting Access to Flows
  • Flow Overview
  • Run Overview
  • Sharing Flows with Teams
  • Triggers
  • Secrets
  • Secrets in Headers
  • Secrets in Request/Response Scripts
  • Actions
  • Methods
  • URL
  • Headers
  • Request
  • Response
  • Request Script
  • Response Script
  • Code
  • Data
  • The Data Object
  • The Response Object
  • The Return Statement
  • Runs
  • Run Logs
  • Manually Run Flow
  • Re-Run
  • Importing/Exporting Flows
  • Examples
  • Create JIRA Issue
  • Update JIRA Issue
  • Create ServiceNow Incident
  • Create Azure DevOps Work Item
  • Prioritize Vulnerability with Threat Intelligence from VulnDB
  • Trigger an Automated Scan in Tenable
  • Create Slack Message
  • Create Teams Message
  • Send Vulnerability to PowerBI
  • Create Salesforce Opportunity
  • Create a Webhook
  • Send Custom Email
  1. Core & Enterprise
  2. Modules

Flows

PreviousAdministrationNextSelf-Service RESTful API

Last updated 1 month ago

Overview

The Flows module is a comprehensive, end-to-end automation engine powered by .

Flows can help you to automate AttackForge with nearly unlimited systems. You can streamline processes across your organization to save time and focus on what's important.

Some examples you can do with Flows:

  • Create custom webhooks.

  • Send custom email notifications on events.

Getting Access to Flows

Flows is included in all AttackForge Enterprise plans, and in the AttackForge Core SME plan. For all others plans, Flows can be add-on from the Administration -> Subscriptions page.

To get started with building a Flow:

  • You must be granted access to another person's existing Flow

Flow Overview

A Flow is comprised of the following:

  • Name - the name of the Flow.

Run Overview

A Run refers to a single execution of a Flow, meaning when a set of actions defined in your Flow is triggered and carried out from start to finish, that is considered one "Run" of the flow; essentially, it's a single instance of your Flow being executed.

Key points about a Run:

  • Trackable status - You can monitor the status of a Run, including whether it succeeded, failed, or is currently running.

Sharing Flows with Teams

When a Flow is created, it belongs to the user who created the Flow (the Flow Owner). Flow Owners cannot be changed (for the time being).

Only Flow Owners are allowed to share their Flows with other users.

To share your Flow:

  • Open your Flow and click on the Settings button

  • Click on Add Access

  • Insert the user's email address or look up and select the user

Once a user has been given access to a Flow, they will be able to do the following:

  • Manually run the Flow

  • Enable/Disable the Flow

You can remove a users' access to your Flows from the Settings.

Triggers

The following Triggers are currently supported:

  • Project Created

  • Project Updated

  • Project Request Created

  • Project Request Updated

  • Project Retest Requested

  • Project Retest Completed

  • Project Retest Cancelled

  • Vulnerability Created

  • Vulnerability Updated

  • Vulnerability Remediation Note Created

  • Vulnerability Remediation Note Updated

  • Vulnerability Evidence Created

  • Vulnerability Evidence Updated

A Flow can be assigned to only one Trigger.

Triggers can be assigned to a Flow when either creating or editing the Flow.

COMING SOON! Ability to create your own custom Triggers which are invoked from custom URLs. This will allow you to execute a Flow on-demand whenever you would need it to run, and also allow for creating custom webhook receivers for your security toolsets and systems to communicate back to AttackForge.

Secrets

Secrets are any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.

You can create Secrets which belong to the Flow. Only users with access to the Flow would be able to view the associated Secrets.

To create a Secret, start by clicking on the Secrets button when creating or editing a Flow.

From here, you can see and manage all of the existing Secrets associated to the Flow.

Click on Add Secret to create a new Secret and enter a Key and a Value. Note the Key must be letters, numbers and underscores only.

NOTE: Secrets are stored encrypted in the database.

COMING SOON: You will be able to create User Secrets which belong to the user and are managed in one place. This makes is easy to rotate passwords and credentials without having to update the Flows.

There are two (2) ways in which you can refer to your Secrets in your Flow:

Secrets in Headers

Secrets in Request/Response Scripts

secrets.<KEY>

Where <KEY> is replaced with the Key associated with the Secret.

IMPORTANT: Make sure to select Use Secrets to ensure your secrets are used in your script.

Actions

For example, if the use case for your Flow is:

  • to create a JIRA Issue every time a Vulnerability is created

You may choose to include two (2) Actions in your Flow:

  • Action 1 - Create JIRA Issue

  • Action 2 - Update Vulnerability with JIRA Issue Key

Every Action is made up of the following components:

  • Verify Certificate - this determines whether to verify if the TLS certificate is valid for the URL.

IMPORTANT: When more than one Action is included in a Flow, the output of an Action will become the input into the next Action.

Methods

The following methods are supported:

  • GET

  • POST

  • PUT

  • PATCH

  • DELETE

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': secrets.jira_auth
        },
        body: {
            fields: {
                summary: summary,
                description: description,
                priority: {
                    name: priority
                },
                issuetype: {
                    name: 'Bug'
                },
                labels: labels
            }
        }
    }
};

URL

const url = 'https://demo.attackforge.com/api/ss/vulnerability/' + data.vulnerability_id;

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
            'x-ssapi-key': secrets.af_auth
        },
        body: {
            project_id: project_id,
            custom_fields: [
                {
                    key: 'jira_issue_key',
                    value: jira_issue_key
                }
            ]
        }
    }
};

Headers

const customHeaderName = 'X_CUSTOM_HEADER_' + customHeader;
const customHeaderValue = customValue;

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            customHeaderName: customHeaderValue,
            'Authorization': secrets.custom_auth
        },
        body: {
            super_secret_something: "..."
        }
    }
};

Request

The Request is a HTTP/HTTPS request to a web address.

The Request is made up of the following components:

  • Body - an optional HTTP body. This is typically required for POST, PUT and PATCH HTTP requests.

IMPORTANT: Requests can be made over both HTTP and HTTPS.

Response

The Response is made of the of the following components:

Request Script

The Request Script is made up of the following components:

Response Script

The Request Script is made up of the following components:

Code

This makes it possible to write logic to help you handle all various use cases for how you want your Flows to work.

You can test and debug your code using the Run option:

If your code fails after running it, you will see an error message with the relevant stack trace:

Data

You can reference the information included within Data as follows:

data.<KEY>

The Data Object

In the following example, we can see that Data Object has vulnerability-related information due to the "vulnerability-created" Event.

You can refer to keys on the Data Object using the following syntax:

data.<KEY>

Using the example above, if you wanted to store the vulnerability Id in a constant, you could do the following:

const vuln_id = data.vulnerability_id;

Keeping with the example above, if you wanted to extract the project Id for the vulnerability, you could do the following:

let project_id = undefined;

if (data.vulnerability_projects) {
    for (let x = 0; x < data.vulnerability_projects.length; x++) {
        if (data.vulnerability_projects[x].id) {
            project_id = data.vulnerability_projects[x].id;
        }
    }
}
return {
    decision: { 
        status: 'continue'
    },
    data: {
        af_project_id: project_id,
        af_vuln_id: vuln_id,
        af_vuln: data
    }
}
   
data = {
    af_project_id: "...",
    af_vuln_id: "...",
    af_vuln: {
        "vulnerability_title": "...",
        ...
    }
}
Logger.debug('Data:');
Logger.debug(JSON.stringify(data));

The Response Object

The Response Object is made up of the following:

You can refer to keys on the Response Object using the following syntax:

response.<KEY>

An example of the Response Object:

response = {
    "statusCode": 200,
    "headers": {},
    "body": ""
}
const body = JSON.parse(response.body);
Logger.debug('Response:');
Logger.debug(JSON.stringify(response));

The Return Statement

The Return Statement is made up of the following components:

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }, 
   request: { 
     url: 'https://www.attackforge.com/api',
     body: {}, 
     headers: {}, 
     method: 'GET', 
   }, 
   data: {} 
};

Decision

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }
};

The decision is the action that your script will take. A decision is made up of the following components:

  • status - a supported status (see below)

  • message - an optional message to display in the logs

You can also include the decision as a string if you do not need to include a message:

return {
   decision: "continue" //also supports "next", "abort", "finish"
};

The following statuses are supported:

CONTINUE

Example using Continue:

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }
};

NEXT

Example using Next:

return {
   decision: { 
     status: 'next', 
     message: 'Asset already exists. Move to create vulnerability', 
   }
};

ABORT

Abort will terminate your Flow as an error condition. This is useful in cases where your Flow can no longer proceed due to various reasons.

Example using Abort:

return {
   decision: { 
     status: 'abort', 
     message: 'Auth token not generated. Check if credentials are valid?', 
   }
};

FINISH

Finish will terminate your Flow as an success condition. This is how you would normally terminate a Flow.

Example using Finish:

return {
   decision: { 
     status: 'finish', 
     message: 'Vulnerability created!', 
   }
};

Request

  • url

  • body

  • headers

  • method

return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }, 
   request: { 
     url: 'https://www.attackforge.com/api',
     body: {}, 
     headers: {}, 
     method: 'GET', 
   }
};

URL

BODY

Example with a JSON Body:

return {
    decision: { 
        status: 'continue'
    },
    request: {
        body: {
            fields: {
                project: {
                    key: jiraProjectKey
                },
                summary: summary,
                description: description,
                priority: {
                    name: priority
                },
                issuetype: {
                    name: 'Bug'
                },
                labels: labels
            }
        }
    }
};

HEADERS

Example Headers:

return {
    decision: { 
        status: 'continue',
    },
    request: {
        headers: {
            'Content-Type': 'application/json',
            'x-ssapi-key': secrets.af_auth,
        }
    }
};

METHOD

Example Headers:

return {
    decision: { 
        status: 'continue',
    },
    request: {
        method: 'GET' //supports GET, POST, PUT, PATCH, DELETE
    }
};

Data

return {
    decision: { 
        status: 'continue',
    },
    data: {
        af_project_id: afProjectId,
        af_vuln: data
    }
};
return {
    decision: { 
        status: 'continue',
    },
    data: {
        af_project_id: data.af_project_id,
        af_vuln: data.af_vuln
    }
};

Runs

Run Logs

  • Event - the related Event which triggered the Flow.

Manually Run Flow

Re-Run

Importing/Exporting Flows

You can export your existing Flows and import Flows at any time. This utility can help to:

  • Share Flows with others, without giving them access to your Flows

  • Create a backup of your Flows

  • Bootstrap a new Flow based on a template

To export a Flow, click on the Export Flow button. The Flow will be exported with a .flow extension format.

To import a Flow, click on Import Flow:

Select the Flow you would like to import (.flow file):

You will have an opportunity to review and adjust the Flow prior to saving.

Examples

The following examples can help you to automate and integrate with common security and enterprise tools. Each example includes a Flow which can be imported in to your own AttackForge to help you get started fast!

Create JIRA Issue

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

Action 1 - Create JIRA Issue

  • Method: POST

  • URL: https://<YOUR-JIRA>/rest/api/2/issue

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = jira_auth

  • Request Script:

let jiraProjectKey = '';
let afProjectId = '';

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    const project = data.vulnerability_projects[i];

    if (project.custom_fields) {
      for (let j = 0; j < project.custom_fields.length; j++) {
        const projectCustomField = project.custom_fields[j];

        if (projectCustomField.key === 'jira_project_key') {
          jiraProjectKey = projectCustomField.value;
          break;
        }
      }
    }

    if (project.id) {
      afProjectId = project.id;
    }

    if (afProjectId && jiraProjectKey) {
      break;
    }
  }
}

jiraProjectKey = String.replace(jiraProjectKey, m/"/g, '\"');
jiraProjectKey = String.replace(jiraProjectKey, m/\\"/g, '\"');

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody(jiraProjectKey)
  }
};

function buildRequestBody(jiraProjectKey) {
  let summary = '';

  if (data.vulnerability_title) {
    summary = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vulnerability_title + ' \n\n ';
  }

  if (data.vulnerability_description) {
    description += '*Description* \n ' + data.vulnerability_description + ' \n\n ';
  }

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data.vulnerability_priority && data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vulnerability_priority + ' \n\n ';
    }
  }

  if (data.vulnerability_attack_scenario && data.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vulnerability_attack_scenario + ' \n\n ';
  }

  if (data.vulnerability_affected_asset_name) {
    description += '*Affected Asset* \n\n * ' + data.vulnerability_affected_asset_name + ' \n\n ';
  }
  else if (data.vulnerability_affected_assets) {
    description += '*Affected Asset(s)*: \n';

    for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
      if (data.vulnerability_affected_assets[i].asset?.name) {
        description += '\n * ' + data.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data.vulnerability_steps_to_reproduce) {
    description += '*Steps to Reproduce* \n\n ' + data.vulnerability_steps_to_reproduce + ' \n\n ';
  }

  let notes;

  if (data.vulnerability_notes) {
    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (notes === undefined) {
        notes = data.vulnerability_notes[i].note;
      }
      else {
        notes += '\n\n' + data.vulnerability_notes[i].note;
      }
    }
  }

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  Logger.debug(JSON.stringify(tags));

  if (tags.length > 0) {
    description = description + '*Tags* \n\n ';
    
    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }    
  }

  description = String.replace(description, m/"/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      project: {
          key: jiraProjectKey
      },
      summary: summary,
      description: description,
      priority: {
          name: priority
      },
      issuetype: {
          name: 'Bug'
      },
      labels: labels
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.key) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing JIRA Issue Key (body.key)',
    }, 
  };
}
else if (!data?.af_project_id) {
  if (data) {
    Logger.error(JSON.stringify(data));
  }  

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  if (data) {
    Logger.error(JSON.stringify(data));
  } 

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data.af_vuln_id,
      jira_issue_key: body.key
    } 
  };
}

Action 2 - Update AF Vuln with JIRA Issue Key

  • Method: PUT

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Secret; Value = af_auth

  • Request Script:

if (data?.jira_issue_key && data?.af_vuln_id && data?.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.com/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
          project_id: data.af_project_id,
          custom_fields: [
            {
              key: 'jira_issue_key',
              value: data.jira_issue_key
            }
          ]
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'missing required fields'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}

Update JIRA Issue

Initial Set Up

  • Event: Vulnerability Updated

  • Secrets:

Action 1 - Get JIRA Issue

  • Method: GET

  • URL: <defined in Request Script>

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = jira_auth

  • Request Script:

if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted.',
      }
  };
}

let jiraIssueKey;

if (data.vulnerability_custom_fields) {
  for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
    if (data.vulnerability_custom_fields[i].key === 'jira_issue_key') {
      jiraIssueKey = data.vulnerability_custom_fields[i].value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'no JIRA Issue Key found',
      }
  };
}

if (jiraIssueKey) {
  return {
    data: {
      vuln: data
    },
    request: {
      url: 'https://attackforge.atlassian.net/rest/api/3/issue/' + jiraIssueKey,
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.key) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'finish',
      message: 'missing JIRA Issue Key (body.key) - Issue likely deleted',
    }
  };
}
else if (!data?.vuln) {
  if (data) {
    Logger.error(JSON.stringify(data));
  }

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.vuln',
    },
  };
}
else {
  return {
    data: {
      vuln: data?.vuln,
      jira_issue_key: body?.key
    } 
  };
}

Action 2 - Update JIRA Issue

  • Method: <defined in Request Script>

  • URL: <defined in Request Script>

  • Headers:

    • <defined in Request Script>

  • Request Script:

if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'finish',
      message: 'no JIRA Issue Key found',
    }
  };
}
else if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'missing data.vuln',
    }
  };
}
else {
  let url = 'https://attackforge.atlassian.net/rest/api/2/issue/' + data.jira_issue_key;

  return {
    request: {
      url: url,
      body: buildRequestBody(),
    }
  };
}

function buildRequestBody() {
  let summary = '';

  if (data.vuln.vulnerability_title) {
    summary = '[SECURITY][VULNERABILITY] ' + data.vuln.vulnerability_title;
  }

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data?.vuln?.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vuln.vulnerability_title + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_description) {
    description += '*Description* \n ' + data.vuln.vulnerability_description + ' \n\n ';
  }

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data?.vuln?.vulnerability_priority && data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      const tag = data.vuln.vulnerability_tags[i];

      if (tag =~ m/CVSSv3.1 Base Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Base Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Temporal Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Temporal Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Environmental Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Environmental Score: ', '');
      }

      if (tag =~ m/CVSS: 3.1\//) {
        cvssVector = String.replace(tag, 'CVSS:3.1/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vuln.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vuln.vulnerability_priority + ' \n\n ';
    }
  }

  if (data?.vuln?.vulnerability_attack_scenario && data?.vuln?.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vuln.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vuln.vulnerability_attack_scenario + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_affected_asset_name) {
    description += '*Affected Asset* \n\n * ' + data.vuln.vulnerability_affected_asset_name + ' \n\n ';
  }
  else if (data?.vuln?.vulnerability_affected_assets) {
    description += '*Affected Asset(s)*: \n';

    for (let i = 0; i < data.vuln.vulnerability_affected_assets.length; i++) {
      if (data.vuln.vulnerability_affected_assets[i].asset?.name) {
        description += '\n * ' + data.vuln.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data?.vuln?.vulnerability_steps_to_reproduce) {
    description += '*Steps to Reproduce* \n\n ' + data.vuln.vulnerability_steps_to_reproduce + ' \n\n ';
  }

  let notes;

  if (data?.vuln?.vulnerability_notes) {
    for (let i = 0; i < data.vuln.vulnerability_notes.length; i++) {
      if (notes === undefined) {
        notes = data.vuln.vulnerability_notes[i].note;
      }
      else {
        notes += '\n\n' + data.vuln.vulnerability_notes[i].note;
      }
    }
  }

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vuln.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vuln.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vuln.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  if (tags.length > 0) {
    description += '*Tags* \n\n ';

    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }
  }

  description = String.replace(description, m/ "/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data?.vuln?.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data?.vuln?.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data?.vuln?.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data?.vuln?.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data?.vuln?.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      summary: summary,
      description: description,
      priority: {
        name: priority
      },
      issuetype: {
        name: 'Bug'
      },
      labels: labels
    }
  };
}
  • Response Script:

if (response.statusCode === 204) {
  return {
    decision: 'finish'
  };
}
else {
  Logger.info(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'JIRA Issue update failed',
    },
  };
}

Create ServiceNow Incident

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

Action 1 - Create SNOW Incident

  • Method: POST

  • URL: https://<YOUR-SNOW>/api/now/table/incident

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = snow_auth

  • Request Script:

let afProjectId;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id) {
      afProjectId = data.vulnerability_projects[i].id;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is undefined'
    }
  };
}

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  const body = {};

  if (data.vulnerability_title) {
    body.short_description = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  let priority = 3;

  if (data.vulnerability_priority === 'Critical') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 2;
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 3;
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 3;
  }

  body.impact = priority;
  body.urgency = priority;
  body.priority = priority;
  body.severity = priority;

  let description = '';

  if (data.vulnerability_affected_asset_name) {
    description += 'Affected Asset: ' + data.vulnerability_affected_asset_name + '\n\n';
  }
  else if (data.vulnerability_affected_assets) {
    description += 'Affected Asset(s): ';

    for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
      if (data.vulnerability_affected_assets[i].asset?.name) {
        description += '\n* ' + data.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data.vulnerability_description) {
    description += 'Description: \n' + data.vulnerability_description + '\n\n';
  }

  if (data.vulnerability_likelihood_of_exploitation) {
    description += 'Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '\n\n';
  }

  let cvssScore;
  let cvssVector;

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
      }
    }
  }

  if (cvssScore && cvssVector) {
    description += 'CVSS Score: ' + cvssScore + '\nCVSS Vector: ' + cvssVector + '\n\n';
  }

  if (data.vulnerability_attack_scenario) {
    description += 'Attack Scenario (Technical Risk): \n' + data.vulnerability_attack_scenario + '\n\n';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += 'Remediation Recommendation: \n' + data.vulnerability_remediation_recommendation + '\n\n';
  }

  if (data.vulnerability_steps_to_reproduce) {
    description += 'Steps to Reproduce: \n' + data.vulnerability_steps_to_reproduce + '\n\n';
  }

  if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
    description += 'Notes:';

    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (data.vulnerability_notes[i].note) {
        description += '\n' + data.vulnerability_notes[i].note;
      }
    }
  }

  if (data.vulnerability_tags && data.vulnerability_tags.length > 0) {
    description += 'Tags:';

    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      description += '\n* ' + data.vulnerability_tags[i];
    }
  }

  body.description = description;

  return body;
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.result?.sys_id) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing SNOW Incident SysId (body.result.sys_id)',
    },
  };
}
else if (!data?.af_project_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data?.af_vuln_id,
      snow_incident_number: body.result.number
    } 
  };
}

Action 2 - Update AF Vuln with SNOW Incident Id

  • Method: PUT

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Secret; Value = af_auth

  • Request Script:

if (data.snow_incident_number && data.af_vuln_id && data.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
        project_id: data.af_project_id,
        custom_fields: [
          {
            key: 'snow_incident_number',
            value: data.snow_incident_number
          }
        ]
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'missing required fields'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}

Create Azure DevOps Work Item

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

Action 1 - Create ADO Work Item

  • Method: POST

  • URL: https://dev.azure.com/<YOUR-ADO-TENANT>/<YOUR-ADO-PROJECT>/_apis/wit/workitems/$Issue?api-version=6.1-preview.3

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json-patch+json

    • Key = Authorization; Type = Secret; Value = ado_auth

  • Request Script:

let afProjectId;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id) {
      afProjectId = data.vulnerability_projects[i].id;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is undefined',
    },
  };
}

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  const fields = [];

  let title = '';

  if (data.vulnerability_title) {
    title = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  Array.push(fields, {
    from: null,
    op: 'add',
    path: '/fields/System.Title',
    value: title
  });

  let priority = 4;

  if (data.vulnerability_priority === 'Critical') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 2;
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 3;
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 4;
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 4;
  }

  Array.push(fields, {
    from: null,
    op: 'add',
    path: '/fields/Microsoft.VSTS.Common.Priority',
    value: priority
  });

  if (data.vulnerability_priority) {
    Array.push(fields, {
      from: null,
      op: 'add',
      path: '/fields/System.Tags',
      value: data.vulnerability_priority + ', Security Vulnerability'
    });
  }

  let description = '';

  if (data.vulnerability_title) {
    description += '<h1>[SECURITY][VULNERABILITY] ' + data.vulnerability_title + '</h1><br/>';
  }

  if (data.vulnerability_description) {
    const sanitizedDescription = String.replace(data.vulnerability_description, m/\r\n|\n|\r/gi, '<br>');
    description += '<h2>Description</h2><p>' + sanitizedDescription + '</p>';
  }

  if (data.vulnerability_affected_asset_name) {
    description += '<h2>Affected Asset</h2><ul><li>' + data.vulnerability_affected_asset_name + '</li></ul>';
  }
  else if (data.vulnerability_affected_assets) {
      description += '<h2>Affected Asset(s)</h2><ul>';

      for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
        if (data.vulnerability_affected_assets[i].asset?.name) {
          description += '<li>' + data.vulnerability_affected_assets[i].asset.name + '</li>';
        }
      }

      description += '</ul>';
  }

  let cvssScore;
  let cvssVector;

  if (data.vulnerability_priority && data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
      }
    }

    if (cvssScore && cvssVector) {
      description +=  
        "<h2>Technical Severity</h2>" +
        "<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
          "<thead>" +
            "<td><b>Rating</b></td>" +
            "<td><b>CVSSv3.1 Score</b></td>" +
          "</thead>" +
          "<tbody>" +
            "<td>" + data.vulnerability_priority + "</td>" +
            "<td>" + cvssScore + "</td>" +
          "</tbody>" +
        "</table>" +
        "<p>CVSS 3.1 Vector String: <a href='https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=" + cvssVector + "'>" + cvssVector + "</a></p>";
    }
    else {
      description += 
        "<h2>Technical Severity</h2>" +
        "<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
          "<thead>" +
            "<td><b>Rating</b></td>" +
          "</thead>" +
          "<tbody>" +
            "<td>" + data.vulnerability_priority + "</td>" +
          "</tbody>" +
        "</table>";
    }
  }

  if (data.vulnerability_likelihood_of_exploitation && data.vulnerability_attack_scenario) {
    const sanitizedAttackScenario = String.replace(data.vulnerability_attack_scenario, m/\r\n|\n|\r/gi, '<br>');

    description += 
      "<h2>Attack Scenario (Technical Risk)</h2>" +
      "<p>Likelihood of Exploitation: " + data.vulnerability_likelihood_of_exploitation + "/10</p>" +
      "<p>" + sanitizedAttackScenario + "</p>";
  }

  if (data.vulnerability_remediation_recommendation) {
    const sanitizedRemediationRecommendation = String.replace(data.vulnerability_remediation_recommendation, m/\r\n|\n|\r/gi, '<br>');
    description += "<h2>Recommendations</h2><p>" + sanitizedRemediationRecommendation + "</p>";
  }

  if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
    description += "<h2>Notes</h2>";

    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (data.vulnerability_notes[i].note) {
        const sanitizedNote = String.replace(data.vulnerability_notes[i].note, m/\r\n|\n|\r/gi, '<br>');
        description += '<p>' + sanitizedNote + '</p>';
      }
    }
  }

  if (data.vulnerability_steps_to_reproduce) {
    const sanitizedPOC = String.replace(data.vulnerability_steps_to_reproduce, m/\r\n|\n|\r/gi, '<br>');
    description += "<h2>Steps to Reproduce</h2><p>" + sanitizedPOC + "</p>";
  }

  if (data.vulnerability_tags) {
    description += "<h2>Tags</h2><ul>";

    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      description += '<li>' + data.vulnerability_tags[i] + '</li>';
    }

    description += '</ul>';
  }

  Array.push(fields, {
      from: null,
      op: 'add',
      path: "/fields/System.Description",
      value: description
  });

  return fields;
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8; api-version=6.1-preview.3') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8; api-version=6.1-preview.3'
    }
  };
}

if (!body?.id) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing ADO Work Item Id (body.id)',
    },
  };
}
else if (!data?.af_project_id) {
  Logger.error(JSON.stringify(data ?? {}));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    }, 
  };
}
else if (!data?.af_vuln_id) {
  Logger.error(JSON.stringify(data ?? {}));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  Logger.debug('normal result');

  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data?.af_vuln_id,
      ado_work_item_id: body.id
    } 
  };
}

Action 2 - Update AF Vuln with ADO Work Item Id

  • Method: PUT

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Secret; Value = af_auth

  • Request Script:

if (data.ado_work_item_id && data.af_vuln_id && data.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
        project_id: data.af_project_id,
        custom_fields: [
          {
            key: 'ado_work_item_id',
            value: JSON.stringify(data.ado_work_item_id)
          }
        ]
      }
    }
  };
}
else {
    return {
        decision: { 
            status: 'abort',
            message: 'missing required fields'
        }
    };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}

Prioritize Vulnerability with Threat Intelligence from VulnDB

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Vulnerability Created

  • Secrets:

    • vulndb_client_id - your VulnDB Client Id

    • vulndb_client_secret - your VulnDB Client Secret

Action 1 - Get VulnDB OAuth Token

  • Method: POST

  • URL: https://vulndb.flashpoint.io/oauth/token

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

  • Request Script:

let cve;

if (data.vulnerability_custom_fields) {
    for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
      if (data.vulnerability_custom_fields[i].key === 'cve') {
        cve = data.vulnerability_custom_fields[i].value;
        break;
      }
    }
}

if (!cve) {
  return {
    decision: { 
      status: 'finish',
      message: 'No CVE found'
    }
  };
}

return {
  data: {
    cve: cve,
    vuln: data
  },
  request: {
    body: {
      client_id: secrets.vulndb_client_id,
      client_secret: secrets.vulndb_client_secret,
      grant_type: 'client_credentials'
    }
  }
};
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (body?.access_token) {
  return {
    data: {
      vulnDBToken: body.access_token,
      cve: data?.cve,
      vuln: data?.vuln
    } 
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      cause: 'VulnDB access token not found'
    }
  };
}

Action 2 - Get Threat Intel for Vuln from VulnDB

  • Method: <defined in Request Script>

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • <others defined in Request Script>

  • Request Script:

if (data?.vulnDBToken && data?.cve && data?.vuln) {
  return {
    data: {
      cve: data.cve,
      vuln: data.vuln
    },
    request: { 
      url: 'https://vulndb.flashpoint.io/api/v2/vulnerabilities/' + data.cve + '/find_by_cve_id',
      headers: {
        Authorization: 'Bearer ' + data.vulnDBToken
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'VulnDB access token not found'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (!body?.results?[0]) {
  return {
    decision: { 
      status: 'abort',
      message: 'VulnDB CVE not found'
    }
  };
}

return {
  data: {
    cve: data?.cve,
    vulnDB: body.results[0],
    vuln: data?.vuln
  } 
};

Action 3 - Apply Threat Intel, Prioritize Vulnerability and Update Vulnerability

  • Method: <defined in Request Script>

  • URL: <defined in Request Script>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-KEY; Type = Secret; Value = af_auth

  • Request Script:

if (data?.vuln && data?.vulnDB) {
  const score = calculateScore(data.vuln, data.vulnDB);
  const priority = convertScoreToPriority(score);

  return {
      request: {
        url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.vuln.vulnerability_id,
        body: {
          priority: priority,
          custom_fields: [
            {
              key: 'threat_score',
              value: JSON.stringify(score)
            }
          ]
        }
      }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Missing required vuln data'
    }
  };
}

function calculateScore(vuln, vdb) {
  let score = 0;

  // Maximum score = 140
  // Higher the score = Higher Priority to fix!

  // [ THREAT CONTEXT ]

  const internetFacing = isInternetFacing(vuln);

  // Is the threat related to exposed/internet-facing services?
  if (internetFacing) {
    score += 10;
  }

  if (vdb.cvss_version_three_metrics) {
    for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
      const metrics = vdb.cvss_version_three_metrics[i];

      // Is the CVSS score 8 or higher?
      if (metrics.score !== undefined && metrics.score >= 8) {
        score += 5;
      }

      // Is the threat easily exploitable?
      if (metrics.attack_complexity === 'HIGH') {
        score += 1;
      }
      else if (metrics.attack_complexity === 'MEDIUM') {
        score += 5;
      }
      else if (metrics.attack_complexity === 'LOW') {
        score += 10;
      }

      // No user interaction required?
      if (metrics.user_interaction === 'NONE') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
    }
  }
  else if (vdb.cvss_metrics) {
    for (let i = 0; i < vdb.cvss_metrics.length; i++) {
      const metrics = vdb.cvss_metrics[i];

      // Is the CVSS score 8 or higher?
      if (metrics.score !== undefined && metrics.score >= 8) {
        score += 5;
      }

      // Is the threat easily exploitable?
      if (metrics.attack_complexity === 'HIGH') {
        score += 1;
      }
      else if (metrics.attack_complexity === 'MEDIUM') {
        score +=  5;
      }
      else if (metrics.attack_complexity === 'LOW') {
        score += 10;
      }
    }
  }

  if (vdb.classifications) {
    for (let i = 0; i < vdb.classifications.length; i++) {
      const classifications = vdb.classifications[i];

      // Is there a public exploit available for this threat?
      if (classifications.name === 'exploit_public') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // Does this threat require configuration changes?
      else if (classifications.name === 'solution_workaround') {
        score += 5;
      }
      // Does the threat grant unauthorized access?
      else if (classifications.name === 'location_remote') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // Disclosure in the wild?
      else if (classifications.name === 'disclosure_in_wild') {
        score += 5;
      }
      else if (classifications.name === 'disclosure_uncoordinated_disclosure') {
        score += 5;
      }
      // Wormable?
      else if (classifications.name === 'exploit_wormified') {
        if (isInternetFacing) {
          score += 15;
        }
        else {
          score += 10;
        }
      }
      // Virus / Malware?
      else if (classifications.name === 'exploit_virus_malware') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // PoC Public?
      else if (classifications.name === 'exploit_poc_public') {
        if (isInternetFacing) {
          score += 15;
        }
        else {
          score += 10;
        }
      }
    }
  }

  // Is a patch available for this threat?
  let patchExists = false;

  if (vdb.classifications) {
    for (let i = 0; i < vdb.classifications.length; i++) {
      const classifications = vdb.classifications[i];

      if (classifications.name === 'solution' || classifications.name === 'solution_upgrade') {
        patchExists = true;
        break;
      }
    }
  }
  else if (vdb.cvss_version_three_metrics) {
    for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
      const metrics = vdb.cvss_version_three_metrics[i];

      if (metrics.remediation_level === 'OFFICIAL_FIX' || metrics.remediation_level === 'TEMPORARY_FIX') {
        patchExists = true;
        break;
      }
    }
  }

  if (!patchExists) {
    score += 10;
  }

  // Social Risk Score (Vuln DB)
  if (vdb.social_risk_score === 'High') {
    score += 5;
  }
  else if (vdb.social_risk_score === 'Medium') {
    score += 3;
  }
  else if (vdb.social_risk_score === 'Low') {
    score += 1;
  }

  // What is the likelihood that this threat could be used in a ransomware attack?
  if (vdb.ransomware_likelihood === 'High') {
    if (isInternetFacing) {
      score += score + 10;
    }
    else {
      score += 5;
    }
  }
  else if (vdb.ransomware_likelihood === 'Medium') {
    if (isInternetFacing) {
      score += 5;
    }
    else {
      score += 3;
    }
  }
  else if (vdb.ransomware_likelihood === 'Low') {
    score += 1;
  }

  if (vdb.tags) {
    for (let i = 0; i < vdb.tags.length; i++) {
      // Is this a known CISA KEV vulnerability?
      if (vdb.tags[i] === 'cisa_kev') {
        score += 10;
        break;
      }
    }
  }

  return score;
}

function convertScoreToPriority(score) {
  if (score > 0 && score <= 30) {
    return 'Low';
  }
  else if (score > 30 && score <= 70) {
    return 'Medium';
  }
  else if (score > 70 && score <= 110) {
    return 'High';
  }
  else if (score > 110 && score <= 140) {
    return 'Critical';
  }
  else {
    return 'Info';
  }
}

function isInternetFacing(vuln) {
  if (vuln.vulnerability_affected_assets) {
    for (let i = 0; i < vuln.vulnerability_affected_assets.length; i++) {
      if (vuln.vulnerability_affected_assets[i].asset?.custom_fields) {
        const assetCustomFields = vuln.vulnerability_affected_assets[i].asset?.custom_fields;

        for (let j = 0; j < assetCustomFields.length; j++) {
          if (assetCustomFields[j].key === 'internet_facing' && assetCustomFields[j].value === 'Yes') {
            return true;
          }
        }
      }
    }
  }

  return false;
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'Vulnerability not updated'
    }
  };
}

Trigger an Automated Scan in Tenable

Initial Set Up

  • Event: Project Created

  • Secrets:

Action 1 - Schedule Vulnerability Scan in Tenable

  • Method: <insert>

  • URL: <insert>

  • Headers:

    • Key = Accept; Type = Value; Value = application/json

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = tenable_auth

  • Request Script:

<insert>
  • Response Script:

<insert>

Create Slack Message

Initial Set Up

  • Event: Vulnerability Created

  • Secrets:

Action 1 - Post Slack Message

  • Method: POST

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = slack_auth

  • Request Script:

return {
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  let afProjectId;
  let afProjectName;
  let channel;

  if (data.vulnerability_projects) {
    for (let i = 0; i < data.vulnerability_projects.length; i++) {
      if (data.vulnerability_projects[i].name) {
        afProjectName = data.vulnerability_projects[i].name;
      }

      if (data.vulnerability_projects[i].id) {
        afProjectId = data.vulnerability_projects[i].id;
      }

      if (data.vulnerability_projects[i].custom_fields) {
        for (let j = 0; j < data.vulnerability_projects[i].custom_fields.length; j++) {
          if (data.vulnerability_projects[i].custom_fields[j].key === 'slack_channel') {
            channel = data.vulnerability_projects[i].custom_fields[j].value;
            break;
          }
        }
      }
    }
  }

  let text = '';

  if (data.vulnerability_priority) {
    text += '[' + data.vulnerability_priority + '] ';
  }

  text += 'Vulnerability ';

  if (data.vulnerability_title) {
    text += '[' + data.vulnerability_title + '] ';
  }

  text += 'discovered in Project';

  if (afProjectName) {
    text += ' [' + afProjectName + ']';
  }

  if (afProjectId) {
    text += ' - <https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + '|View Vulnerability>';
  }

  return {
    channel: channel,
    text: text
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (body?.ok === true) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'SLACK message not posted',
    },
  };   
}

Create Teams Message

Initial Set Up

  • Event: Vulnerability Created

  • Secrets:

Action 1 - Post Teams Message

  • Method: POST

  • URL: <YOUR-INCOMING-WEBHOOK>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Accept; Type = Value; Value = */*

  • Request Script:

let afProjectId;
let afProjectName;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id && data.vulnerability_projects[i].name) {
      afProjectId = data.vulnerability_projects[i].id;
      afProjectName = data.vulnerability_projects[i].name;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is falsy'
    }
  };
}

if (!afProjectName) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectName is falsy'
    }
  };
}

let text = '';

if (data.vulnerability_priority) {
  text += '[' + data.vulnerability_priority + '] ';
}

text += 'Vulnerability ';

if (data.vulnerability_title) {
  text += '[' + data.vulnerability_title + '] ';
}

text += 
  'discovered in Project' + 
  ' [' + afProjectName + ']' + 
  ' - [View Vulnerability](https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + ')';


return {
  request: {
    url: 'https://prod-26.australiasoutheast.logic.azure.com:443/workflows/e25ebc22ccf5438190dc46a087450e5c/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=' + secrets.sig,
    body: {
      type: 'message',
      attachments: [
        {
          contentType: 'application/vnd.microsoft.card.adaptive',
          content: {
            $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
            type: 'AdaptiveCard',
            version: '1.2',
            body: [
              {
                type: 'TextBlock',
                text: text
              }
            ]
          }
        }
      ]
    }
  }
};
  • Response Script:

if (response?.statusCode === 202) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'TEAMS message not posted',
    },
  };   
}

Send Vulnerability to PowerBI

Initial Set Up

  • Event: Vulnerability Created

  • Secrets:

Action 1 - Send Vulnerability to PowerBI

  • Method: POST

  • URL: <YOUR-PUSH-URL>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

  • Request Script:

return {
  request: {
    url: 'https://api.powerbi.com/beta/544b6efc-1149-4f8c-a17a-cad3c50cd5f8/datasets/d5ff54b4-2be2-4e02-baa2-9c6aa20a6c55/rows?experience=power-bi&key=' + secrets.powerbi_key,
    body: [
      {
        vuln_title: data.vulnerability_title,
        vuln_priority: data.vulnerability_priority
      }
    ]
  }
};
  • Response Script:

if (response?.statusCode === 200) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'PowerBI data not posted',
    },
  };   
}

Create Salesforce Opportunity

Initial Set Up

  • Event: Project Requested

  • Secrets:

Action 1 - Get Salesforce OAuth Token

  • Method: POST

  • URL: https://login.salesforce.com/services/oauth2/token

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded

  • Request Script:

return {
  request: {
    body: 'grant_type=password&client_id=' + secrets.sf_client_id + '&client_secret=' + secrets.sf_client_secret + '&username=' + secrets.sf_username + '&password=' + secrets.sf_password
  }, 
  data: {
    project_request: data
  } 
};
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (body?.access_token) {
  return {
    data: {
      project_request: data.project_request,
      sf_access_token: body.access_token
    }  
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce access token not found'
    }
  };
}

Action 2 - Create Salesforce Opportunity

  • Method: POST

  • URL: https://<YOUR-SALESFORCE>/services/data/v63.0/sobjects/Opportunity

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

  • Request Script:

if (data?.project_request && data?.sf_access_token) {
  let closeDate = '2030-12-31';
  
  if (data.project_request.project_request_end_date) {
    closeDate = String.slice(data.project_request.project_request_end_date, 0, 10);
  }

  return {
    request: {
      headers: {
        'Authorization': 'Bearer ' + data.sf_access_token
      },
      body: {
        'Name': data.project_request.project_request_name,
        'StageName': 'Needs Analysis',
        'CloseDate': closeDate
      }
    }
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce access token not found'
    }
  };
}
  • Response Script:

let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (response.statusCode === 201 && body?.id && body.success === true) {
  return {
    decision: 'finish'
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce Opportunity not created'
    }
  };
}

Create a Webhook

Initial Set Up

  • Event: Vulnerability Created

  • Secrets: None

Action 1 - Send Data to Webhook

  • Method: POST

  • URL: <YOUR-WEBHOOK-ADDRESS>

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = Authorization; Type = Secret; Value = auth

  • Request Script:

// Not required for this webhook
  • Response Script:

// Not required for this webhook

Send Custom Email

The purpose of this example is to send an email to account managers when a Retest is requested.

Initial Set Up

Important: This example requires access to the AttackForge Self-Service API and AttackForge Flows

  • Event: Project Retest Requested

  • Secrets:

Action 1 - Send Email to Account Managers

  • Method: POST

  • URL: https://<YOUR-ATTACKFORGE>/api/ss/email

  • Headers:

    • Key = Content-Type; Type = Value; Value = application/json

    • Key = X-SSAPI-Key; Type = Value; Value = af_auth

  • Request Script:

return {
  request: { 
    body: {
      to: [
        "admin@attackforge.com",
        {
          "user_id": "67b45e6de6da84559d9860fc"
        },
      ],
      cc: [
        "support@attackforge.com"
      ],
      subject: "New Project Requested - Please Follow Up",
      text: data.project_request_name + ' just requested to start on ' + data.project_request_start_date,
      html: '<p>[<b>' + data.project_request_name + '</b>] just requested to start on [<b>' + data.project_request_start_date + '</b>]</p>'
    },
  },
};
  • Response Script:

const body = JSON.parse(response.body);

if (body?.status === 'Accepted') {
  return {
    decision: {  
      status: 'finish',
      message: 'Email sent!'
    },  
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Email not sent'
    }
  };
}

Integrate your vulnerability data with ticketing tools like , , , and others.

Visualize your pentesting data in powerful tools like and

Help make better risk decisions by sending your vulnerability data to GRC platforms like , , and .

Create workflow automations by chaining together

Trigger automated scanning activities in your security toolset like , and .

Create messages on collaboration platforms like and .

Prioritize vulnerabilities with threat-intelligence like

You must have ; or

- the trigger which initiates a .

- a sequence of steps which are executed in order during a .

- any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.

Triggered by an Event - A Run is initiated by a , like a new vulnerability or an update to a project, or a manual action.

Provides details - Each Run has details like start time, duration, and the specific taken within the Flow.

IMPORTANT: A normal Run will only be executed in the context of the and related . For example, if the was "vulnerability-created" - the Run will only initiate for the vulnerability for which the has access to the vulnerability. Test runs can be manually executed with test data at any time.

Edit the Flow, including changing Name, , and

View and individual Runs, including Re-run

the Flow

A Trigger is an action which initiates a . Triggers can be initiated from or manually initiated.

You can also view, manage and create secrets in the and in the :

Select the Secret directly in the

Refer to the Secret in the or

When creating or modifying within the , you can select 'Secret' for the header type. This will then allow you to select from an existing Secret, or create a new Secret.

When creating or modifying the or the , you can refer to secrets using the following syntax:

Actions are either one activity, or a sequence of activities, which are executed in order during a .

This involves formatting the vulnerability into the necessary format, and making a HTTPS request to the JIRA API to create the issue.

This involves making a HTTPS request to the to set the JIRA Issue Key custom field.

The primary purpose of an Action is to make an update to a system. The system could be AttackForge (via the ) or an external system.

Every Action is made up of a and a .

The is the HTTP request which is made by the Action.

The is the HTTP response from the server which received the HTTP request.

- this is the HTTP method i.e. GET, POST, PUT, etc. that will be used for the

- this is the URL that will be used for the .

- these are the headers that will be sent when the is made.

- this is the script which will execute before the is made.

- this is the script which will execute after the is returned.

Methods are the HTTP methods/verbs that will be used for the i.e. GET, POST, PUT, etc.

Methods can be selected when editing the :

Methods can also be programatically set in your in the :

The URL is the web address that will be used for the , for example https://acmecorp.atlassian.net/rest/api/2/issue

The URL can be entered in when editing the :

The URL can also be programatically set in your in the . This is useful if your URL has a dynamic component which needs to be computed:

The Headers are the HTTP headers that will be sent when the is made.

The Headers can be manually entered in when editing the :

The Headers can also be programatically set in your in the . This is useful if your Headers have a dynamic component which needs to be computed:

The Response is the HTTP server response to a .

The Request Script is the script which will execute before the is made.

The Response Script is the script which will execute after the is returned.

Flows support - a powerful interpreted programming language created by AttackForge.

You can take advantage of in to help you to debug and test your code.

Data is contextually relevent information for your and .

Where <KEY> is replaced with the associated key on the .

For more information on Data, please see .

is contextually relevant information for your and .

The Data Object is an that holds the .

The first in your Flow will contain in the which is relevent to your . For example, if your Flow was assigned to the "vulnerability-created" Event, then your Data Object will contain all of the information relating to the vulnerability.

However from this point forward, you can control how you would like your Data Object to look for the and any subsequent going forward.

If you needed to pass this information to the next step of this Flow, which using the example above will be the on the first - you can include the "data" key in your and pass in an with key/value pairs as follows:

Continuing with this example, the will now have the following Data Object:

When viewing the details of a - you can see what was passed as input and output into an

If you would need to log the Data Object for visibility or debugging during execution of a you can do the following:

You can then view the details in the

The Response Object is the HTTP information which is sent back from the server during the .

The Response Object is available in the .

The statusCode will be accessible as a .

The headers will be accessible as a .

The body will be accessible as a .

If you are expecting the body to be returned as a payload (which is common for ) - you must first parse the body into JSON format before you can access it using , see example below:

When viewing the details of a - you can see the and Response Headers and Response Body:

If you would need to log the Response Object for visibility or debugging during execution of a you can do the following:

You can then view the details in the

The Return Statement is the action to take for your and .

Continue will instruct your script to continue with normal execution. For example, continuing in the will result in the being made. Continuing in the will result in executing the next .

Next will instruct your or to move to the next . This is useful if a in your Flow is conditional i.e. it may or may not need to be made.

The Request is the HTTP Request information that your will use. Request is made up of the following components:

The URL is the URL that the will be sent to. This field is optional. If it is not specified, the set in the will prevail. If it is specified, it will take precedence over the set in the .

The Body is the payload body that will be sent in the . This field is optional. If it is not specified, no body will be sent in the .

The Headers are the HTTPS Headers that the will use. This field is optional. If it is not specified, the set in the will prevail. If it is specified, it will take precedence over the set in the .

The Method is the HTTPS Method that the will be sent to. This field is optional. If it is not specified, the set in the will prevail. If it is specified, it will take precedence over the set in the .

Data is an object that can be used to pass information between to , and from to the next .

The Data in the for the first of the Flow will be the Event information, for example "vulnerability-created" fields. From then onwards, you can override what the data will be in the and beyond.

Example with Data in the of the first in the Flow:

Example with Data in the of the first in the Flow. This example will pass on the "af_project_id" and "af_vuln" that was passed in the Data from the on to the next in the Flow.

Flow Runs is where you can view the history of each you have access to, across all of your Flows. It is a consolidated view for every . You can access this page from the Flows module.

You can also view for a specific Flow by clicking on Flows and then clicking on the name of a Flow.

When clicking on a , you will see an overview of the history for that , including the following information:

Run Status - Whether the was Completed or Failed.

Started - the timestamp of when the started execution.

Duration - the duration (in milliseconds) for execution of the until completion or failure.

Actions - the in-scope during the .

Data - the input and output of each .

HTTP - the , , , , Response Status Code and for each in the Flow.

Logs - the logs for each .

If you include in your or , you will be able to see the logs here during execution of a Flow.

You can manually run a Flow at any time. This is useful for testing your Flow. When you manually run a Flow, the input into the first will be test data. You can modify the test data to match your testing needs.

You can manually re-run a Flow at any time. When you Re-Run a Flow, it will execute with exactly the same input data into the first .

After a Flow has Re-Run, you will notice the will show that it was Re-Run.

IMPORTANT: Exported Flows do not contain the values of . However for compatibility and convenience, the key/name will be included in the export.

NOTE: These are just some common examples so far. We are constantly adding more Flow examples as we continue to roll out Flows. Remember - Flows can interact with any HTTP interface, including AttackForge and any other external systems! Unleash your imagination and creativity

The purpose of this example is to create a when a Vulnerability is created in AttackForge, and to update AttackForge to assign the JIRA Issue Key against the Vulnerability.

This example Flow can be downloaded from our and into your AttackForge.

af_auth - your .

jira_auth - your

The purpose of this example is to update a when a Vulnerability is updated in AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

jira_auth - your

The purpose of this example is to create a when a Vulnerability is created in AttackForge, and to update AttackForge to assign the SNOW Incident Id against the Vulnerability.

This example Flow can be downloaded from our and into your AttackForge.

af_auth - your .

snow_auth - your

The purpose of this example is to create a when a Vulnerability is created in AttackForge, and to update AttackForge to assign the Azure DevOps Work Item Id against the Vulnerability.

This example Flow can be downloaded from our and into your AttackForge.

af_auth - your .

ado_auth - your

The purpose of this example is to prioritize a vulnerability based on threat intelligence information harnessed from and to apply a custom score/rating to the vulnerability.

This example Flow can be downloaded from our and into your AttackForge.

af_auth - your .

The purpose of this example is to schedule a when a Project is created in AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

tenable_auth - your

The purpose of this example is to create a message in a when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

slack_auth - your

URL:

The purpose of this example is to create a message in a when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

teams_auth - your

The purpose of this example is to send data to when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

powerbi_key - your

The purpose of this example is to create a when a Project is requested in AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

sf_client_id - your

sf_client_secret - your

sf_password - Your

sf_username - Your

The purpose of this example is to post data to a when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

This example Flow can be downloaded from our and into your AttackForge.

af_auth - your .

Atlassian JIRA
ServiceNow
Azure DevOps
BMC Helix
Power BI
Tableau
RSA Archer
MetricStream
OneTrust
LogicGate
AttackForge Self-Service APIs
Rapid7
Tenable
Qualys
Slack
Teams
VulnDB
access to at least one (1) Event
JIRA Create Issue API
Update Vulnerability Self-Service API
Self-Service APIs
AFScript
Logging
AFScript
HTTP Response Status Code
HTTP Response Headers
HTTP Response Body
Number
Object
String
JSON
RESTful APIs
dot or bracket notation
😄
Self-Service API
JIRA Issue
AttackForge Self-Service API token
JIRA API token
JIRA Issue
JIRA API token
ServiceNow Incident
AttackForge Self-Service API token
SNOW API Key
Azure DevOps Work Item
AttackForge Self-Service API token
ADO Personal Access Token
FlashPoint VulnDB
AttackForge Self-Service API token
vulnerability scan in Tenable
Tenable API Key
Slack Channel
Slack Bot Token
https://slack.com/api/chat.postMessage
Teams Channel
Incoming Web Hook
PowerBI
Push Semantic Model - Push URL
Salesforce Opportunity
Salesforce OAuth Client Id
Salesforce OAuth Client Secret
Salesforce User Password
SalesForce Username
Webhook
AttackForge Self-Service API token
Event Trigger
Run
Actions
Run
Secrets
Trigger
Actions
Flow Owner
Event Trigger
Event Trigger
Flow Owner
Event Trigger
Actions
Secrets
Run history
Export
Events
Run
Request Script
Response Script
Headers
Request Script
Response Script
Headers
Action
Request Script
Response Script
Run
Request
Response
Request
Response
Method
Request
URL
Request
Headers
Request
Request Script
Request
Response Script
Response
Request
Action
Request Script
Return Statement
Request
Action
Request Script
Return Statement
Request
Action
Request Script
Return Statement
URL
Method
Headers
Request Script
Request
Response Object
Response Script
Request
Code
Data
Return Statement
Response
Code
Data
Return Statement
Request Script
Response Script
Data Object
Data Object
Data
Request Script
Response Script
Object
Data
Action
Data
Request Script
Trigger Event
Response Script
Actions
Object
Response Script
Action
Response Object
Response Script
Run
Data
Action.
Run
Run Logs
Response
Response Script
Status Code
Run
Response
Run
Run Logs
Request Script
Response Script
Decision
Request
Data
Request Script
Request
Response Script
Action
Request Script
Response Script
Action
Request
Request Script
Request
URL
Action
URL
Action
Request
Headers
Action
Headers
Action
Request
Method
Action
Method
Action
Request Script
Response Script
Response Script
Action
Request Script
Action
Response Script
Request Script
Action
Response Script
Action
Request Script
Action
Run
Run
Runs
Run
Run
Run
Run
Run
Actions
Run
Action
URL
Method
Headers
Request Body
Response Body
Action
Action
Logging
Request Script
Response Script
Action
Action
Event Trigger
Secrets
Secret
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
Flows GitHub Repository
imported
AFScript
Request
Request