Files
cve-dashboard/.kiro/specs/config-wizard/design.md
2026-05-13 14:22:04 -06:00

20 KiB

Design Document: Config Wizard

Overview

The Config Wizard is a single-file Node.js CLI script (configure.js) at the project root that interactively guides deployers through configuring all environment variables for the CVE Dashboard. It replaces the manual process of copying .env.example and editing values by hand.

The wizard uses only the Node.js built-in readline module — no external dependencies. It writes two output files: backend/.env and frontend/.env. It derives smart defaults by parsing docker-compose.yml for Postgres credentials and propagates the backend PORT into frontend URL defaults.

Key design goals:

  • Zero dependencies beyond Node.js 18+ standard library
  • Single file implementation for easy maintenance
  • Idempotent re-runs that preserve existing values and unmanaged variables
  • Group-based flow with skip capability for unused integrations

Architecture

The wizard follows a linear pipeline architecture:

flowchart TD
    A[Start: node configure.js] --> B[Validate project root]
    B --> C[Parse existing .env files]
    C --> D[Parse docker-compose.yml for defaults]
    D --> E[Display welcome message]
    E --> F[Group loop: prompt variables]
    F --> G[Display summary]
    G --> H{User confirms?}
    H -->|Yes| I[Handle existing file backup/overwrite]
    I --> J[Write backend/.env]
    J --> K[Write frontend/.env]
    K --> L[Display success + next steps]
    H -->|No| M{Restart or exit?}
    M -->|Restart| E
    M -->|Exit| N[Exit code 1]
    B -->|Missing dirs| O[Error + exit code 1]

Execution Flow

  1. Validation — Confirm backend/ and frontend/ directories exist relative to CWD
  2. Parse existing — Read current backend/.env and frontend/.env if they exist, extract key-value pairs
  3. Parse docker-compose — Extract Postgres credentials from docker-compose.yml using line-by-line parsing
  4. Welcome — Display purpose and instructions
  5. Group loop — Iterate through variable groups in order, prompting for each variable
  6. Summary — Display all configured values (sensitive ones masked) and target file paths
  7. Confirmation — User approves or rejects; rejection offers restart or exit
  8. Write — Generate env file content and write to disk with backup handling

Signal Handling

A SIGINT handler (Ctrl+C) is registered at startup. When triggered:

  • Close the readline interface
  • Print a cancellation message
  • Exit with code 1
  • No files are written regardless of progress

Components and Interfaces

Module Structure

All code lives in a single configure.js file organized into these logical sections:

configure.js
├── Constants & Configuration
│   ├── VARIABLE_DESCRIPTORS[]     — metadata for all managed variables
│   ├── GROUP_ORDER[]              — ordered list of group names
│   ├── GROUP_DESCRIPTIONS{}       — one-line description per group
│   └── SENSITIVE_VARS[]           — list of variable names to mask
├── Parsing Functions
│   ├── parseEnvFile(filePath)     — read existing .env into key-value map
│   ├── parseDockerCompose(filePath) — extract Postgres config
│   └── resolveShellDefault(str)   — extract default from ${VAR:-default} syntax
├── Validation Functions
│   ├── validatePort(value)
│   ├── validateCorsOrigins(value)
│   ├── validateDatabaseUrl(value)
│   ├── validateSessionSecret(value)
│   └── validateRequired(value)
├── Display Functions
│   ├── printWelcome()
│   ├── printGroupHeader(group)
│   ├── printSummary(config, skippedGroups)
│   └── maskSensitive(name, value)
├── Prompt Functions
│   ├── promptVariable(rl, descriptor, currentValue)
│   ├── promptYesNo(rl, question, defaultNo)
│   └── promptOverwrite(rl, filePath)
├── File Writing
│   ├── generateEnvContent(variables, groups)
│   ├── writeEnvFile(filePath, content)
│   └── createBackup(filePath)
└── Main Flow
    └── main()

Key Interfaces

Variable Descriptor

{
  name: String,           // e.g. "PORT"
  group: String,          // e.g. "Core Settings"
  required: Boolean,      // true = must have a value
  default: String|null,   // factory default value
  description: String,    // max 120 chars
  docUrl: String|null,    // URL for obtaining the value
  sensitive: Boolean,     // true = mask in display
  validator: String|null  // name of validation function to apply
}

Parsed Config State

