feat: add return classification for archive chart, CARD API integration, compliance charts, systemd services
This commit is contained in:
339
.kiro/specs/card-api-integration/design.md
Normal file
339
.kiro/specs/card-api-integration/design.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# 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
|
||||
|
||||
```mermaid
|
||||
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)
|
||||
|
||||
```mermaid
|
||||
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:
|
||||
```javascript
|
||||
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`):
|
||||
```json
|
||||
[
|
||||
{ "_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}`):
|
||||
```json
|
||||
{
|
||||
"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`):
|
||||
```json
|
||||
{
|
||||
"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](https://github.com/dubzzz/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)
|
||||
Reference in New Issue
Block a user