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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://support.attackforge.com/attackforge-enterprise/modules/flows/azure-devops-ado.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