{
  values: Map<String, String>,       // variable name → entered value
  skippedGroups: Set<String>,        // groups the user declined
  existingBackend: Map<String, String>,  // parsed from existing backend/.env
  existingFrontend: Map<String, String>, // parsed from existing frontend/.env
  unmanagedBackend: String[],        // lines not matching managed vars
  unmanagedFrontend: String[],       // lines not matching managed vars
  derivedDefaults: {                 // computed from docker-compose/port
    DATABASE_URL: String|null,
    databaseUrlSource: 'compose'|'fallback',
    REACT_APP_API_BASE: String|null,
    CORS_ORIGINS: String|null
  }
}

parseEnvFile(filePath) → { managed: Map, unmanaged: String[] }

Reads a .env file line by line. For each non-empty, non-comment line, splits on the first = character. If the key matches a managed variable, stores it in managed. Otherwise, preserves the raw line (including preceding comments) in unmanaged.

parseDockerCompose(filePath) → { user, password, database, port } | null

Line-by-line parser that extracts:

  • POSTGRES_USER from POSTGRES_USER: value or POSTGRES_USER: ${VAR:-default}
  • POSTGRES_PASSWORD from POSTGRES_PASSWORD: ${VAR:-default} (resolves the default)
  • POSTGRES_DB from POSTGRES_DB: value
  • Host port from - "host:container" under the ports: key

Returns null if the file doesn't exist or can't be parsed.

generateEnvContent(variables, groupOrder, groupDescriptions) → String

Produces the final .env file content:

  • Group header comments (# --- Group Name ---)
  • KEY=value lines, with values containing spaces/#/quotes wrapped in double quotes
  • Omits optional variables with no value
  • Appends unmanaged variables in a # Custom Variables section

Data Models

Variable Descriptor Registry

The complete list of managed variables with their metadata:

Name Group Required Default Sensitive Validator
PORT Core Settings yes 3001 no validatePort
API_HOST Core Settings yes localhost no
CORS_ORIGINS Core Settings yes (derived) no validateCorsOrigins
DATABASE_URL Database yes (derived) yes validateDatabaseUrl
SESSION_SECRET Session yes yes validateSessionSecret
NVD_API_KEY NVD API no yes
IVANTI_API_KEY Ivanti Integration no yes
IVANTI_CLIENT_ID Ivanti Integration no 1550 no
IVANTI_FIRST_NAME Ivanti Integration no no
IVANTI_LAST_NAME Ivanti Integration no no
IVANTI_BU_FILTER Ivanti Integration no NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM no
IVANTI_MANAGED_BUS Ivanti Integration no NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM no
IVANTI_SKIP_TLS Ivanti Integration no false no
ATLAS_API_URL Atlas Integration no no
ATLAS_API_USER Atlas Integration no no
ATLAS_API_PASS Atlas Integration no yes
ATLAS_SKIP_TLS Atlas Integration no false no
JIRA_BASE_URL Jira Integration no no
JIRA_AUTH_METHOD Jira Integration no basic no
JIRA_API_USER Jira Integration no no
JIRA_API_TOKEN Jira Integration no yes
JIRA_PAT Jira Integration no yes
JIRA_PROJECT_KEY Jira Integration no no
JIRA_ISSUE_TYPE Jira Integration no Task no
JIRA_SKIP_TLS Jira Integration no false no
CARD_API_URL CARD Integration no no
CARD_API_USER CARD Integration no no
CARD_API_PASS CARD Integration no yes
CARD_SKIP_TLS CARD Integration no false no
GITLAB_URL GitLab Integration no http://steam-gitlab.charterlab.com no
GITLAB_PROJECT_ID GitLab Integration no no
GITLAB_PAT GitLab Integration no yes
REACT_APP_API_BASE Frontend Settings yes (derived) no
REACT_APP_API_HOST Frontend Settings yes (derived) no

Group Order and Descriptions

const GROUP_ORDER = [
  'Core Settings',
  'Database',
  'Session',
  'NVD API',
  'Ivanti Integration',
  'Atlas Integration',
  'Jira Integration',
  'CARD Integration',
  'GitLab Integration',
  'Frontend Settings'
];

const GROUP_DESCRIPTIONS = {
  'Core Settings': 'Server port, hostname, and CORS configuration',
  'Database': 'PostgreSQL connection string for persistent storage',
  'Session': 'Secret key for signing session cookies',
  'NVD API': 'National Vulnerability Database API key for CVE lookups',
  'Ivanti Integration': 'RiskSense platform credentials for vulnerability sync',
  'Atlas Integration': 'Atlas InfoSec API for action plan management',
  'Jira Integration': 'Jira Data Center for ticket creation and tracking',
  'CARD Integration': 'CARD asset ownership API for host lookups',
  'GitLab Integration': 'GitLab API for feedback submission (bug reports)',
  'Frontend Settings': 'React app API endpoint configuration'
};

Optional (Skippable) Groups

Groups that present a skip prompt before entering: NVD API, Ivanti Integration, Atlas Integration, Jira Integration, CARD Integration, GitLab Integration.

Core Settings, Database, Session, and Frontend Settings are always prompted.

Docker-Compose Parsing Approach

The parser reads docker-compose.yml line by line without a full YAML parser. It uses a simple state machine:

  1. Find service — Look for a line matching postgres: (indented under services:)
  2. Find environment — Within the postgres service block, look for environment:
  3. Extract vars — Read POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD values
  4. Find ports — Look for ports: within the postgres service block
  5. Extract host port — Parse - "host:container" to get the host port (left side)

Shell variable substitution (${VAR:-default}) is resolved by extracting the default value after :-.

function resolveShellDefault(value) {
  const match = value.match(/\$\{[^:}]+:-([^}]+)\}/);
  return match ? match[1] : value;
}

