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
23 KiB
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.hostIdmaps directly to Atlashost_idin URL paths. Ivantif.idmaps to Atlasactive_host_findings_idin request bodies. No translation layer is needed.
Architecture
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
- ReportingPage mounts, fetches Ivanti findings from existing cache (existing behavior)
- ReportingPage fetches
GET /api/atlas/status— returns all cached Atlas rows - Frontend builds a
Map<hostId, atlasStatus>and passes it to table rendering - Each Host column cell checks the map — if a match exists, renders an AtlasBadge
Data Flow: Manual Sync
- User clicks Atlas sync button
- Frontend sends
POST /api/atlas/sync - Backend extracts unique
hostIdvalues fromivanti_findings_cache.findings_json - Backend calls
GET /hosts/{host_id}/action-plansfor each host (with concurrency limit of 5) - Backend upserts each result into
atlas_action_plans_cache - Backend returns summary
{ synced, withPlans, failed } - Frontend re-fetches
GET /api/atlas/statusand updates badges
Data Flow: Create/Update Plan
- User clicks AtlasBadge → slide-out panel opens
- Panel fetches
GET /api/atlas/hosts/:hostId/action-plansfor live data - User fills create form or edits existing plan
- Frontend sends
PUT(create) orPATCH(update) to/api/atlas/hosts/:hostId/action-plans - Backend validates request body, proxies to Atlas API, logs audit entry
- 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.
// 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 usernameATLAS_API_PASS— service account passwordATLAS_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.
// 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:
- Parse
ivanti_findings_cache.findings_jsonto extract uniquehostIdvalues (skip nulls) - Process hosts in batches of 5 concurrent requests using
Promise.allSettled - For each host, call
atlasGet('/hosts/' + hostId + '/action-plans') - On 2xx: upsert cache row with plan count and summary JSON
- On non-2xx: increment failure counter, log warning, continue
- Return
{ synced: N, withPlans: N, failed: N }
Validation (PUT create):
plan_typemust be one of:decommission,remediation,false_positive,risk_acceptance,scan_exclusioncommit_datemust match/^\d{4}-\d{2}-\d{2}$/hostIdparam must be a positive integer
Validation (PATCH update):
action_plan_idmust be a non-empty stringupdatesmust be a non-null object
Validation (POST bulk):
host_idsmust be a non-empty array of positive integersplan_typeandcommit_datevalidated 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
atlasStatusisundefined(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:
- Header: hostname, host ID, close button
- Plan list: fetched from
GET /api/atlas/hosts/:hostId/action-planson open. Each plan shows type, commit date, status, and optional VNR/EXC references - Create form (if
canWrite): plan type dropdown, commit date picker, optional fields (qualys_id, active_host_findings_id, jira_vnr, archer_exc) - 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
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)
[
{
"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):
{
"plan_type": "remediation",
"commit_date": "2026-07-01",
"active_host_findings_id": 2281281250
}
Update (PATCH /hosts/{host_id}/action-plans):
{
"action_plan_id": "ap-123",
"updates": {
"commit_date": "2026-08-01"
}
}
Bulk Create (POST /hosts/create-bulk-action-plans):
{
"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
// 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 400–599 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, orATLAS_API_PASSare 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 — 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:
// 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 400–599 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