# Azure DevOps (ADO)

## Create Azure DevOps Work Item

{% embed url="<https://youtu.be/qnlhLAT41f4?si=ZsmAenXI8VwElo8u>" %}

The purpose of this example is to create a [Azure DevOps Work Item](https://learn.microsoft.com/en-us/azure/devops/boards/work-items/about-work-items?view=azure-devops\&tabs=agile-process) 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 [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

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

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_auth - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).
  * ado\_auth - your [ADO Personal Access Token](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\&tabs=Windows)

**Action 1 - Create ADO Work Item**&#x20;

* **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**:

```javascript
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**:

```javascript
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**:

```javascript
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**:

```javascript
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'
    },
  };
}
```
