Automated task processing with JIRA API. It’s no secret that task tracker is one of the most critical resources of any big organization. The whole working process can be represented as creating, processing and closing various tasks. Without task tracker there will be complete disaster, collapse and anarchy. So, it’s very important to work with this instrument efficiently. And when I write efficiently, I mean in automated way. It’s a blog about security automation after all. 😉
This post will be about Jira task tracker. I have already wrote how Jira can be used in VM Remediation process. That post was mainly about the main principles and how remediation tasks look in Jira WEB-GUI. Now, I will go further and show how to use it as a source of important information and easily deal with daily routine tasks using some trivial scripting. It is all possible because of advanced Jira Rest API.
Let’s say we have some regular tasks of some type. For example, to detect vulnerabilities on some hosts using Nessus and make a comment about founded vulnerabilities in the task. You can make a script that we will search for this kind of tasks in Jira, process them, add scan results to the comment and close the task. Of course it works the best when these tasks are also were created with in some automated way, in this case parsing will be much easier.
“Issue”, is the right name for the task in Jira; but I frequently use “issue”, “task” and “ticket” interchangeably. Sorry for this.
So, we need to take this steps:
- Authorization
- Search for existing Jira issues using some search request
- View description, data and comments of the issue
- Download files attached to the issue
- Make some task processing
- Add a new comment to the issue
- Change status of the issue
Atlassian JIRA is a very well documented products. I have used this articles about IRA REST API:
Authorization
For authorization you can use your own login and password. Just use “-u jira_login:jira_password
” in every curl request.
Searching for Jira issues
Jira supports advanced search request in a special JIRA Query Language (JQL). For example, to get tasks of some project and with some text in Summary field you can use this search request: “jql=project=SECSCAN AND Summary ~ \”Scan task 123\””
curl -u jira_login:jira_password -X GET --data-urlencode "jql=project=SECSCAN AND Summary ~ \"Scan task 123\"" -H "Content-Type: application/json" "https:///rest/api/2/search" | python -m json.tool
In response we receive a huge JSON structure:
{ "expand": "names,schema", "issues": [ { "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", "fields": { "aggregateprogress": { "progress": 0, "total": 0 }, "aggregatetimeestimate": null, "aggregatetimeoriginalestimate": null, "aggregatetimespent": null, "assignee": { "active": true, "avatarUrls": { "16x16": "https://[file_server]/avatar/47ce8937d79322e6a9b8c68e4798c0a9?d=mm&s=16", "24x24": "https://[file_server]/avatar/47ce8937d79322e6a9b8c68e4798c0a9?d=mm&s=24", "32x32": "https://[file_server]/avatar/47ce8937d79322e6a9b8c68e4798c0a9?d=mm&s=32", "48x48": "https://[file_server]/avatar/47ce8937d79322e6a9b8c68e4798c0a9?d=mm&s=48" }, "displayName": "Alexander Leonov", "emailAddress": "aleonov@corporation.com", "key": "aleonov@corporation.com", "name": "aleonov@corporation.com", "self": "https://[jira_server]/rest/api/2/user?username=aleonov%40corporation.com", "timeZone": "Europe/Moscow" }, "components": [], "created": "2017-01-01T08:00:04.000+0300", "creator": { "active": true, "avatarUrls": { "16x16": "https://[jira_server]/secure/useravatar?size=xsmall&ownerId=secbot&avatarId=12322", "24x24": "https://[jira_server]/secure/useravatar?size=small&ownerId=secbot&avatarId=12322", "32x32": "https://[jira_server]/secure/useravatar?size=medium&ownerId=secbot&avatarId=12322", "48x48": "https://[jira_server]/secure/useravatar?ownerId=secbot&avatarId=12322" }, "displayName": "sec bot", "emailAddress": "secbot@corporation.com", "key": "secbot", "name": "secbot", "self": "https://[jira_server]/rest/api/2/user?username=secbot", "timeZone": "Europe/Moscow" }, "customfield_10040": null, [...] "customfield_40606": null, "description": "Scan the hosts listed in attached file.", "duedate": null, "fixVersions": [], "issuelinks": [], "issuetype": { "avatarId": 12422, "description": "", "iconUrl": "https://[jira_server]/secure/viewavatar?size=xsmall&avatarId=12422&avatarType=issuetype", "id": "15322", "name": "Access check", "self": "https://[jira_server]/rest/api/2/issuetype/15322", "subtask": false }, "labels": [], "lastViewed": null, "priority": { "iconUrl": "https://[jira_server]/images/icons/priorities/major.svg", "id": "3", "name": "PRIORITY NAME", "self": "https://[jira_server]/rest/api/2/priority/3" }, "progress": { "progress": 0, "total": 0 }, "project": { "avatarUrls": { "16x16": "https://[jira_server]/secure/projectavatar?size=xsmall&pid=10123&avatarId=10005", "24x24": "https://[jira_server]/secure/projectavatar?size=small&pid=10123&avatarId=10005", "32x32": "https://[jira_server]/secure/projectavatar?size=medium&pid=10123&avatarId=10005", "48x48": "https://[jira_server]/secure/projectavatar?pid=10123&avatarId=10005" }, "id": "10123", "key": "SECSCAN", "name": "SECURITY SCAN", "self": "https://[jira_server]/rest/api/2/project/10123" }, "reporter": { "active": true, "avatarUrls": { "16x16": "https://[file_server]/avatar/e35cb040a2321352217b9d05af15e61?d=mm&s=16", "24x24": "https://[file_server]/avatar/e35cb040a2321352217b9d05af15e61?d=mm&s=24", "32x32": "https://[file_server]/avatar/e35cb040a2321352217b9d05af15e61?d=mm&s=32", "48x48": "https://[file_server]/avatar/e35cb040a2321352217b9d05af15e61?d=mm&s=48" }, "displayName": "sec bot", "emailAddress": "secbot@corporation.com", "key": "secbot", "name": "secbot", "self": "https://[jira_server]/rest/api/2/user?username=secbot", "timeZone": "Europe/Moscow" }, "resolution": null, "resolutiondate": null, "status": { "description": "Task is ready to start work", "iconUrl": "https://[jira_server]/images/icons/statuses/open.png", "id": "1", "name": "New", "self": "https://[jira_server]/rest/api/2/status/1", "statusCategory": { "colorName": "blue-gray", "id": 2, "key": "new", "name": "STATUS NAME", "self": "https://[jira_server]/rest/api/2/statuscategory/2" } }, "subtasks": [], "summary": "Scan task 123", "timeestimate": null, "timeoriginalestimate": null, "timespent": null, "updated": "2017-01-09T11:44:43.000+0300", "versions": [], "votes": { "hasVoted": false, "self": "https://[jira_server]/rest/api/2/issue/SECSCAN-1244/votes", "votes": 0 }, "watches": { "isWatching": false, "self": "https://[jira_server]/rest/api/2/issue/SECSCAN-1244/watchers", "watchCount": 1 }, "workratio": -1 }, "id": "1833537", "key": "SECSCAN-1244", "self": "https://[jira_server]/rest/api/2/issue/1833537" } ], "maxResults": 50, "startAt": 0, "total": 1 }
In the bottom of the JSON response you can see total amount of the issues in the search results. Most information on the issue is available here: summary, description, issue id (key), who created this issue and who is processing it right now. It may be a good practice to search in this JSON response if we really found what we needed, because sometimes Jira may return some unnecessary results as well. Jira search engine can be too much intelligent. 😉
Issue Details
The only things that you won’t find in search response are attached files and commentaries. To get them you should request the issue by it’s id https://[jira_server]/rest/api/2/issue/1833537 (see url in “self”) or the key https://[jira_server]/rest/api/2/issue/SECSCAN-1244
curl -u jira_login:jira_password -X GET -H "Content-Type: application/json" "https://[jira_server]/rest/api/2/issue/SECSCAN-1244"
I will show here only new fields of response:
{ "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", "fields": { "aggregateprogress": { "progress": 0, "total": 0 }, "aggregatetimeestimate": null, "aggregatetimeoriginalestimate": null, "aggregatetimespent": null, ... "attachment": [ { "author": { "active": true, "avatarUrls": { "16x16": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=16", "24x24": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=24", "32x32": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=32", "48x48": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=48" }, "displayName": "John Smith", "emailAddress": "j.smith@corporation.com", "key": "j.smith@corporation.com", "name": "j.smith@corporation.com", "self": "https://[jira_server]/rest/api/2/user?username=j.smith%40corporation.com", "timeZone": "Europe/Moscow" }, "content": "https://[jira_server]/secure/attachment/701925/Hosts.xlsx", "created": "2017-01-09T11:44:26.000+0300", "filename": "Hosts.xlsx", "id": "701925", "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "self": "https://[jira_server]/rest/api/2/attachment/701925", "size": 14381 }, ... ], "comment": { "comments": [ { "author": { "active": true, "avatarUrls": { "16x16": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=16", "24x24": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=24", "32x32": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=32", "48x48": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=48" }, "displayName": "John Smith", "emailAddress": "j.smith@corporation.com", "key": "j.smith@corporation.com", "name": "j.smith@corporation.com", "self": "https://[jira_server]/rest/api/2/user?username=j.smith%40corporation.com", "timeZone": "Europe/Moscow" }, "body": "A very interesting comment", "created": "2017-01-10T12:39:46.000+0300", "id": "701925", "self": "https://[jira_server]/rest/api/2/issue/1833537/comment/4192745", "updateAuthor": { "active": true, "avatarUrls": { "16x16": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=16", "24x24": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=24", "32x32": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=32", "48x48": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=48" }, "displayName": "John Smith", "emailAddress": "j.smith@corporation.com", "key": "j.smith@corporation.com", "name": "j.smith@corporation.com", "self": "https://[jira_server]/rest/api/2/user?username=j.smith%40corporation.com", "timeZone": "Europe/Moscow" }, "updated": "2017-01-10T12:39:46.000+0300" } ], "maxResults": 1, "startAt": 0, "total": 1 }, ... "id": "1833537", "key": "SECSCAN-1244", "self": "https://[jira_server]/rest/api/2/issue/1833537" }
Now you can download the attachments using url in “content”: https://[jira_server]/secure/attachment/701925/Hosts.xlsx
curl -u jira_login:jira_password -X GET -H "Content-Type: application/json" https://[jira_server]/secure/attachment/701925/Hosts.xlsx -o "Hosts.xlsx"
Processing the task
You convert xlsx to parsable csv using ssconvert tool (part of gnumeric):
ssconvert "Hosts.xlsx" "Hosts.csv"
If you really want to know how perform vulnerability scans of your hosts automatically, check the post “Nessus API for hosts scanning“. You can also check similar stuff for Qualys, Rapid7 Nexpose and FSecure.
Adding a new comment
Now suppose we have scan results in the file ScanResults.txt and we want to add text from this file as a comment to the issue. We need to created ScanResults.json with structure: '{ "body": "Our comment" }'
This bash code can create such file for you. It's very ugly, but it works:
rm ScanResults.json
echo -n "{ \"body\": \"" >> ScanResults.json
cat ScanResults.txt | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g'>> ScanResults.json
echo "\" }" >> ScanResults.json
curl -u jira_login:jira_password -X POST --data @ScanResults.json -H "Content-Type: application/json" "https://[jira_server]/rest/api/2/issue/SECSCAN-1244/comment"
If you request the SECSCAN-1244 issue once again, you will see something new in the “comment” section:
curl -u jira_login:jira_password -X GET -H "Content-Type: application/json" "https://[jira_server]/rest/api/2/issue/SECSCAN-1244"
... "comment": { "comments": [ { "author": { "active": true, "avatarUrls": { "16x16": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=16", "24x24": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=24", "32x32": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=32", "48x48": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=48" }, "displayName": "John Smith", "emailAddress": "j.smith@corporation.com", "key": "j.smith@corporation.com", "name": "j.smith@corporation.com", "self": "https://[jira_server]/rest/api/2/user?username=j.smith%40corporation.com", "timeZone": "Europe/Moscow" }, "body": "Scan result comment", "created": "2017-01-11T15:56:01.000+0300", "id": "4201223", "self": "https://[jira_server]/rest/api/2/issue/1826537/comment/4201023", "updateAuthor": { "active": true, "avatarUrls": { "16x16": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=16", "24x24": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=24", "32x32": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=32", "48x48": "https://[file_server]/avatar/d2216716462b11cc50ef22d2e3e32536?d=mm&s=48" }, "displayName": "John Smith", "emailAddress": "j.smith@corporation.com", "key": "j.smith@corporation.com", "name": "j.smith@corporation.com", "self": "https://[jira_server]/rest/api/2/user?username=j.smith%40corporation.com", "timeZone": "Europe/Moscow" }, "updated": "2017-01-12T17:22:22.000+0300" }, ...
Ok, let’s say tha we need to delete this comment. Use this request for it:
curl -u jira_login:jira_password -X DELETE -H "X-Atlassian-Token: no-check" https://[jira_server]/rest/api/2/issue/1826537/comment/4201023
Closing the issue
Ok, now we have fully processed the issue and ready to close it. First of all, you need to get available statuses:
curl -u jira_login:jira_password -X GET -H "Content-Type: application/json" https://[jira_server]/rest/api/2/issue/SECSCAN-1244/transitions
Here is one of them:
{ "id": "102", "name": "Closed", "to": { "description": "", "iconUrl": "https://[file_server]/images/icons/statuses/closed.png", "id": "10004", "name": "Closed", "self": "https://[jira_server]/rest/api/2/status/10005", "statusCategory": { "colorName": "green", "id": 3, "key": "done", "name": "\u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e", "self": "https://[jira_server]/rest/api/2/statuscategory/3" } } }
So, we now the transition id for “Closed” status and we can send a request to close the issue:
curl -D- -u jira_login:jira_password -X POST --data "{ \"transition\": { \"id\":\"102\" } }" -H "Content-Type: application/json" https://[jira_server]/rest/api/2/issue/SECSCAN-1244/transitions
As you can see, everything is very easy and intuitive. In the next post about Jira I am going to write about automated creating and managing the Jira issues, for example, for IT vulnerability remediation tasks.
Upd. There is no example of creating an issue. You can see it here “Creating an issue using a project key and field names”.
Hi! My name is Alexander and I am a Vulnerability Management specialist. You can read more about me here. Currently, the best way to follow me is my Telegram channel @avleonovcom. I update it more often than this site. If you haven’t used Telegram yet, give it a try. It’s great. You can discuss my posts or ask questions at @avleonovchat.
А всех русскоязычных я приглашаю в ещё один телеграмм канал @avleonovrus, первым делом теперь пишу туда.
Great article and tips.
I had hope to see Python code to copy and paste for myself :D.
Also its not great idea to use username/password in Curl, somebody can use it in its own environment (or will commit his/her scripts to github :D)
Maybe there are some others ways to auth to Jira (keys, api keys, so on). Will check later for myself
Hi Nikolay,
Thanks for comment! JIRA API also supports Cookie-based Authentication and OAuth authentication, but I haven’t try these options yet.
You can convert this curl request to python using automated translators, like http://curl.trillworks.com/
Awesome sharing! Thanks Alexander
Pingback: Vulnerability Management for Network Perimeter | Alexander V. Leonov
Pingback: Atlassian Jira, Python and automated labeling | Alexander V. Leonov
Pingback: Confluence REST API for reading and updating wiki pages | Alexander V. Leonov
Pingback: Managing JIRA Scrum Sprints using API | Alexander V. Leonov
Hello,
Thanks for the information, when iM trying its giving below error:
curl -u $uname:$token -X POST –data @/tmp/ScanResults.json -H “Content-Type: application/json” https://abc.atlassian.net/rest/api/3/issue/abc-1234/comment
{“errorMessages”:[],”errors”:{“comment”:”Comment body is not valid!”}}
cat ScanResults.json
{ “body”: “Testing…
” }
Great job on this! Finally I can close a ticket via The API! I found a ton of other content that was wrong before this page.