Files
cve-dashboard/.kiro/specs/atlas-action-plans/design.md
root 4c04c9870a Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.

Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync

Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row

Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00

498 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design Document: Atlas Action Plans Integration
## Overview
This feature integrates the Atlas InfoSec action plans API into the STEAM Security Dashboard, allowing users to view and manage compliance action plans for host findings directly from the ReportingPage. The integration follows the existing proxy-and-cache pattern used by the Ivanti integration — a backend helper handles HTTP communication with the external API, a SQLite cache stores host-level status for fast page loads, and Express routes expose both cached status and proxied CRUD operations to the React frontend.
The frontend adds two visual elements to the ReportingPage: a small badge in the Host column indicating action plan coverage, and a slide-out panel for viewing, creating, and updating plans. A manual sync button — matching the existing Ivanti sync button pattern — lets users refresh cached Atlas data on demand.
### Key Design Decisions
- **Proxy pattern over direct frontend calls**: The frontend never talks to Atlas directly. All Atlas API calls go through the STEAM backend, which handles authentication, TLS configuration, and audit logging. This keeps Atlas credentials server-side and provides a single audit trail.
- **Cache-then-fetch**: The ReportingPage loads cached badge data from SQLite on mount (fast), and users trigger a manual sync to refresh from Atlas (slow, one API call per host). This matches the existing Ivanti sync UX.
- **Sequential host sync (not bulk GET)**: The Atlas API only exposes per-host `GET /hosts/{host_id}/action-plans`. There is no bulk status endpoint, so the sync iterates over unique host IDs from the Ivanti cache. Concurrency is limited to avoid overwhelming Atlas.
- **Basic Auth with runtime base64 encoding**: Atlas uses `Authorization: Basic <base64(user:pass)>` rather than the API key pattern used by Ivanti. The helper computes this at request time from environment variables.
- **ID mapping**: Ivanti `host.hostId` maps directly to Atlas `host_id` in URL paths. Ivanti `f.id` maps to Atlas `active_host_findings_id` in request bodies. No translation layer is needed.
## Architecture
```mermaid
graph TD
subgraph Frontend
RP[ReportingPage]
AB[AtlasBadge]
SP[AtlasSlideOutPanel]
end
subgraph Backend
AR[Atlas Router<br/>/api/atlas/*]
AH[Atlas Helper<br/>atlasApi.js]
AC[(Atlas Cache<br/>SQLite)]
IC[(Ivanti Cache<br/>SQLite)]
AL[Audit Log]
end
subgraph External
ATLAS[Atlas InfoSec API<br/>https://atlas-infosec.caas.charterlab.com]
end
RP -->|GET /api/atlas/status| AR
RP -->|POST /api/atlas/sync| AR
AB -->|click| SP
SP -->|GET /api/atlas/hosts/:id/action-plans| AR
SP -->|PUT /api/atlas/hosts/:id/action-plans| AR
SP -->|PATCH /api/atlas/hosts/:id/action-plans| AR
AR -->|read cached status| AC
AR -->|read host IDs| IC
AR -->|GET, PUT, PATCH, POST| AH
AR -->|logAudit| AL
AH -->|HTTPS + Basic Auth| ATLAS
AR -->|upsert cache| AC
```
### Data Flow: Page Load
1. ReportingPage mounts, fetches Ivanti findings from existing cache (existing behavior)
2. ReportingPage fetches `GET /api/atlas/status` — returns all cached Atlas rows
3. Frontend builds a `Map<hostId, atlasStatus>` and passes it to table rendering
4. Each Host column cell checks the map — if a match exists, renders an AtlasBadge
### Data Flow: Manual Sync
1. User clicks Atlas sync button
2. Frontend sends `POST /api/atlas/sync`
3. Backend extracts unique `hostId` values from `ivanti_findings_cache.findings_json`
4. Backend calls `GET /hosts/{host_id}/action-plans` for each host (with concurrency limit of 5)
5. Backend upserts each result into `atlas_action_plans_cache`
6. Backend returns summary `{ synced, withPlans, failed }`
7. Frontend re-fetches `GET /api/atlas/status` and updates badges
### Data Flow: Create/Update Plan
1. User clicks AtlasBadge → slide-out panel opens
2. Panel fetches `GET /api/atlas/hosts/:hostId/action-plans` for live data
3. User fills create form or edits existing plan
4. Frontend sends `PUT` (create) or `PATCH` (update) to `/api/atlas/hosts/:hostId/action-plans`
5. Backend validates request body, proxies to Atlas API, logs audit entry
6. On success, panel refreshes plan list and frontend re-fetches cached status
## Components and Interfaces
### Backend: Atlas API Helper (`backend/helpers/atlasApi.js`)
A new helper module following the same pattern as `ivantiApi.js` — promise-based HTTP using Node's `https` module, with TLS skip support.
```javascript
// Exported functions
function atlasRequest(method, urlPath, body, options)
// method: 'GET' | 'PUT' | 'PATCH' | 'POST'
// urlPath: e.g. '/hosts/29329662/action-plans'
// body: object | null (null for GET)
// options: { timeout?: number }
// Returns: Promise<{ status: number, body: string }>
// Convenience wrappers
function atlasGet(urlPath, options)
function atlasPut(urlPath, body, options)
function atlasPatch(urlPath, body, options)
function atlasPost(urlPath, body, options)
```
**Configuration** (read from `process.env` at module load):
- `ATLAS_API_URL` — base URL (e.g. `https://atlas-infosec.caas.charterlab.com`)
- `ATLAS_API_USER` — service account username
- `ATLAS_API_PASS` — service account password
- `ATLAS_SKIP_TLS``'true'` to disable certificate verification
**Auth header**: `Authorization: Basic ${Buffer.from(user + ':' + pass).toString('base64')}`
**Timeouts**: 15s default for single-host endpoints, 60s for bulk. Passed via `options.timeout`.
**Error handling**: Network errors and timeouts reject the promise. Non-2xx responses resolve normally with `{ status, body }` — the caller decides how to handle them.
### Backend: Migration (`backend/migrations/add_atlas_action_plans_cache.js`)
Creates the `atlas_action_plans_cache` table following the existing migration pattern.
```javascript
// Table schema
db.run(`
CREATE TABLE IF NOT EXISTS atlas_action_plans_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL UNIQUE,
has_action_plan INTEGER NOT NULL DEFAULT 0,
plan_count INTEGER NOT NULL DEFAULT 0,
plans_json TEXT NOT NULL DEFAULT '[]',
synced_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`);
db.run(`
CREATE INDEX IF NOT EXISTS idx_atlas_cache_host_id
ON atlas_action_plans_cache(host_id)
`);
```
### Backend: Atlas Router (`backend/routes/atlas.js`)
Factory function pattern: `createAtlasRouter(db, requireAuth)` returns an Express Router mounted at `/api/atlas`.
| Method | Path | Auth | Group | Description |
|--------|------|------|-------|-------------|
| `GET` | `/status` | requireAuth | any | Return all cached Atlas rows |
| `POST` | `/sync` | requireAuth | Admin, Standard_User | Sync Atlas data for all Ivanti hosts |
| `GET` | `/hosts/:hostId/action-plans` | requireAuth | any | Proxy to Atlas GET plans |
| `PUT` | `/hosts/:hostId/action-plans` | requireAuth | Admin, Standard_User | Proxy to Atlas create plan |
| `PATCH` | `/hosts/:hostId/action-plans` | requireAuth | Admin, Standard_User | Proxy to Atlas update plan |
| `POST` | `/hosts/bulk-action-plans` | requireAuth | Admin, Standard_User | Proxy to Atlas bulk create |
**Sync implementation**:
1. Parse `ivanti_findings_cache.findings_json` to extract unique `hostId` values (skip nulls)
2. Process hosts in batches of 5 concurrent requests using `Promise.allSettled`
3. For each host, call `atlasGet('/hosts/' + hostId + '/action-plans')`
4. On 2xx: upsert cache row with plan count and summary JSON
5. On non-2xx: increment failure counter, log warning, continue
6. Return `{ synced: N, withPlans: N, failed: N }`
**Validation (PUT create)**:
- `plan_type` must be one of: `decommission`, `remediation`, `false_positive`, `risk_acceptance`, `scan_exclusion`
- `commit_date` must match `/^\d{4}-\d{2}-\d{2}$/`
- `hostId` param must be a positive integer
**Validation (PATCH update)**:
- `action_plan_id` must be a non-empty string
- `updates` must be a non-null object
**Validation (POST bulk)**:
- `host_ids` must be a non-empty array of positive integers
- `plan_type` and `commit_date` validated same as PUT
### Frontend: AtlasBadge Component
A small inline badge rendered inside the Host column cell, next to the hostname. Clicking it opens the slide-out panel.
**Props**: `{ hostId, atlasStatus, onClick }`
**Rendering logic**:
- If `atlasStatus` is `undefined` (host not in Atlas cache): render nothing
- If `atlasStatus.has_action_plan === 0`: render warning badge (amber border, "0" text)
- If `atlasStatus.plan_count > 0`: render success badge (emerald border, count text)
**Style**: Small pill badge using the design system's badge pattern — monospace font, 0.58rem, inline-flex, with border color indicating status. Positioned after the hostname text in the OverrideCell wrapper.
### Frontend: AtlasSlideOutPanel Component
A right-side drawer panel, similar in concept to the existing FP submission detail panels. Renders over the table content with a semi-transparent backdrop.
**Props**: `{ hostId, hostName, onClose, canWrite }`
**Sections**:
1. **Header**: hostname, host ID, close button
2. **Plan list**: fetched from `GET /api/atlas/hosts/:hostId/action-plans` on open. Each plan shows type, commit date, status, and optional VNR/EXC references
3. **Create form** (if `canWrite`): plan type dropdown, commit date picker, optional fields (qualys_id, active_host_findings_id, jira_vnr, archer_exc)
4. **Edit capability** (if `canWrite`): inline edit on existing plans, submits via PATCH
**State management**: Local component state — plan list, loading, error, form values. No global state needed since the panel is ephemeral.
### Frontend: Atlas Sync Button
A new button in the ReportingPage toolbar, placed adjacent to the existing Ivanti sync button. Uses the same styling pattern — `RefreshCw` icon, monospace uppercase text, sky blue accent color. Differentiated by a `Database` icon prefix and "Atlas" label.
**State**: `atlasSyncing` boolean, `atlasStatus` map (keyed by hostId), `atlasError` string.
### Server.js Integration
```javascript
const createAtlasRouter = require('./routes/atlas');
// ...
app.use('/api/atlas', createAtlasRouter(db, requireAuth));
```
## Data Models
### Atlas Cache Table (`atlas_action_plans_cache`)
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY AUTOINCREMENT | Row ID |
| `host_id` | INTEGER | NOT NULL UNIQUE | Ivanti host ID (= Atlas host_id) |
| `has_action_plan` | INTEGER | NOT NULL DEFAULT 0 | 1 if any plans exist, 0 otherwise |
| `plan_count` | INTEGER | NOT NULL DEFAULT 0 | Number of action plans |
| `plans_json` | TEXT | NOT NULL DEFAULT '[]' | JSON array of plan summaries |
| `synced_at` | DATETIME | NOT NULL DEFAULT CURRENT_TIMESTAMP | Last sync timestamp |
**Index**: `idx_atlas_cache_host_id` on `host_id`
### Plan Summary JSON Shape (stored in `plans_json`)
```json
[
{
"action_plan_id": "ap-123",
"plan_type": "remediation",
"commit_date": "2026-07-01",
"status": "active",
"qualys_id": "QID-12345",
"active_host_findings_id": 2281281250,
"jira_vnr": null,
"archer_exc": null
}
]
```
The exact shape depends on what the Atlas API returns. The backend stores the raw response array as-is, extracting only `plan_count` and `has_action_plan` for the cache columns.
### Atlas API Request/Response Shapes
**Create (PUT `/hosts/{host_id}/action-plans`)**:
```json
{
"plan_type": "remediation",
"commit_date": "2026-07-01",
"active_host_findings_id": 2281281250
}
```
**Update (PATCH `/hosts/{host_id}/action-plans`)**:
```json
{
"action_plan_id": "ap-123",
"updates": {
"commit_date": "2026-08-01"
}
}
```
**Bulk Create (POST `/hosts/create-bulk-action-plans`)**:
```json
{
"host_ids": [29329662, 29329663],
"plan_type": "decommission",
"commit_date": "2026-07-01"
}
```
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `ATLAS_API_URL` | Yes | Atlas InfoSec API base URL |
| `ATLAS_API_USER` | Yes | Service account username for Basic Auth |
| `ATLAS_API_PASS` | Yes | Service account password for Basic Auth |
| `ATLAS_SKIP_TLS` | No | Set to `true` to skip TLS cert verification (default: `false`) |
### Frontend State Shape
```javascript
// Atlas status map — keyed by hostId (number)
const atlasStatusMap = new Map([
[29329662, { host_id: 29329662, has_action_plan: 1, plan_count: 2, synced_at: '2026-07-01 12:00:00' }],
[29329663, { host_id: 29329663, has_action_plan: 0, plan_count: 0, synced_at: '2026-07-01 12:00:00' }],
]);
```
## 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: Basic Auth header round-trip
*For any* pair of (username, password) strings, the Authorization header produced by the Atlas helper should decode (via base64) to exactly `username:password`.
**Validates: Requirements 1.2**
### Property 2: Non-2xx responses resolve with status and body
*For any* HTTP status code in the range 400599 and any response body string, the Atlas helper should resolve the promise with an object containing that exact status code and body, rather than rejecting.
**Validates: Requirements 1.7**
### Property 3: Error messages contain method and path
*For any* HTTP method string and URL path string, when a network error or timeout occurs, the Atlas helper's rejection error message should contain both the method and the path.
**Validates: Requirements 1.8**
### Property 4: Unique host ID extraction
*For any* array of finding objects (each with an optional `hostId` field), the sync operation should extract exactly the set of unique, non-null `hostId` values — no duplicates, no nulls.
**Validates: Requirements 3.2**
### Property 5: Cache upsert derives correct plan_count and has_action_plan
*For any* host ID and any array of action plan objects returned by the Atlas API, after upserting into the cache, the stored `plan_count` should equal the array length and `has_action_plan` should equal 1 if the array is non-empty, 0 otherwise.
**Validates: Requirements 3.4**
### Property 6: Sync response count invariant
*For any* sync operation over N unique hosts where M hosts fail, the response should satisfy: `synced + failed = N` and `withPlans <= synced`.
**Validates: Requirements 3.6**
### Property 7: Status endpoint returns all cached rows with required fields
*For any* set of rows inserted into the Atlas cache table, the `GET /api/atlas/status` endpoint should return exactly that many rows, and each row should contain `host_id`, `has_action_plan`, `plan_count`, and `synced_at` fields.
**Validates: Requirements 4.2, 4.3**
### Property 8: plan_type validation
*For any* string, the PUT endpoint's plan_type validation should accept the string if and only if it is one of: `decommission`, `remediation`, `false_positive`, `risk_acceptance`, `scan_exclusion`.
**Validates: Requirements 5.3**
### Property 9: commit_date validation
*For any* string, the PUT endpoint's commit_date validation should accept the string if and only if it matches the pattern `YYYY-MM-DD` (four digits, hyphen, two digits, hyphen, two digits).
**Validates: Requirements 5.4**
### Property 10: PATCH body validation
*For any* request body object, the PATCH endpoint should accept it if and only if `action_plan_id` is a non-empty string and `updates` is a non-null object.
**Validates: Requirements 5.7**
### Property 11: Bulk request validation
*For any* request body object, the bulk POST endpoint should accept it if and only if `host_ids` is a non-empty array of positive integers, `plan_type` is one of the five valid types, and `commit_date` matches `YYYY-MM-DD`.
**Validates: Requirements 5.10**
### Property 12: Non-2xx Atlas response passthrough
*For any* non-2xx status code and error body returned by the Atlas API, the proxy route should return that same status code and body to the frontend client.
**Validates: Requirements 5.12**
### Property 13: Badge visibility and content
*For any* finding with a `hostId` and any atlas status map, the AtlasBadge should render if and only if the `hostId` exists as a key in the map. When rendered with `plan_count > 0`, the badge text should contain the plan count value.
**Validates: Requirements 6.2, 6.5, 6.6**
### Property 14: Panel displays all plan fields
*For any* action plan object containing `plan_type`, `commit_date`, `status`, and optional reference fields (`jira_vnr`, `archer_exc`), the rendered slide-out panel should include all non-null field values in its output.
**Validates: Requirements 7.3**
## Error Handling
### Atlas API Communication Errors
| Error Scenario | Handling | User Impact |
|----------------|----------|-------------|
| Network timeout (15s/60s) | Helper rejects promise with descriptive error | Sync: host skipped, counted as failed. Proxy: 502 returned to frontend with error message |
| DNS resolution failure | Helper rejects promise | Same as timeout |
| TLS certificate error (when `ATLAS_SKIP_TLS` is false) | Helper rejects promise | Same as timeout |
| Atlas API returns 401 (bad credentials) | Helper resolves with `{ status: 401, body }` | Sync: all hosts fail. Proxy: 401 forwarded to frontend. Error banner shown |
| Atlas API returns 404 (host not found) | Helper resolves with `{ status: 404, body }` | Sync: host skipped. Proxy: 404 forwarded to frontend |
| Atlas API returns 422 (validation error) | Helper resolves with `{ status: 422, body }` | Proxy: 422 forwarded to frontend. Panel shows validation error |
| Atlas API returns 500 (server error) | Helper resolves with `{ status: 500, body }` | Sync: host skipped. Proxy: 500 forwarded to frontend |
### Backend Validation Errors
| Error Scenario | HTTP Status | Response |
|----------------|-------------|----------|
| Missing or invalid `plan_type` | 400 | `{ error: 'plan_type must be one of: decommission, remediation, ...' }` |
| Missing or invalid `commit_date` | 400 | `{ error: 'commit_date must be a valid YYYY-MM-DD date string' }` |
| Missing `action_plan_id` on PATCH | 400 | `{ error: 'action_plan_id is required and must be a non-empty string' }` |
| Missing `updates` on PATCH | 400 | `{ error: 'updates is required and must be an object' }` |
| Empty or invalid `host_ids` on bulk | 400 | `{ error: 'host_ids must be a non-empty array of positive integers' }` |
| Non-integer `hostId` URL param | 400 | `{ error: 'hostId must be a positive integer' }` |
| Unauthenticated request | 401 | `{ error: 'Authentication required' }` |
| Viewer group on restricted endpoint | 403 | `{ error: 'Insufficient permissions', required: [...], current: 'Viewer' }` |
### Frontend Error Handling
- **Sync failure**: Error banner displayed below the Atlas sync button (matching existing Ivanti sync error pattern). Button re-enabled.
- **Panel fetch failure**: "Failed to load action plans" message inside the panel with a retry button.
- **Create/update failure**: Error message displayed inline in the form, preserving user input for correction.
- **Network error**: Generic "Unable to reach server" message with retry option.
### Environment Configuration Errors
- If `ATLAS_API_URL`, `ATLAS_API_USER`, or `ATLAS_API_PASS` are not set, the Atlas helper logs a warning at module load time. All Atlas API calls will fail with a descriptive error rather than crashing the server.
- The Atlas router checks for helper availability and returns 503 if Atlas is not configured.
## Testing Strategy
### Unit Tests
Unit tests cover specific examples, edge cases, and integration points:
**Atlas Helper (`atlasApi.js`)**:
- Correct URL construction from base URL + path
- Basic Auth header format for known credentials
- TLS skip flag respected (rejectUnauthorized option)
- Timeout values for single vs bulk endpoints
- GET, PUT, PATCH, POST methods set correctly
**Atlas Router validation**:
- Valid plan_type values accepted, invalid rejected
- Valid commit_date formats accepted, invalid rejected
- PATCH body with missing action_plan_id rejected
- Bulk request with empty host_ids rejected
- Non-integer hostId param rejected
- Auth middleware applied to correct endpoints (401/403 responses)
**Atlas Cache operations**:
- Upsert creates new row when host_id doesn't exist
- Upsert updates existing row when host_id exists
- Status endpoint returns empty array when cache is empty
- Migration is idempotent (runs twice without error)
**Frontend components**:
- AtlasBadge renders nothing when host not in status map
- AtlasBadge renders warning style when plan_count is 0
- AtlasBadge renders success style when plan_count > 0
- AtlasSlideOutPanel shows create form for Admin/Standard_User
- AtlasSlideOutPanel hides create form for Viewer
- Sync button disabled during sync, re-enabled after
### Property-Based Tests
Property-based tests verify universal properties across generated inputs. Each test runs a minimum of 100 iterations.
**Library**: [fast-check](https://github.com/dubzzz/fast-check) — the standard PBT library for JavaScript/Node.js.
**Configuration**: Each property test runs with `{ numRuns: 100 }` minimum.
**Tag format**: Each test includes a comment referencing its design property:
```javascript
// Feature: atlas-action-plans, Property 1: Basic Auth header round-trip
```
| Property | Test Description | Generator Strategy |
|----------|-----------------|-------------------|
| Property 1 | Encode then decode Basic Auth header | Generate random (user, pass) string pairs, verify round-trip |
| Property 2 | Non-2xx status codes resolve | Generate integers 400599 and random body strings |
| Property 3 | Error messages contain method and path | Generate random method names and URL path strings |
| Property 4 | Unique host ID extraction | Generate arrays of objects with optional numeric hostId fields |
| Property 5 | Cache upsert correctness | Generate (hostId, planArray) pairs, verify derived fields |
| Property 6 | Sync count invariant | Generate (totalHosts, failureCount) pairs, verify arithmetic |
| Property 7 | Status returns all cached rows | Generate N cache rows, verify response count and fields |
| Property 8 | plan_type validation | Generate random strings, verify acceptance matches valid set |
| Property 9 | commit_date validation | Generate random strings, verify acceptance matches date pattern |
| Property 10 | PATCH body validation | Generate random objects with varying field presence |
| Property 11 | Bulk validation | Generate objects with varying host_ids, plan_type, commit_date |
| Property 12 | Error passthrough | Generate non-2xx codes and body strings, verify forwarding |
| Property 13 | Badge visibility and content | Generate findings and status maps, verify render logic |
| Property 14 | Panel plan field display | Generate plan objects, verify all non-null fields appear |
### Integration Tests
Integration tests verify end-to-end behavior with mocked Atlas API:
- Full sync flow: populate Ivanti cache → trigger sync → verify Atlas cache populated
- Create plan flow: send PUT → verify Atlas API called → verify audit logged
- Update plan flow: send PATCH → verify Atlas API called → verify audit logged
- Bulk create flow: send POST → verify Atlas API called with correct body
- Error resilience: mix of successful and failing hosts during sync
- Auth enforcement: verify 401/403 for each endpoint with wrong credentials/group