If parsing fails at any step, the function returns null and the caller falls back to the hardcoded default: postgresql://steam:sV4xmC9xAUCFop0ypxMVS056QgPqGrX@localhost:5433/cve_dashboard.

Env File Output Format

# --- Core Settings ---
PORT=3001
API_HOST=localhost
CORS_ORIGINS=http://localhost:3000

# --- Database ---
DATABASE_URL=postgresql://steam:sV4xmC9xAUCFop0ypxMVS056QgPqGrX@localhost:5433/cve_dashboard

# --- Session ---
SESSION_SECRET=my-very-long-secret-key-here

# --- Custom Variables ---
MY_CUSTOM_VAR=preserved_value

Values containing spaces, #, or quote characters are wrapped in double quotes:

SOME_VAR="value with spaces"
ANOTHER_VAR="value#with#hashes"

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: Descriptor registry invariants

For any variable descriptor in the registry, the variable name must appear exactly once across all groups, and the group description must be non-empty and at most 120 characters long.

Validates: Requirements 2.3, 2.5

Property 2: Variable ordering within groups

For any Variable_Group, all required variables in that group must appear before all optional variables in the descriptor list ordering.

Validates: Requirements 2.4

Property 3: Group presentation order

For any two consecutive variables in the descriptor list, the first variable's group index in GROUP_ORDER must be less than or equal to the second variable's group index.

Validates: Requirements 2.1

Property 4: Sensitive value masking

For any string value longer than 8 characters, maskSensitive should return a string showing only the first 4 and last 4 characters with asterisks in between. For any string value of 8 characters or fewer, maskSensitive should return the full value unchanged.

Validates: Requirements 3.4

Property 5: Shell variable default resolution

For any string containing the pattern ${VARNAME:-defaultvalue}, resolveShellDefault should extract and return defaultvalue. For any string not containing that pattern, it should return the original string.

Validates: Requirements 4.1

Property 6: DATABASE_URL construction

For any valid Postgres credentials tuple (user, password, port, database) where port is an integer in [1, 65535], the constructed DATABASE_URL should equal postgresql://{user}:{password}@localhost:{port}/{database}.

Validates: Requirements 4.2

Property 7: Derived URL defaults from PORT

For any valid port number P, the derived REACT_APP_API_BASE should equal http://localhost:{P}/api, REACT_APP_API_HOST should equal http://localhost:{P}, and CORS_ORIGINS should equal http://localhost:3000.

Validates: Requirements 4.6

Property 8: Port validation

For any string input, validatePort should return true if and only if the trimmed value is a string representation of an integer in the range [1, 65535].

Validates: Requirements 5.2

Property 9: CORS origins validation

For any comma-separated string, validateCorsOrigins should return true if and only if every trimmed entry starts with http:// or https://.

Validates: Requirements 5.3

Property 10: DATABASE_URL validation

For any string, validateDatabaseUrl should return true if and only if the string starts with postgresql:// or equals the literal string sqlite.

Validates: Requirements 5.4

Property 11: SESSION_SECRET validation

For any string, validateSessionSecret should return true if and only if its length is at least 16 characters.

Validates: Requirements 5.6

Property 12: Required variable rejection of whitespace

For any string composed entirely of whitespace characters (including empty string), validateRequired should return false.

Validates: Requirements 5.1

Property 13: Env value quoting

For any key-value pair, generateEnvContent should wrap the value in double quotes if and only if the value contains a space, #, or quote character. Values without those characters should appear unquoted.

Validates: Requirements 6.3

Property 14: Optional variable omission

