# 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: ```mermaid 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 ```javascript { 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 ```javascript { values: Map, // variable name → entered value skippedGroups: Set, // groups the user declined existingBackend: Map, // parsed from existing backend/.env existingFrontend: Map, // 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 ```javascript 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 `:-`. ```javascript 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](https://github.com/dubzzz/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: ```bash cd backend npx jest __tests__/config-wizard*.test.js ``` Property tests use `fast-check` as the generator library within Jest test cases.