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

23 KiB
Raw Blame History

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

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.

// 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.

// 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

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 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 — 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 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