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
- Validation — Confirm
backend/andfrontend/directories exist relative to CWD - Parse existing — Read current
backend/.envandfrontend/.envif they exist, extract key-value pairs - Parse docker-compose — Extract Postgres credentials from
docker-compose.ymlusing line-by-line parsing - Welcome — Display purpose and instructions
- Group loop — Iterate through variable groups in order, prompting for each variable
- Summary — Display all configured values (sensitive ones masked) and target file paths
- Confirmation — User approves or rejects; rejection offers restart or exit
- 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_USERfromPOSTGRES_USER: valueorPOSTGRES_USER: ${VAR:-default}POSTGRES_PASSWORDfromPOSTGRES_PASSWORD: ${VAR:-default}(resolves the default)POSTGRES_DBfromPOSTGRES_DB: value- Host port from
- "host:container"under theports: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=valuelines, with values containing spaces/#/quotes wrapped in double quotes- Omits optional variables with no value
- Appends unmanaged variables in a
# Custom Variablessection
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:
- Find service — Look for a line matching
postgres:(indented underservices:) - Find environment — Within the postgres service block, look for
environment: - Extract vars — Read
POSTGRES_DB,POSTGRES_USER,POSTGRES_PASSWORDvalues - Find ports — Look for
ports:within the postgres service block - 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
.envunreadable → warns, backs up, uses factory defaults - No existing
.envfiles → normal first-run behavior
Testing Strategy
Unit Tests
Unit tests cover the pure functions in isolation:
resolveShellDefault()— various${VAR:-default}patternsparseDockerCompose()— valid compose files, missing files, malformed contentparseEnvFile()— standard files, quoted values, comments, empty lines, malformed linesvalidatePort()— boundary values (0, 1, 65535, 65536), non-numeric, floatsvalidateCorsOrigins()— single/multiple origins, invalid schemes, whitespacevalidateDatabaseUrl()— postgresql://, sqlite, invalid prefixesvalidateSessionSecret()— boundary at 15/16 charactersvalidateRequired()— empty, whitespace-only, valid valuesmaskSensitive()— 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:
- Descriptor registry invariants (uniqueness, description length)
- Variable ordering within groups (required before optional)
- Group presentation order (monotonic group index)
- Sensitive value masking (length-based behavior)
- Shell variable default resolution (pattern extraction)
- DATABASE_URL construction (format correctness)
- Derived URL defaults from PORT (format correctness)
- Port validation (range check)
- CORS origins validation (scheme check)
- DATABASE_URL validation (prefix check)
- SESSION_SECRET validation (length check)
- Required variable rejection of whitespace
- Env value quoting (conditional wrapping)
- Optional variable omission
- Skipped group exclusion
- Env file round-trip parsing
- Unmanaged variable preservation
- 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
.envfiles → 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.