Files

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:

  1. Helper module (backend/helpers/cardApi.js) — already built and UAT-tested. Handles HTTP transport, OAuth token lifecycle, and high-level CARD API wrappers.
  2. 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.
  3. 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_name or _id
  • The update_token is nested at owner.update_token in the owner record
  • The assets endpoint requires a disposition query 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_token with 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):

  1. Validate queue item: exists, belongs to req.user.id, workflow_type = 'CARD', status = 'pending'
  2. Fetch owner record via getOwner(assetId) to get fresh update_token
  3. Extract update_token from owner.update_token (nested path)
  4. Execute CARD mutation with the update_token
  5. On success: update queue item status = 'complete', log audit, return response
  6. 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/card prefix
  • 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)