chore: reorganize docs, document migrations, gitignore operational files for v1.0.0 release
This commit is contained in:
1316
docs/api/atlasinfosec-api-spec.json
Normal file
1316
docs/api/atlasinfosec-api-spec.json
Normal file
File diff suppressed because it is too large
Load Diff
155
docs/api/ivanti-api-python-wrapper.py
Normal file
155
docs/api/ivanti-api-python-wrapper.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# Ivanti API class/wrapper | Evan Compton (P2886385), updated 11/13/2025
|
||||
|
||||
### ! README | IMPORTANT INFORMATION ! ###
|
||||
# requires an "Ivanti_config.ini" file in the same directory
|
||||
# edit "Ivanti_config_template.ini", then save as "Ivanti_config.ini"
|
||||
|
||||
### ? CODE PURPOSE ? ###
|
||||
# the primary purpose of this class/wrapper is to export data as a Pandas Dataframe and/or a CSV file
|
||||
# this class primarily targets these endpoints: host, tag, hostFinding, vulnerability
|
||||
# it should work on other endpoints as well, but the 4 above are the only ones tested
|
||||
# usage examples of this class are at the end of this file
|
||||
|
||||
# library imports
|
||||
import requests, urllib3, configparser, pandas as pd
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3 import Retry
|
||||
|
||||
# fix (ignore) SSL verification...
|
||||
# Charter-specific issue; feel free to fix this if you can...
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
# Ivanti API class
|
||||
class Ivanti:
|
||||
def __init__(self, config_file='./Ivanti_config.ini'):
|
||||
# read our config file
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_file)
|
||||
|
||||
# set up environment & auth
|
||||
PLATFORM = config.get('platform', 'url') + config.get('platform', 'api_ver')
|
||||
IVANTI_API_KEY = config.get('secrets', 'api_key')
|
||||
self.CLIENT_ID = config.get('platform', 'client_id')
|
||||
self.URL_BASE = f'{PLATFORM}/client/{self.CLIENT_ID}'
|
||||
|
||||
# universal header for our requests
|
||||
self.header = {
|
||||
'x-api-key': IVANTI_API_KEY,
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
|
||||
# dictionaries for filters and fields, sorted with keys by endpoint prefixes
|
||||
self.filters = {}
|
||||
self.fields = {}
|
||||
return
|
||||
|
||||
# function used for HTTP requests- thank you, Ivanti... useful code
|
||||
def request(max_retries=5, backoff_factor=0.5, status_forcelist=(419,429)):
|
||||
"""
|
||||
Create a Requests session that uses automatic retries.
|
||||
:param max_retries: Maximum number of retries to attempt
|
||||
:type max_retries: int
|
||||
:param backoff_factor: Backoff factor used to calculate time between retries.
|
||||
:type backoff_factor: float
|
||||
:param status_forcelist: A tuple containing the response status codes that should trigger a retry.
|
||||
:type status_forcelist: tuple
|
||||
:return: Requests Session
|
||||
:rtype: Requests Session Object
|
||||
"""
|
||||
session = requests.Session()
|
||||
retry = Retry(
|
||||
total=max_retries,
|
||||
read=max_retries,
|
||||
connect=max_retries,
|
||||
backoff_factor=backoff_factor,
|
||||
status_forcelist=status_forcelist,
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
session.mount('https://', adapter)
|
||||
return session
|
||||
|
||||
# retrieve all filters for an endpoint (tag, host, etc)
|
||||
def get_filters(self, endp='tag'):
|
||||
URL_FILTERS = f'{self.URL_BASE}/{endp}/filter'
|
||||
self.last_resp = self.request().get(URL_FILTERS, headers=self.header, verify=False)
|
||||
self.filters[endp] = self.last_resp.json()
|
||||
return self.filters[endp]
|
||||
|
||||
# retrieve all fields for an endpoint (tag, host, etc)
|
||||
def get_fields(self, endp='tag'):
|
||||
URL_FIELDS = f'{self.URL_BASE}/{endp}/export/template'
|
||||
self.last_resp = self.request().get(URL_FIELDS, headers=self.header, verify=False)
|
||||
self.fields[endp] = self.last_resp.json()['exportableFields']
|
||||
return self.fields[endp]
|
||||
|
||||
# this uses the "{subject}/search" endpoint instead of "{subject}/export"
|
||||
def search(self, endp='tag', save=None, pages=None, size=750):
|
||||
'''
|
||||
Uses the "/client/{client_id}/{subject}/search" endpoint to export data as JSON.
|
||||
:param endp: String for endpoint name; host, tag, group, etc. (default: "tag")
|
||||
:param save: String for filename to save, end with ".csv" (default: none)
|
||||
:param pages: Integer to limit the number of pages to pull (default: all pages)
|
||||
:param size: Integer defining how many records to pull per page (default: 750 records)
|
||||
:return: Pandas DataFrame
|
||||
'''
|
||||
# most endpoints follow the same URL structure and usage pattern
|
||||
# filters and fields dont matter for searches- only for exports!
|
||||
URL_SEARCH = f'{self.URL_BASE}/{endp}/search'
|
||||
body = {
|
||||
'projection': 'basic', # can also be set to 'detail'
|
||||
'sort': [
|
||||
{
|
||||
'field': 'id',
|
||||
'direction': 'ASC'
|
||||
}
|
||||
],
|
||||
'page': 0,
|
||||
'size': size
|
||||
}
|
||||
|
||||
# post a search, get first page
|
||||
resp = self.request().post(URL_SEARCH, headers=self.header, json=body, verify=False)
|
||||
if resp.status_code != 200:
|
||||
raise Exception(f'[!] ERROR: Search failed.\n- code: {resp.status_code}\n- text: {resp.text}')
|
||||
totalPages = int(resp.json()['page']['totalPages'])
|
||||
totalRecords = int(resp.json()['page']['totalElements'])
|
||||
body['page'] = int(resp.json()['page']['number']) + 1
|
||||
msg = f'[?] Search requested for "{endp}"\n[?] Total pages: {totalPages}\n[?] Total records: {totalRecords}\n[?] Batch size: {size}'
|
||||
if pages:
|
||||
msg += f'\n[?] Page limit: {pages} pages'
|
||||
print(msg)
|
||||
|
||||
# limit results?
|
||||
if pages:
|
||||
totalPages = pages
|
||||
|
||||
# loop until the last page
|
||||
subject = f'{endp[:-1]}ies' if endp.endswith('y') else f'{endp}s'
|
||||
data = []
|
||||
while body['page'] < totalPages:
|
||||
resp = self.request().post(URL_SEARCH, headers=self.header, json=body, verify=False)
|
||||
body['page'] = int(resp.json()['page']['number']) + 1
|
||||
data.extend(resp.json()['_embedded'][subject])
|
||||
print(f'[?] Page progress: [{body["page"]}/{totalPages}] ({len(data)} total records retrieved)\r', end='')
|
||||
print(f'\n[+] Search completed. {len(data)} records retrieved!')
|
||||
|
||||
# make a nice dataframe, save file if wanted, return the frame
|
||||
df = pd.DataFrame(data)
|
||||
if save:
|
||||
df.to_csv(save, index=False)
|
||||
return df
|
||||
|
||||
### ? EXAMPLE USAGE ? ###
|
||||
# configure the connection and auth, create an instance object
|
||||
#API = Ivanti('./Ivanti_config.ini')
|
||||
|
||||
# the "search" function goes to the "/client/{clientID}/{subject}/search" endpoint
|
||||
#df = API.search('host', save='IvantiHostsTest_5pages.csv', pages=5)
|
||||
#df = API.search('tag', save='IvantiTagsTest_5pages.csv', pages=5)
|
||||
#df = API.search('hostFinding', save='IvantiHostFindingsTest_5pages.csv', pages=5)
|
||||
#df = API.search('vulnerability', save='IvantiVulnerabilitiesTest_5pages.csv', pages=5)
|
||||
|
||||
# you can also retrieve all possible filters and exportable fields per subject
|
||||
#filters = API.get_fields('host')
|
||||
#fields = API.get_filters('tag')
|
||||
194
docs/api/ivanti-api-reference.md
Normal file
194
docs/api/ivanti-api-reference.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Ivanti / RiskSense API Reference
|
||||
|
||||
Base URL: `https://platform4.risksense.com/api/v1`
|
||||
Swagger: `https://platform4.risksense.com/doc/swagger.json`
|
||||
|
||||
Auth: `x-api-key` header. Error codes: 401 bad key, 419 insufficient privileges, 429 rate limited.
|
||||
|
||||
## Endpoints Used
|
||||
|
||||
### Search Workflow Batches
|
||||
|
||||
```
|
||||
POST /client/{clientId}/workflowBatch/search
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Standard JSON body with filters, projection, sort, page, size. Used by `ivantiWorkflows.js` for the daily sync.
|
||||
|
||||
### Create False Positive Workflow
|
||||
|
||||
```
|
||||
POST /client/{clientId}/workflowBatch/falsePositive/request
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
This endpoint does NOT accept JSON. It requires `multipart/form-data` with the following fields:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `name` | string | yes | Workflow batch name (max 255) |
|
||||
| `reason` | string | yes | Reason for the FP determination |
|
||||
| `description` | string | yes | Description (can be empty string but field must be present) |
|
||||
| `expirationDate` | string | yes | ISO-8601 date, e.g. `2026-06-01` |
|
||||
| `overrideControl` | string | yes | `AUTHORIZED`, `NONE`, or `AUTOMATED`. Use `AUTHORIZED` for standard FP workflows. `NONE` with `isEmptyWorkflow=true` is rejected (400). |
|
||||
| `isEmptyWorkflow` | boolean | yes | `true` if no findings attached, `false` otherwise |
|
||||
| `subjectFilterRequest` | string | yes | Stringified JSON (see format below) |
|
||||
| `files` | file | no | Attachments sent inline in the same request |
|
||||
|
||||
#### subjectFilterRequest format
|
||||
|
||||
This is the critical field. It must be a stringified JSON object with this exact structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "hostFinding",
|
||||
"filterRequest": {
|
||||
"filters": [
|
||||
{
|
||||
"field": "id",
|
||||
"exclusive": false,
|
||||
"operator": "IN",
|
||||
"value": "2283734550,2283734551"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key details:
|
||||
- `subject` must be `"hostFinding"` — without this, the API returns 500
|
||||
- `filters` is nested inside `filterRequest`, NOT at the top level — `{"filters":[]}` at the top level returns 500
|
||||
- `value` for multiple IDs is comma-separated as a single string, not an array
|
||||
- `operator` values: `EXACT`, `IN`, `LIKE`, `WILDCARD`, `RANGE`, `CIDR`
|
||||
- For empty workflows, use `{"subject":"hostFinding","filterRequest":{"filters":[]}}` with `isEmptyWorkflow=true`
|
||||
|
||||
#### Response (200/202)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 33418832,
|
||||
"created": "2026-04-08T18:16:08"
|
||||
}
|
||||
```
|
||||
|
||||
Returns HTTP 200 or 202 (Accepted — async job creation). Response contains a numeric `id` (the workflow batch job ID) and `created` timestamp. No `generatedId` or `uuid` in this response.
|
||||
|
||||
### Map Findings to Existing Workflow (tested 2026-04-13)
|
||||
|
||||
```
|
||||
POST /client/{clientId}/workflowBatch/falsePositive/{workflowBatchUuid}/map
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Maps additional host findings to an existing FP workflow batch. Used by the FP submission editing feature to add findings after initial creation.
|
||||
|
||||
**Critical: one finding per call.** The map endpoint only reliably maps one finding per request. Sending multiple finding IDs via the `IN` operator or comma-separated values results in only the first finding being mapped. The multipart/form-data format (used by the create endpoint) returns 500 on this endpoint.
|
||||
|
||||
#### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "hostFinding",
|
||||
"filterRequest": {
|
||||
"filters": [
|
||||
{
|
||||
"field": "id",
|
||||
"exclusive": false,
|
||||
"operator": "EXACT",
|
||||
"value": "2283734550"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key details:
|
||||
- Must be `application/json` (NOT multipart/form-data — returns 500)
|
||||
- Use `EXACT` operator with a single finding ID per call
|
||||
- `IN` operator with comma-separated IDs only maps the first finding
|
||||
- Loop through findings and make one API call per finding
|
||||
- The `workflowBatchUuid` in the URL is the UUID from the search endpoint (not the numeric batch ID from create)
|
||||
|
||||
#### Response (200)
|
||||
|
||||
Returns the updated workflow batch object on success.
|
||||
|
||||
#### UUID resolution
|
||||
|
||||
The `workflowBatchUuid` required in the URL is NOT returned by the create endpoint. To obtain it:
|
||||
|
||||
1. Search via `POST /client/{clientId}/workflowBatch/search` with `{ field: 'name', operator: 'EXACT', value: '<workflow_name>' }`
|
||||
2. Use `projection: 'internal'` to get full batch objects
|
||||
3. The UUID is in the `uuid` field of the returned batch object
|
||||
4. Cache the UUID locally after first resolution (stored in `ivanti_fp_submissions.ivanti_workflow_batch_uuid`)
|
||||
|
||||
#### Implementation in dashboard
|
||||
|
||||
The `resolveWorkflowBatchUuid()` helper in `backend/routes/ivantiFpWorkflow.js` handles UUID resolution:
|
||||
- Returns cached UUID if available in the local submission record
|
||||
- Otherwise searches Ivanti by workflow name, extracts `batch.uuid`, and caches it for future use
|
||||
|
||||
The findings map loop in the `POST /submissions/:id/findings` endpoint:
|
||||
- Iterates through each finding ID individually
|
||||
- Makes one JSON POST per finding with `EXACT` operator
|
||||
- Tracks which findings succeeded vs failed
|
||||
- Only marks queue items as complete for successfully mapped findings
|
||||
- Returns both `addedFindings` and `failedFindings` arrays in the response
|
||||
|
||||
### Other Workflow Endpoints (from Swagger)
|
||||
|
||||
These are available but not all are currently used by the dashboard:
|
||||
|
||||
| Endpoint | Purpose | Status |
|
||||
|----------|---------|--------|
|
||||
| `/workflowBatch/acceptance/request` | Risk acceptance workflow | Not used |
|
||||
| `/workflowBatch/remediation/request` | Remediation workflow | Not used |
|
||||
| `/workflowBatch/severityChange/request` | Severity change workflow | Not used |
|
||||
| `/workflowBatch/{workflowType}/approve` | Approve a workflow (needs `workflowBatchUuid`) | Not used |
|
||||
| `/workflowBatch/{workflowType}/reject` | Reject a workflow | Not used |
|
||||
| `/workflowBatch/{workflowType}/rework` | Send back for rework | Not used |
|
||||
| `/workflowBatch/{workflowType}/update` | Update a workflow | Not used |
|
||||
| `/workflowBatch/{workflowType}/{workflowBatchUuid}/map` | Map findings to workflow | Used (FP editing) |
|
||||
| `/workflowBatch/{workflowType}/{workflowBatchUuid}/unmap` | Unmap findings | Not used |
|
||||
| `/workflowBatch/{workflowType}/{workflowBatchUuid}/attach` | Attach file to existing workflow | **Broken — see note** |
|
||||
| `/workflowBatch/{workflowType}/{workflowBatchUuid}/detach` | Detach file | Not used |
|
||||
| `/workflowBatch/model` | Get model/schema | Not used |
|
||||
| `/workflowBatch/filter` | Get available filter fields | Not used |
|
||||
| `/workflowBatch/suggest` | Get suggested values for a filter field | Not used |
|
||||
|
||||
### Known Limitations
|
||||
|
||||
#### Attach endpoint does not work (tested 2026-04-13)
|
||||
|
||||
The `/workflowBatch/{workflowType}/{workflowBatchUuid}/attach` endpoint is listed in the Swagger spec but returns HTTP 400 (Bad Request) for all tested request formats:
|
||||
|
||||
- `multipart/form-data` with field name `file` (singular) — 400
|
||||
- `multipart/form-data` with field name `files` (plural) — 400
|
||||
- Tested with `Content-Type: application/octet-stream` and `image/png` — both 400
|
||||
- Tested with both `ivantiMultipartPost` and `ivantiFormPost` helpers — both 400
|
||||
|
||||
The Ivanti response is a generic Spring Boot error with no detail message:
|
||||
```json
|
||||
{"timestamp":"...","status":400,"error":"Bad Request","path":"/api/v1/client/1550/workflowBatch/falsePositive/{uuid}/attach"}
|
||||
```
|
||||
|
||||
**Workaround:** File attachments can only be uploaded during the initial workflow creation (sent inline with the `/workflowBatch/falsePositive/request` endpoint). To add attachments to an existing workflow, users must upload them directly in the Ivanti platform UI.
|
||||
|
||||
#### Search by numeric batch ID does not work
|
||||
|
||||
The `/workflowBatch/search` endpoint does not support filtering by the numeric `id` returned from the create endpoint. Searching with `{ field: 'id', operator: 'EXACT', value: '33432541' }` returns 0 results. Searching by `name` field works and returns the workflow batch object including the `uuid` field needed for map/attach operations.
|
||||
|
||||
#### UUID not returned by create endpoint
|
||||
|
||||
The `/workflowBatch/falsePositive/request` create endpoint returns only `{ id: <number>, created: <timestamp> }`. The `uuid` needed for map/attach/approve/reject operations must be obtained separately via the search endpoint.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `IVANTI_API_KEY` | — | Required. API key for authentication |
|
||||
| `IVANTI_CLIENT_ID` | `1550` | Client ID in the Ivanti platform |
|
||||
| `IVANTI_SKIP_TLS` | `false` | Set `true` to skip TLS verification |
|
||||
| `IVANTI_FIRST_NAME` | — | Used for workflow search filter (sync) |
|
||||
| `IVANTI_LAST_NAME` | — | Used for workflow search filter (sync) |
|
||||
7
docs/api/ivanti-config-template.ini
Normal file
7
docs/api/ivanti-config-template.ini
Normal file
@@ -0,0 +1,7 @@
|
||||
[platform]
|
||||
url = https://platform4.risksense.com
|
||||
api_ver = /api/v1
|
||||
# PROD 1550 | UAT 1551
|
||||
client_id = <pick 1550 or 1551>
|
||||
[secrets]
|
||||
api_key = <your API key here>
|
||||
53228
docs/api/ivanti-neurons-swagger.json
Normal file
53228
docs/api/ivanti-neurons-swagger.json
Normal file
File diff suppressed because one or more lines are too long
170
docs/api/jira-api-use-cases.md
Normal file
170
docs/api/jira-api-use-cases.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Jira REST API Use Cases — STEAM Security Dashboard
|
||||
|
||||
## Overview
|
||||
|
||||
The STEAM Security Dashboard is a self-hosted vulnerability management tool used by the NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG teams. It integrates with Jira Data Center to create, track, and sync vulnerability remediation tickets linked to CVE records.
|
||||
|
||||
All API calls are made from a single Node.js backend process. The integration uses Basic Auth with a service account and enforces Charter's posted rate limits client-side.
|
||||
|
||||
---
|
||||
|
||||
## Charter Compliance Summary
|
||||
|
||||
| Requirement | Implementation |
|
||||
|---|---|
|
||||
| Authentication | Basic Auth with service account (`JIRA_API_USER` + `JIRA_API_TOKEN`) |
|
||||
| Rate limit — daily | Client-side enforced: 1 440 requests/day max |
|
||||
| Rate limit — burst | Client-side enforced: 60 requests/minute max |
|
||||
| Inter-request delay — GETs | 1 second minimum between GET requests |
|
||||
| Inter-request delay — writes | 2 seconds minimum between PUT/POST/DELETE requests |
|
||||
| Explicit field lists | Every GET includes `?fields=` parameter; `/rest/api/2/field` is blocked |
|
||||
| No bulk updates | Issues are updated one at a time; `/rest/api/2/issue/bulk` is blocked |
|
||||
| Bulk reads via JQL | Multi-ticket sync uses a single `GET /rest/api/2/search` with JQL query parameters, not per-issue GETs |
|
||||
| Single-issue fetch via JQL | `GET /rest/api/2/search?jql=key="KEY" AND project=<KEY>&fields=...&maxResults=1` |
|
||||
| JQL scoping | All recurring JQL queries include `updated >= -Xh` clause and `project = <KEY>` scoping |
|
||||
| `maxResults` cap | Search queries capped at 1 000 results per page |
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Connection Test
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `GET /rest/api/2/myself` |
|
||||
| **Trigger** | Admin clicks "Test Connection" on the Jira settings panel |
|
||||
| **Frequency** | Manual, infrequent (a few times per day at most) |
|
||||
| **Purpose** | Verify service account credentials and connectivity |
|
||||
| **Fields requested** | Default (myself endpoint returns user profile) |
|
||||
|
||||
### 2. Create Issue
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `POST /rest/api/2/issue` |
|
||||
| **Trigger** | User clicks "Create in Jira" from a CVE detail panel |
|
||||
| **Frequency** | Manual, estimated 5–20 per day |
|
||||
| **Purpose** | Create a vulnerability remediation ticket linked to a CVE/vendor pair |
|
||||
| **Fields sent** | `project.key`, `summary`, `issuetype.name`, `description` |
|
||||
| **Notes** | A local record is also created in the dashboard database linking the Jira key to the CVE |
|
||||
|
||||
### 3. Get Single Issue
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `GET /rest/api/2/search?jql=key="ISSUE-KEY" AND project=<KEY>&fields=summary,status,assignee,created,updated,priority,issuetype,project,resolution&maxResults=1` |
|
||||
| **Trigger** | User clicks "Sync" on a single Jira ticket row |
|
||||
| **Frequency** | Manual, estimated 10–30 per day |
|
||||
| **Purpose** | Refresh a single ticket's status and summary from Jira via JQL search |
|
||||
| **Notes** | Uses JQL-based lookup instead of single-issue GET per Charter compliance. Fields are always specified explicitly. |
|
||||
|
||||
### 4. Update Issue
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `PUT /rest/api/2/issue/{issueKey}` |
|
||||
| **Trigger** | Future feature — local edits synced back to Jira |
|
||||
| **Frequency** | Manual, estimated 5–10 per day when enabled |
|
||||
| **Purpose** | Update issue summary or other fields from the dashboard |
|
||||
| **Notes** | Issues are updated one at a time; bulk PUT is not used |
|
||||
|
||||
### 5. Add Comment
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `POST /rest/api/2/issue/{issueKey}/comment` |
|
||||
| **Trigger** | Dashboard adds audit trail comments to linked tickets |
|
||||
| **Frequency** | Automated on certain actions, estimated 5–15 per day |
|
||||
| **Purpose** | Maintain an audit trail on the Jira ticket for compliance visibility |
|
||||
|
||||
### 6. Get Transitions
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `GET /rest/api/2/issue/{issueKey}/transitions` |
|
||||
| **Trigger** | Dashboard checks available workflow transitions before moving a ticket |
|
||||
| **Frequency** | Manual, paired with transition calls, estimated 5–10 per day |
|
||||
| **Purpose** | Discover valid status transitions for the issue's current workflow state |
|
||||
|
||||
### 7. Transition Issue
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `POST /rest/api/2/issue/{issueKey}/transitions` |
|
||||
| **Trigger** | User moves a ticket to a new status from the dashboard |
|
||||
| **Frequency** | Manual, estimated 5–10 per day |
|
||||
| **Purpose** | Move ticket through workflow states (e.g., Open to In Progress to Closed) |
|
||||
|
||||
### 8. JQL Search (Bulk Sync)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `GET /rest/api/2/search?jql=...&fields=...&maxResults=...&startAt=...` |
|
||||
| **Trigger** | Admin clicks "Sync All" on the Jira tickets panel |
|
||||
| **Frequency** | Manual, estimated 1–3 times per day |
|
||||
| **Purpose** | Bulk-refresh all tracked tickets in a single request instead of per-issue GETs |
|
||||
| **JQL pattern** | `key in ("KEY-1", "KEY-2", ...) AND updated >= -24h AND project = <KEY>` |
|
||||
| **Fields requested** | `summary, status, assignee, created, updated, priority, issuetype, project, resolution` |
|
||||
| **Batch size** | 100 keys per JQL query; multiple batches if needed |
|
||||
| **Notes** | Uses GET with URL-encoded query parameters per Charter compliance. Stops early if rate limit budget is running low (burst remaining <= 5 or daily remaining <= 10) |
|
||||
|
||||
### 9. Issue Lookup
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Endpoint** | `GET /rest/api/2/search?jql=key="ISSUE-KEY" AND project=<KEY>&fields=...&maxResults=1` |
|
||||
| **Trigger** | User looks up a Jira issue by key from the dashboard search |
|
||||
| **Frequency** | Manual, estimated 5–15 per day |
|
||||
| **Purpose** | Quick lookup of any Jira issue to view its current state via JQL search |
|
||||
|
||||
---
|
||||
|
||||
## Estimated Daily API Usage
|
||||
|
||||
| Operation | Estimated calls/day | Method | Delay enforced |
|
||||
|---|---|---|---|
|
||||
| Connection test | 2–5 | GET | 1s |
|
||||
| Create issue | 5–20 | POST | 2s |
|
||||
| Get single issue | 10–30 | GET | 1s |
|
||||
| Update issue | 5–10 | PUT | 2s |
|
||||
| Add comment | 5–15 | POST | 2s |
|
||||
| Get transitions | 5–10 | GET | 1s |
|
||||
| Transition issue | 5–10 | POST | 2s |
|
||||
| JQL search (sync) | 1–5 | GET | 1s |
|
||||
| Issue lookup | 5–15 | GET | 1s |
|
||||
| **Total estimated** | **43–120** | | |
|
||||
|
||||
Well within the 1 440/day limit. Burst usage stays under 60/minute due to enforced inter-request delays.
|
||||
|
||||
---
|
||||
|
||||
## Blocked Endpoints
|
||||
|
||||
The integration explicitly blocks these endpoints to comply with Charter policy:
|
||||
|
||||
- `/rest/api/2/field` — field metadata is never queried; fields are specified in code
|
||||
- `/rest/api/2/issue/bulk` — bulk updates are not used; issues are updated individually
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **429 responses**: Surfaced to the user as "Rate limit exceeded. Try again later." No automatic retry.
|
||||
- **5xx responses**: Surfaced as "Jira API error" with the response body for debugging.
|
||||
- **Network failures**: Caught and surfaced with the error message.
|
||||
- **Timeout**: 15 second timeout per request; surfaced as a timeout error.
|
||||
|
||||
---
|
||||
|
||||
## UAT Test Evidence
|
||||
|
||||
The UAT test script (`backend/scripts/jira-uat-test.js`) exercises all use cases listed above and produces a log file at `backend/scripts/jira-uat-test.log`. This log can be attached to or referenced in the ATLSUP approval ticket.
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
node scripts/jira-uat-test.js
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user