16 KiB
Design Document: CARD API Integration
Overview
This design integrates the CARD Asset Ownership API into the STEAM Security Dashboard, enabling users to confirm, decline, and redirect CARD assets directly from the Ivanti Queue. The integration follows the existing architectural patterns established by the Atlas API integration (atlasApi.js / atlas.js route), adding OAuth Bearer token management with automatic caching and refresh.
The implementation is split into three layers:
- Helper module (
backend/helpers/cardApi.js) — already built and UAT-tested. Handles HTTP transport, OAuth token lifecycle, and high-level CARD API wrappers. - Route module (
backend/routes/cardApi.js) — new Express router that proxies CARD operations, validates queue items, orchestrates the two-step update_token flow, and logs audit entries. - Frontend UI — CARD action buttons (Confirm, Decline, Redirect) on queue items, team selection dropdowns, and an asset search panel.
Key Findings from UAT Testing
- Token endpoint is
POST /api/v1/auth/get_token(not GET) - Team name field in API responses is
card_team_nameor_id - The
update_tokenis nested atowner.update_tokenin the owner record - The assets endpoint requires a
dispositionquery parameter (returns 500 without it) - The helper module and UAT test script are already built and validated
Architecture
graph TD
subgraph Frontend
QP[Ivanti Queue Panel] --> AB[CARD Action Buttons]
AB --> CF[Confirm Form]
AB --> DF[Decline Form]
AB --> RF[Redirect Form]
QP --> AS[Asset Search Panel]
end
subgraph Backend
CR[cardApi Route<br>/api/card/*] --> CM[cardApi Helper]
CR --> DB[(SQLite DB<br>ivanti_todo_queue)]
CR --> AL[Audit Logger]
CM --> TM[Token Manager]
end
subgraph External
CARD[CARD API<br>card.charter.com]
end
CF --> CR
DF --> CR
RF --> CR
AS --> CR
CM --> CARD
TM --> CARD
Request Flow for Mutations (Confirm/Decline/Redirect)
sequenceDiagram
participant UI as Frontend
participant Route as cardApi Route
participant DB as SQLite
participant Helper as cardApi Helper
participant CARD as CARD API
UI->>Route: POST /api/card/queue/:id/confirm
Route->>DB: Validate queue item (exists, user, CARD, pending)
DB-->>Route: Queue item record
Route->>Helper: getOwner(assetId)
Helper->>CARD: GET /api/v1/owner/{assetId}
CARD-->>Helper: Owner record with update_token
Helper-->>Route: { owner: { update_token: "..." } }
Route->>Helper: confirmAsset(assetId, team, token, comment)
Helper->>CARD: POST /api/v2/owner/{assetId}/confirm?update_token=...
CARD-->>Helper: Success response
Helper-->>Route: { status: 200, body: "..." }
Route->>DB: UPDATE status = 'complete'
Route->>AL: logAudit(card_confirm, ...)
Route-->>UI: { success: true, cardResponse: ... }
Components and Interfaces
1. CARD API Helper (backend/helpers/cardApi.js) — Already Built
The helper module is complete and UAT-tested. It exports:
| Export | Type | Description |
|---|---|---|
isConfigured |
boolean |
true when CARD_API_URL, CARD_API_USER, CARD_API_PASS are all set |
missingVars |
string[] |
List of missing env var names |
cardGet(urlPath, options) |
function |
GET request with Bearer auth, returns { status, body } |
cardPost(urlPath, body, options) |
function |
POST request with Bearer auth, returns { status, body } |
getTeams() |
function |
GET /api/v1/teams — returns { status, body, ok } |
getTeamAssets(teamName, opts) |
function |
GET /api/v1/team/{name}/assets with disposition, page, pageSize |
getOwner(assetId) |
function |
GET /api/v1/owner/{assetId} — returns owner record with update_token |
confirmAsset(assetId, team, token, comment) |
function |
POST /api/v2/owner/{id}/confirm |
declineAsset(assetId, team, token, comment) |
function |
POST /api/v2/owner/{id}/decline |
redirectAsset(assetId, from, to, token) |
function |
POST /api/v2/owner/{id}/{from}/redirect |
invalidateToken() |
function |
Clears cached Bearer token |
testConnection() |
function |
Acquires token and returns { ok, token } or { ok, error } |
Token Manager (internal to helper):
- Acquires tokens via
POST /api/v1/auth/get_tokenwith Basic Auth - Caches in memory with 1-hour TTL, refreshes when within 60s of expiry
- Automatically retries once on HTTP 401 (invalidate → re-acquire → retry)
2. CARD API Route (backend/routes/cardApi.js) — New
Factory function: createCardApiRouter(db, requireAuth) → Express Router
Endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /api/card/status |
Returns { configured: boolean } |
| GET | /api/card/teams |
Proxies CARD teams list |
| GET | /api/card/teams/:teamName/assets |
Proxies team assets with disposition (required), page, page_size (default 50) |
| GET | /api/card/owner/:assetId |
Proxies owner record lookup |
| POST | /api/card/queue/:queueItemId/confirm |
Confirm asset — body: { teamName, assetId, comment? } |
| POST | /api/card/queue/:queueItemId/decline |
Decline asset — body: { teamName, assetId, comment? } |
| POST | /api/card/queue/:queueItemId/redirect |
Redirect asset — body: { fromTeam, toTeam, assetId } |
Middleware: All endpoints use requireAuth(db) + requireGroup('Admin', 'Standard_User').
Mutation flow (confirm/decline/redirect):
- Validate queue item: exists, belongs to
req.user.id,workflow_type = 'CARD',status = 'pending' - Fetch owner record via
getOwner(assetId)to get freshupdate_token - Extract
update_tokenfromowner.update_token(nested path) - Execute CARD mutation with the
update_token - On success: update queue item
status = 'complete', log audit, return response - On failure: leave queue item as
pending, log audit failure, return error
3. Frontend Components
Modified: Ivanti Queue panel in the existing queue UI
New UI elements:
- CARD Action Buttons: Confirm, Decline, Redirect buttons rendered on pending CARD queue items
- Confirm/Decline Form: Team dropdown (from
/api/card/teams) + optional comment field - Redirect Form: From Team dropdown + To Team dropdown
- Asset Search Panel: Team dropdown + disposition filter + paginated results table
- Loading/Error States: Inline loading indicators and error messages per queue item
Session-level caching: Teams list fetched once per browser session and reused across all forms.
4. Server Integration (backend/server.js)
Mount the new route:
const createCardApiRouter = require('./routes/cardApi');
// ...
app.use('/api/card', createCardApiRouter(db, requireAuth));
Data Models
Existing: ivanti_todo_queue Table
No schema changes required. CARD items use workflow_type = 'CARD'.
| Column | Type | Notes |
|---|---|---|
id |
INTEGER | Primary key |
user_id |
INTEGER | FK to users |
finding_id |
TEXT | Ivanti finding identifier |
finding_title |
TEXT | Finding description |
cves_json |
TEXT | JSON array of CVE IDs |
ip_address |
TEXT | Asset IP address (used as CARD Asset_ID with suffix) |
hostname |
TEXT | Asset hostname |
vendor |
TEXT | Empty string for CARD items |
workflow_type |
TEXT | 'CARD' for this integration |
status |
TEXT | 'pending' or 'complete' |
created_at |
DATETIME | Auto-set |
updated_at |
DATETIME | Auto-updated |
CARD API Response Shapes (from UAT testing)
Teams response (GET /api/v1/teams):
[
{ "_id": "NTS-AEO-STEAM", "card_team_name": "NTS-AEO-STEAM", ... },
{ "_id": "NTS-ACCESS-ENG", "card_team_name": "NTS-ACCESS-ENG", ... }
]
Team name extraction: t.card_team_name || t._id
Owner record (GET /api/v1/owner/{assetId}):
{
"owner": {
"update_token": "abc123...",
"dispositions": [
{ "team": "NTS-AEO-STEAM", "disposition": "confirmed", ... }
],
...
}
}
Update token path: response.owner.update_token
Team assets (GET /api/v1/team/{name}/assets?disposition=confirmed&page_size=50):
{
"assets": [ { "asset_id": "98.8.142.56-NATL", ... } ],
"total": 150,
"page": 1,
"page_size": 50
}
Audit Log Entries
| Action | entityType | entityId | Details |
|---|---|---|---|
card_confirm |
ivanti_todo_queue |
queue item ID | { assetId, teamName, comment, cardStatus } |
card_decline |
ivanti_todo_queue |
queue item ID | { assetId, teamName, comment, cardStatus } |
card_redirect |
ivanti_todo_queue |
queue item ID | { assetId, fromTeam, toTeam, cardStatus } |
card_search |
card_asset |
team name | { disposition, resultCount } |
card_action_failed |
ivanti_todo_queue |
queue item ID | { actionType, assetId, error, cardStatus } |
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: isConfigured reflects environment variable presence
For any combination of the three required environment variables (CARD_API_URL, CARD_API_USER, CARD_API_PASS) being present/non-empty or absent/empty, isConfigured SHALL be true if and only if all three are present and non-empty.
Validates: Requirements 1.1, 3.3
Property 2: All CARD API responses have consistent shape
For any successful CARD API call (any HTTP method and URL path), the resolved Promise SHALL contain an object with a numeric status field and a string body field.
Validates: Requirements 1.7
Property 3: Token acquisition errors include status and body
For any non-success HTTP status code and response body returned by the token acquisition endpoint, the rejected Promise error message SHALL include both the HTTP status code and the response body text.
Validates: Requirements 2.7
Property 4: CARD API error status codes are forwarded through proxy
For any HTTP error status code (4xx or 5xx) returned by the CARD API on a proxied request, the route SHALL return that same status code to the client along with a JSON error body containing the upstream error message.
Validates: Requirements 4.9
Property 5: Queue item validation rejects invalid states for mutations
For any CARD mutation request (confirm, decline, or redirect), if the referenced queue item does not exist, does not belong to the requesting user, has workflow_type other than 'CARD', or has status other than 'pending', the endpoint SHALL reject the request with the appropriate HTTP error code (404 or 400) without calling the CARD API.
Validates: Requirements 5.4, 5.5, 5.6
Property 6: Mutation input validation enforces required fields
For any CARD mutation request, the endpoint SHALL reject with HTTP 400 if any required field is missing or empty: teamName and assetId for confirm/decline; fromTeam, toTeam, and assetId for redirect. Optional fields (e.g., comment) SHALL be accepted when absent.
Validates: Requirements 5.11, 5.12, 5.13
Property 7: CARD mutation audit entries contain required fields
For any CARD mutation action (confirm, decline, or redirect) executed through the dashboard, the audit log entry SHALL contain the correct action name (card_confirm, card_decline, or card_redirect), entityType of 'ivanti_todo_queue', the queue item ID as entityId, the requesting user's userId, username, and ipAddress, and a details object containing the assetId and CARD API response status.
Validates: Requirements 9.1, 9.2, 9.3, 9.6
Error Handling
CARD Helper Error Handling
| Scenario | Behavior |
|---|---|
| CARD API unreachable / timeout | Reject with [card-api] {METHOD} {path} failed: {reason} |
| Token endpoint returns non-2xx | Reject with [card-api] Token acquisition failed with HTTP {status}: {body} |
| Token endpoint returns unparseable JSON | Fall back to raw body as token string; reject if empty |
| Token endpoint returns empty token | Reject with [card-api] Token parse failure: empty token in response body. |
| Non-auth request returns 401 | Invalidate token, re-acquire, retry once. If retry also 401, return the 401 |
Route Error Handling
| CARD API Status | Route Response | Error Message |
|---|---|---|
| 401 (token endpoint) | 401 | CARD authorization failed. Check service account credentials. |
| 403 (token endpoint) | 403 | CARD access denied. The service account may not be onboarded with the CARD team. |
| 525 (token endpoint) | 502 | CARD LDAP error. The service account may not be provisioned correctly. |
| 401 (API call, after retry) | 401 | CARD token expired or invalid. The request has been retried once automatically. |
| 403 (API call) | 403 | Insufficient CARD permissions for this operation. |
| Any unhandled error | 502 | CARD API request failed. + details |
| Not configured | 503 | CARD API is not configured. + missing vars |
All errors are logged to console with [card-api] prefix for consistent log filtering.
Frontend Error Handling
- Inline error messages on the affected queue item (no modal popups)
- Loading state disables action buttons to prevent double-submission
- Network errors display a generic "Unable to reach server" message
- CARD-specific errors display the backend error message verbatim
Testing Strategy
Property-Based Tests (fast-check)
The project uses Jest as the test runner. Property-based tests will use fast-check with a minimum of 100 iterations per property.
Each property test references its design document property:
| Property | Test File | What It Validates |
|---|---|---|
| Property 1: isConfigured | backend/__tests__/card-isConfigured.property.test.js |
Env var combinations → isConfigured correctness |
| Property 2: Response shape | backend/__tests__/card-response-shape.property.test.js |
All API responses have { status, body } |
| Property 3: Token error messages | backend/__tests__/card-token-errors.property.test.js |
Error messages include status + body |
| Property 4: Error forwarding | backend/__tests__/card-error-forwarding.property.test.js |
Proxy forwards CARD error status codes |
| Property 5: Queue validation | backend/__tests__/card-queue-validation.property.test.js |
Invalid queue states rejected correctly |
| Property 6: Input validation | backend/__tests__/card-input-validation.property.test.js |
Required fields enforced on mutations |
| Property 7: Audit entries | backend/__tests__/card-audit-entries.property.test.js |
Mutation audit logs have correct shape |
Tag format: Feature: card-api-integration, Property {N}: {title}
Unit Tests (example-based)
- Token acquisition flow (mock HTTP, verify Basic Auth header)
- Token caching and refresh timing
- 401 retry logic (mock 401 → 200 sequence)
- Two-step update_token flow (getOwner → mutation)
- Specific CARD API endpoint URL construction
- Default page_size=50 on assets endpoint
- TLS skip configuration
Integration Tests
- Route mounting at
/api/cardprefix - Auth middleware enforcement (401 without session)
- End-to-end confirm/decline/redirect with mocked CARD API
- Asset search with pagination
Frontend Tests
- CARD action buttons render only on pending CARD items
- Form submission sends correct request body
- Loading state disables buttons
- Error messages display inline
- Teams list caching (single fetch per session)