# 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) |
| `IVANTI_MANAGED_BUS` | `NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM` | Comma-separated list of BUs considered "managed" for archive drift classification. Findings leaving these BUs are classified as bu_reassignment. |