For any optional variable descriptor with no user-provided value and no default value, generateEnvContent should not include a line for that variable in the output.

Validates: Requirements 6.4

Property 15: Skipped group exclusion

For any Variable_Group that the user declines, the generated env file content should contain no KEY=value lines for any variable belonging to that group.

Validates: Requirements 7.2, 7.3

Property 16: Env file round-trip parsing

For any valid env file content produced by generateEnvContent, parsing it with parseEnvFile should recover all the original key-value pairs for managed variables.

Validates: Requirements 9.1, 9.2

Property 17: Unmanaged variable preservation

For any existing env file containing lines with keys not in the managed variable list, those lines should appear unchanged in the "Custom Variables" section of the generated output.

Validates: Requirements 9.4

Property 18: Managed key deduplication

For any managed variable name that appears both in the wizard-entered values and in the unmanaged lines parsed from an existing file, the generated output should contain exactly one occurrence of that key, using the wizard-entered value.

Validates: Requirements 9.5

Error Handling

Categories

Error Type Handling Strategy
Missing project structure Print error to stderr, exit code 1
SIGINT (Ctrl+C) Close readline, print cancellation, exit code 1, no files written
Invalid user input Display format error, re-prompt same variable
Docker-compose parse failure Log warning, fall back to hardcoded defaults
Existing env file unreadable Log warning, use factory defaults, preserve file as backup
File write failure Print error with path and reason, exit immediately without writing remaining files
Overwrite declined Skip that file, continue with other files

Error Messages

All error messages follow the pattern: Error: {what went wrong}. {what to do about it}.

Examples:

  • Error: This script must be run from the project root (backend/ and frontend/ directories not found). Run from the directory containing both folders.
  • Error: PORT must be an integer between 1 and 65535.
  • Error: Could not write to backend/.env (Permission denied). Check file permissions and try again.

Graceful Degradation

The wizard degrades gracefully when optional infrastructure is missing:

  • No docker-compose.yml → uses hardcoded DATABASE_URL default
  • Malformed docker-compose.yml → warns and uses hardcoded default
  • Existing .env unreadable → warns, backs up, uses factory defaults
  • No existing .env files → normal first-run behavior

Testing Strategy

Unit Tests

Unit tests cover the pure functions in isolation:

  • resolveShellDefault() — various ${VAR:-default} patterns
  • parseDockerCompose() — valid compose files, missing files, malformed content
  • parseEnvFile() — standard files, quoted values, comments, empty lines, malformed lines
  • validatePort() — boundary values (0, 1, 65535, 65536), non-numeric, floats
  • validateCorsOrigins() — single/multiple origins, invalid schemes, whitespace
  • validateDatabaseUrl() — postgresql://, sqlite, invalid prefixes
  • validateSessionSecret() — boundary at 15/16 characters
  • validateRequired() — empty, whitespace-only, valid values
  • maskSensitive() — short values (≤8), long values, exact boundary (8, 9 chars)
  • generateEnvContent() — quoting rules, group headers, omission of empty optionals

Property-Based Tests

Property-based tests use fast-check to verify universal properties across generated inputs. Each property test runs a minimum of 100 iterations.

Tests are tagged with: Feature: config-wizard, Property {N}: {title}

Properties to implement:

  1. Descriptor registry invariants (uniqueness, description length)
  2. Variable ordering within groups (required before optional)
  3. Group presentation order (monotonic group index)
  4. Sensitive value masking (length-based behavior)
  5. Shell variable default resolution (pattern extraction)
  6. DATABASE_URL construction (format correctness)
  7. Derived URL defaults from PORT (format correctness)
  8. Port validation (range check)
  9. CORS origins validation (scheme check)
  10. DATABASE_URL validation (prefix check)
  11. SESSION_SECRET validation (length check)
  12. Required variable rejection of whitespace
  13. Env value quoting (conditional wrapping)
  14. Optional variable omission
  15. Skipped group exclusion
  16. Env file round-trip parsing
  17. Unmanaged variable preservation
  18. Managed key deduplication

Integration Tests

Integration tests verify end-to-end flows using a temporary directory:

  • Full wizard run with all defaults accepted → correct files written
  • Wizard run with existing .env files → values pre-filled correctly
  • Wizard run with skipped groups → those groups absent from output
  • SIGINT at various points → no files written
  • Missing project structure → error exit
  • File write permission error → graceful failure

Test Runner

Tests use Jest (already configured in the project via react-scripts test for frontend). Backend tests run with:

cd backend
npx jest __tests__/config-wizard*.test.js

Property tests use fast-check as the generator library within Jest test cases.