diff --git a/.kiro/specs/config-wizard/.config.kiro b/.kiro/specs/config-wizard/.config.kiro new file mode 100644 index 0000000..3f3e047 --- /dev/null +++ b/.kiro/specs/config-wizard/.config.kiro @@ -0,0 +1 @@ +{"specId": "2a2bf0a4-e548-4777-af9a-0c5e22dd227e", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/config-wizard/design.md b/.kiro/specs/config-wizard/design.md new file mode 100644 index 0000000..ea535a0 --- /dev/null +++ b/.kiro/specs/config-wizard/design.md @@ -0,0 +1,486 @@ +# 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. + diff --git a/.kiro/specs/config-wizard/requirements.md b/.kiro/specs/config-wizard/requirements.md new file mode 100644 index 0000000..ac5f1c8 --- /dev/null +++ b/.kiro/specs/config-wizard/requirements.md @@ -0,0 +1,132 @@ +# Requirements Document + +## Introduction + +The CVE Dashboard requires a number of environment variables to be configured before the backend and frontend can operate correctly. Currently, users must manually create `.env` files by referencing `.env.example` and inline code comments, which is error-prone and provides no validation feedback. This feature introduces an interactive CLI configuration wizard that guides users through entering all required and optional environment variables at deployment time, providing descriptions, documentation references, default values, and input validation. + +## Glossary + +- **Config_Wizard**: A Node.js CLI script that interactively prompts the user to enter environment variable values and writes the resulting configuration to `.env` files +- **Variable_Group**: A logical grouping of related environment variables presented together during the wizard flow (e.g., "Core Settings", "Ivanti Integration", "Jira Integration") +- **Required_Variable**: An environment variable that must have a non-empty value for the application to start or for a core feature to function +- **Optional_Variable**: An environment variable that enhances functionality but is not required for the application to start +- **Variable_Descriptor**: The metadata associated with each environment variable, including its name, description, default value, required/optional status, and documentation reference +- **Backend_Env_File**: The file at `backend/.env` containing backend server configuration +- **Frontend_Env_File**: The file at `frontend/.env` containing frontend build-time configuration +- **Validation_Rule**: A check applied to user input for a variable to verify it meets format or content requirements before writing to the env file +- **Deploy_Script**: The shell script at `scripts/deploy-postgres.sh` that provisions the Docker Postgres container with known credentials and port mappings +- **Docker_Compose_Config**: The `docker-compose.yml` file at the project root that defines the Postgres container name, user, password, database, and port mapping + +## Requirements + +### Requirement 1: Interactive CLI Execution + +**User Story:** As a deployer, I want to run a single command to start the configuration wizard, so that I can set up the dashboard without manually editing env files. + +#### Acceptance Criteria + +1. THE Config_Wizard SHALL be executable via `node configure.js` from the project root directory +2. WHEN the Config_Wizard starts, THE Config_Wizard SHALL display a welcome message explaining its purpose and that it will guide the user through configuring backend and frontend environment variables +3. THE Config_Wizard SHALL use Node.js built-in `readline` module for interactive prompts without requiring additional npm dependencies +4. WHEN the user presses Ctrl+C at any prompt, THE Config_Wizard SHALL exit without writing any configuration files, display a cancellation message to stdout, and terminate with exit code 1 +5. IF the Config_Wizard is executed from a directory that does not contain both a `backend/` and a `frontend/` subdirectory, THEN THE Config_Wizard SHALL display an error message indicating it must be run from the project root and terminate with exit code 1 +6. WHEN the Config_Wizard completes successfully after writing all env files, THE Config_Wizard SHALL terminate with exit code 0 + +### Requirement 2: Variable Grouping and Ordering + +**User Story:** As a deployer, I want variables presented in logical groups, so that I can understand which variables relate to which integration and configure them together. + +#### Acceptance Criteria + +1. THE Config_Wizard SHALL present backend variables organized into the following Variable_Groups in order: Core Settings, Database, Session, NVD API, Ivanti Integration, Atlas Integration, Jira Integration, CARD Integration, GitLab Integration +2. THE Config_Wizard SHALL present frontend variables as a separate Variable_Group named "Frontend Settings" after all backend groups +3. WHEN entering a new Variable_Group, THE Config_Wizard SHALL display the group name as a section header followed by a one-sentence description (maximum 120 characters) of what the group configures +4. THE Config_Wizard SHALL present Required_Variables before Optional_Variables within each Variable_Group +5. THE Config_Wizard SHALL assign each environment variable to exactly one Variable_Group such that no variable appears in more than one group during the wizard flow + +### Requirement 3: Variable Descriptions and Documentation References + +**User Story:** As a deployer, I want each variable to have a short description and a pointer to further documentation, so that I can understand what each variable controls without leaving the terminal. + +#### Acceptance Criteria + +1. WHEN prompting for a variable, THE Config_Wizard SHALL display the following elements in order: the "[REQUIRED]" or "[OPTIONAL]" label, the variable name, a description of no more than 120 characters explaining what the variable controls, and the default value if one exists +2. WHEN a variable has a default value, THE Config_Wizard SHALL display the default value in the prompt enclosed in parentheses and accept an empty input to use that default +3. WHERE a variable belongs to an external-service Variable_Group (NVD API, Ivanti Integration, Atlas Integration, Jira Integration, CARD Integration, or GitLab Integration), THE Config_Wizard SHALL display a documentation URL or instruction for obtaining the value alongside the prompt +4. WHEN a variable is classified as sensitive (containing passwords, secrets, API keys, or tokens), THE Config_Wizard SHALL mask the default value display by showing only the first 4 and last 4 characters with asterisks in between, or show the full value if it is 8 characters or fewer +5. IF a variable has both a default value and a documentation reference, THEN THE Config_Wizard SHALL display both the default value and the documentation reference in the same prompt + +### Requirement 4: Deploy-Aware Defaults + +**User Story:** As a deployer, I want the wizard to automatically derive sensible defaults from the deploy infrastructure (docker-compose.yml, deploy scripts), so that I do not need to manually construct values like DATABASE_URL. + +#### Acceptance Criteria + +1. WHEN the Docker_Compose_Config file exists at the project root, THE Config_Wizard SHALL parse it to extract the Postgres user, password, database name, and host port mapping, resolving any shell variable substitution syntax (`${VAR:-default}`) by using the specified default value +2. WHEN Postgres credentials are available from Docker_Compose_Config, THE Config_Wizard SHALL construct a default DATABASE_URL in the format `postgresql://user:password@localhost:port/database` using the host port from the port mapping (the first value in a `"host:container"` pair) and present it as the default value +3. IF the Docker_Compose_Config file exists but does not contain a Postgres service definition or cannot be parsed, THEN THE Config_Wizard SHALL fall back to the hardcoded default from the Deploy_Script (`postgresql://steam:sV4xmC9xAUCFop0ypxMVS056QgPqGrX@localhost:5433/cve_dashboard`) and display a message indicating that the compose file could not be used +4. WHEN the Docker_Compose_Config file does not exist, THE Config_Wizard SHALL fall back to the hardcoded default from the Deploy_Script (`postgresql://steam:sV4xmC9xAUCFop0ypxMVS056QgPqGrX@localhost:5433/cve_dashboard`) +5. WHEN presenting the DATABASE_URL prompt, THE Config_Wizard SHALL display a message indicating whether the default was auto-derived from Docker_Compose_Config or from the Deploy_Script fallback, and allow the user to override it +6. WHEN the user confirms a PORT value for the backend (whether accepting the default of 3001 or providing a custom value), THE Config_Wizard SHALL use that PORT value to construct the default REACT_APP_API_BASE as `http://localhost:{PORT}/api` and use the hardcoded frontend port 3000 to construct the default CORS_ORIGINS as `http://localhost:3000` for subsequent prompts + +### Requirement 5: Input Validation + +**User Story:** As a deployer, I want the wizard to validate my input before saving, so that I do not end up with a broken configuration due to typos or formatting errors. + +#### Acceptance Criteria + +1. WHEN the user provides an empty or whitespace-only value for a Required_Variable that has no default, THE Config_Wizard SHALL display an error message indicating the variable is required and re-prompt for the value +2. WHEN the user provides a PORT value, THE Config_Wizard SHALL trim leading and trailing whitespace, then validate that the trimmed value is an integer between 1 and 65535 +3. WHEN the user provides a CORS_ORIGINS value, THE Config_Wizard SHALL trim whitespace from each comma-separated entry and validate that each trimmed entry starts with `http://` or `https://` +4. WHEN the user provides a DATABASE_URL value that overrides the derived default, THE Config_Wizard SHALL validate that it starts with `postgresql://` or is the literal string `sqlite` (for SQLite mode) +5. IF validation fails for any input, THEN THE Config_Wizard SHALL display an error message indicating the expected format and re-prompt for the same variable without losing previously entered values, repeating until the user provides valid input or exits via Ctrl+C +6. WHEN the user provides a SESSION_SECRET value, THE Config_Wizard SHALL validate that it is at least 16 characters long + +### Requirement 6: Env File Generation + +**User Story:** As a deployer, I want the wizard to write properly formatted .env files, so that the application can read them without manual editing. + +#### Acceptance Criteria + +1. WHEN the user completes all prompts, THE Config_Wizard SHALL write the backend configuration to the Backend_Env_File path +2. WHEN the user completes all prompts, THE Config_Wizard SHALL write the frontend configuration to the Frontend_Env_File path +3. THE Config_Wizard SHALL write each variable in `KEY=value` format with `#`-prefixed comment headers separating Variable_Groups, and SHALL wrap any value containing spaces, `#`, or quote characters in double quotes +4. THE Config_Wizard SHALL omit Optional_Variables from the output file when the user provides no value and no default exists +5. IF a Backend_Env_File or Frontend_Env_File already exists, THEN THE Config_Wizard SHALL prompt the user to confirm overwrite before writing, offer to create a backup copy named `{original_filename}.backup.{YYYYMMDD_HHmmss}` in the same directory, and abort writing that file without affecting other files if the user declines both overwrite and backup +6. IF writing to the Backend_Env_File or Frontend_Env_File fails due to a filesystem error, THEN THE Config_Wizard SHALL display an error message indicating the file path and the reason for failure, and SHALL exit without writing any remaining files + +### Requirement 7: Skip Optional Integration Groups + +**User Story:** As a deployer, I want to skip entire optional integration groups that I do not use, so that I can complete the wizard quickly without answering irrelevant prompts. + +#### Acceptance Criteria + +1. WHEN entering an optional Variable_Group (Ivanti, Atlas, Jira, CARD, GitLab, NVD), THE Config_Wizard SHALL prompt the user with a yes/no question asking whether they want to configure that integration, defaulting to "no" if the user presses Enter without input +2. WHEN the user declines to configure an optional Variable_Group by entering "n", "no", or pressing Enter to accept the default, THE Config_Wizard SHALL skip all variables in that group and proceed to the next group +3. WHEN the user declines an optional Variable_Group, THE Config_Wizard SHALL not write any variables from that group to the env file +4. WHEN displaying the summary (per Requirement 8), THE Config_Wizard SHALL list each skipped Variable_Group by name with an indication that it was skipped + +### Requirement 8: Summary and Confirmation + +**User Story:** As a deployer, I want to review my configuration before it is written to disk, so that I can catch mistakes before they are persisted. + +#### Acceptance Criteria + +1. WHEN all prompts are complete, THE Config_Wizard SHALL display a summary table organized by Variable_Group showing all configured variables with their values, masking the following variables with asterisks (`********`): SESSION_SECRET, NVD_API_KEY, IVANTI_API_KEY, ATLAS_API_PASS, JIRA_API_TOKEN, JIRA_PAT, CARD_API_PASS, GITLAB_PAT, DATABASE_URL +2. WHEN all prompts are complete, THE Config_Wizard SHALL exclude skipped Variable_Groups from the summary table and display only variables that will be written to the env files +3. WHEN displaying the summary, THE Config_Wizard SHALL show the target file paths (`backend/.env` and `frontend/.env`) and indicate whether each file already exists and will be overwritten +4. WHEN the user confirms the summary, THE Config_Wizard SHALL write the env files and display a success message followed by next steps instructing the user to run `node backend/setup.js` to initialize the database and to start the servers +5. WHEN the user rejects the summary, THE Config_Wizard SHALL prompt the user to choose between restarting the wizard from the beginning with all previously entered values cleared, or exiting without writing any files + +### Requirement 9: Reconfigure Existing Installation + +**User Story:** As a deployer, I want to re-run the wizard on an existing installation and have it pre-fill current values, so that I can update specific variables without re-entering everything. + +#### Acceptance Criteria + +1. WHEN the Config_Wizard starts and a Backend_Env_File already exists, THE Config_Wizard SHALL parse the existing file by splitting each non-comment, non-empty line on the first `=` character, use the parsed current values as defaults in place of factory defaults for each corresponding prompt, and fall back to factory defaults for any managed variables not present in the file +2. WHEN the Config_Wizard starts and a Frontend_Env_File already exists, THE Config_Wizard SHALL parse the existing file by splitting each non-comment, non-empty line on the first `=` character, use the parsed current values as defaults in place of factory defaults for each corresponding prompt, and fall back to factory defaults for any managed variables not present in the file +3. WHEN displaying a prompt with a pre-filled value from an existing env file, THE Config_Wizard SHALL label the default as "[current]" to distinguish it from a factory default +4. THE Config_Wizard SHALL preserve any variables and comment lines in existing env files whose keys are not in the wizard's managed variable list (PORT, API_HOST, CORS_ORIGINS, SESSION_SECRET, DATABASE_URL, NVD_API_KEY, IVANTI_*, ATLAS_*, JIRA_*, CARD_*, GITLAB_*, REACT_APP_*), appending them in a "Custom Variables" section at the end of the generated file +5. IF an existing env file contains a key that matches a managed variable name but appears in a custom or unrecognized section, THEN THE Config_Wizard SHALL use the wizard-entered value for that key and discard the duplicate from the custom variables section +6. IF an existing env file cannot be parsed due to file read errors or contains no valid KEY=value lines, THEN THE Config_Wizard SHALL display a warning message indicating the file could not be loaded, proceed using factory defaults, and preserve the unparseable file as a backup before writing the new configuration diff --git a/.kiro/specs/config-wizard/tasks.md b/.kiro/specs/config-wizard/tasks.md new file mode 100644 index 0000000..977a700 --- /dev/null +++ b/.kiro/specs/config-wizard/tasks.md @@ -0,0 +1,206 @@ +# Implementation Plan: Config Wizard + +## Overview + +Implement `configure.js` as a single-file interactive CLI wizard at the project root. The implementation follows the design's module structure: constants/registry, parsing, validation, display/prompt, file writing, and main flow orchestration. Property-based tests use fast-check in `backend/__tests__/`. + +## Tasks + +- [x] 1. Set up project structure and install test dependencies + - [x] 1.1 Create `configure.js` with shebang, constants, and variable descriptor registry + - Add `#!/usr/bin/env node` header + - Define `VARIABLE_DESCRIPTORS` array with all 32 managed variables and their metadata (name, group, required, default, description, docUrl, sensitive, validator) + - Define `GROUP_ORDER` array, `GROUP_DESCRIPTIONS` object, and `SENSITIVE_VARS` list + - Export constants for testability using `module.exports` at the bottom (conditionally, so the file still runs as a script) + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 3.1, 3.2, 3.3, 3.4_ + + - [x] 1.2 Install fast-check as a dev dependency in backend + - Run `npm install --save-dev fast-check` in `backend/` + - _Requirements: N/A (test infrastructure)_ + +- [x] 2. Implement parsing functions + - [x] 2.1 Implement `resolveShellDefault(str)` function + - Extract default value from `${VAR:-default}` pattern + - Return original string if pattern not found + - _Requirements: 4.1_ + + - [x] 2.2 Implement `parseDockerCompose(filePath)` function + - Line-by-line state machine parser for docker-compose.yml + - Extract POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, and host port + - Resolve shell variable substitution via `resolveShellDefault` + - Return `null` if file missing or unparseable + - _Requirements: 4.1, 4.2, 4.3, 4.4_ + + - [x] 2.3 Implement `parseEnvFile(filePath)` function + - Read file line by line, split on first `=` + - Separate managed variables from unmanaged lines (including preceding comments) + - Handle quoted values (strip surrounding double quotes) + - Return `{ managed: Map, unmanaged: String[] }` + - Return empty maps if file doesn't exist or is unreadable + - _Requirements: 9.1, 9.2, 9.4, 9.6_ + + - [ ]* 2.4 Write property test for shell default resolution + - **Property 5: Shell variable default resolution** + - **Validates: Requirements 4.1** + + - [ ]* 2.5 Write property test for DATABASE_URL construction + - **Property 6: DATABASE_URL construction** + - **Validates: Requirements 4.2** + + - [ ]* 2.6 Write property test for env file round-trip parsing + - **Property 16: Env file round-trip parsing** + - **Validates: Requirements 9.1, 9.2** + +- [x] 3. Implement validation functions + - [x] 3.1 Implement all validation functions + - `validatePort(value)` — integer in [1, 65535] + - `validateCorsOrigins(value)` — each comma-separated entry starts with http:// or https:// + - `validateDatabaseUrl(value)` — starts with `postgresql://` or equals `sqlite` + - `validateSessionSecret(value)` — at least 16 characters + - `validateRequired(value)` — non-empty, non-whitespace-only + - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.6_ + + - [ ]* 3.2 Write property tests for validation functions + - **Property 8: Port validation** + - **Property 9: CORS origins validation** + - **Property 10: DATABASE_URL validation** + - **Property 11: SESSION_SECRET validation** + - **Property 12: Required variable rejection of whitespace** + - **Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.6** + +- [x] 4. Implement display and prompt functions + - [x] 4.1 Implement display functions + - `printWelcome()` — purpose and instructions + - `printGroupHeader(group)` — group name and description + - `printSummary(config, skippedGroups)` — table with masked sensitive values, target file paths, overwrite indicators + - `maskSensitive(name, value)` — first 4 + last 4 with asterisks for strings > 8 chars, full value otherwise + - _Requirements: 1.2, 2.3, 3.4, 8.1, 8.2, 8.3_ + + - [x] 4.2 Implement prompt functions + - `promptVariable(rl, descriptor, currentValue)` — display label, description, default/current, docUrl; validate input; re-prompt on failure + - `promptYesNo(rl, question, defaultNo)` — yes/no with configurable default + - `promptOverwrite(rl, filePath)` — confirm overwrite, offer backup + - _Requirements: 3.1, 3.2, 3.3, 3.5, 5.5, 6.5, 7.1, 9.3_ + + - [ ]* 4.3 Write property test for sensitive value masking + - **Property 4: Sensitive value masking** + - **Validates: Requirements 3.4** + + - [ ]* 4.4 Write property test for derived URL defaults from PORT + - **Property 7: Derived URL defaults from PORT** + - **Validates: Requirements 4.6** + +- [x] 5. Checkpoint + - Ensure all tests pass, ask the user if questions arise. + +- [x] 6. Implement file writing functions + - [x] 6.1 Implement `generateEnvContent(variables, groupOrder, groupDescriptions, unmanagedLines)` function + - Group header comments (`# --- Group Name ---`) + - `KEY=value` lines with conditional double-quote wrapping (spaces, `#`, quotes) + - Omit optional variables with no value and no default + - Append unmanaged variables in `# --- Custom Variables ---` section + - _Requirements: 6.1, 6.2, 6.3, 6.4, 9.4, 9.5_ + + - [x] 6.2 Implement `writeEnvFile(filePath, content)` and `createBackup(filePath)` functions + - Write content to file path using `fs.writeFileSync` + - `createBackup` copies existing file to `{filename}.backup.{YYYYMMDD_HHmmss}` + - Handle filesystem errors with descriptive messages + - _Requirements: 6.5, 6.6_ + + - [ ]* 6.3 Write property test for env value quoting + - **Property 13: Env value quoting** + - **Validates: Requirements 6.3** + + - [ ]* 6.4 Write property test for optional variable omission + - **Property 14: Optional variable omission** + - **Validates: Requirements 6.4** + + - [ ]* 6.5 Write property test for skipped group exclusion + - **Property 15: Skipped group exclusion** + - **Validates: Requirements 7.2, 7.3** + + - [ ]* 6.6 Write property test for unmanaged variable preservation + - **Property 17: Unmanaged variable preservation** + - **Validates: Requirements 9.4** + + - [ ]* 6.7 Write property test for managed key deduplication + - **Property 18: Managed key deduplication** + - **Validates: Requirements 9.5** + +- [x] 7. Implement main flow orchestration + - [x] 7.1 Implement `main()` function and wire all components together + - Validate project root (check `backend/` and `frontend/` exist) + - Parse existing env files for pre-fill defaults + - Parse docker-compose.yml for derived defaults + - Set up readline interface and SIGINT handler + - Group loop: iterate GROUP_ORDER, prompt skip for optional groups, prompt each variable + - Derive REACT_APP_API_BASE, REACT_APP_API_HOST, CORS_ORIGINS from confirmed PORT + - Display summary, prompt confirmation (approve / restart / exit) + - Handle overwrite prompts and write both env files + - Display success message with next steps + - Exit code 0 on success, 1 on error/cancel + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 4.5, 4.6, 7.1, 7.2, 7.3, 7.4, 8.1, 8.2, 8.3, 8.4, 8.5_ + +- [x] 8. Checkpoint + - Ensure all tests pass, ask the user if questions arise. + +- [ ] 9. Write descriptor registry property tests + - [ ]* 9.1 Write property test for descriptor registry invariants + - **Property 1: Descriptor registry invariants** + - **Validates: Requirements 2.3, 2.5** + + - [ ]* 9.2 Write property test for variable ordering within groups + - **Property 2: Variable ordering within groups** + - **Validates: Requirements 2.4** + + - [ ]* 9.3 Write property test for group presentation order + - **Property 3: Group presentation order** + - **Validates: Requirements 2.1** + +- [ ] 10. Write integration and unit tests + - [ ]* 10.1 Write unit tests for parseDockerCompose + - Test valid compose files, missing files, malformed content + - _Requirements: 4.1, 4.2, 4.3, 4.4_ + + - [ ]* 10.2 Write unit tests for parseEnvFile + - Test standard files, quoted values, comments, empty lines, malformed lines + - _Requirements: 9.1, 9.2, 9.4, 9.6_ + + - [ ]* 10.3 Write unit tests for generateEnvContent + - Test group headers, quoting rules, omission of empty optionals, custom variables section + - _Requirements: 6.1, 6.2, 6.3, 6.4, 9.4, 9.5_ + + - [ ]* 10.4 Write integration tests for full wizard flow + - Test full run with defaults accepted, existing env pre-fill, skipped groups, SIGINT handling, missing project structure error + - Use temporary directories and mock readline input + - _Requirements: 1.1, 1.4, 1.5, 7.1, 7.2, 7.3, 8.4, 8.5, 9.1_ + +- [x] 11. Final checkpoint + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for faster MVP +- Each task references specific requirements for traceability +- Checkpoints ensure incremental validation +- Property tests validate universal correctness properties from the design document +- Unit tests validate specific examples and edge cases +- All code goes in a single `configure.js` file at the project root +- Tests go in `backend/__tests__/` using Jest + fast-check +- The file uses `module.exports` conditionally (only when `require.main !== module`) so it can be both a runnable script and importable for testing + +## Task Dependency Graph + +```json +{ + "waves": [ + { "id": 0, "tasks": ["1.1", "1.2"] }, + { "id": 1, "tasks": ["2.1", "2.2", "2.3", "3.1"] }, + { "id": 2, "tasks": ["2.4", "2.5", "2.6", "3.2", "4.1", "4.2"] }, + { "id": 3, "tasks": ["4.3", "4.4", "6.1", "6.2"] }, + { "id": 4, "tasks": ["6.3", "6.4", "6.5", "6.6", "6.7", "7.1"] }, + { "id": 5, "tasks": ["9.1", "9.2", "9.3", "10.1", "10.2", "10.3"] }, + { "id": 6, "tasks": ["10.4"] } + ] +} +``` diff --git a/.kiro/specs/requeue-rejected-fp-as-archer/test.md b/.kiro/specs/requeue-rejected-fp-as-archer/test.md new file mode 100644 index 0000000..0fd0b07 --- /dev/null +++ b/.kiro/specs/requeue-rejected-fp-as-archer/test.md @@ -0,0 +1 @@ +# Test spec diff --git a/.kiro/steering/workflow.md b/.kiro/steering/workflow.md index 6ff7f2b..9e5c5bf 100644 --- a/.kiro/steering/workflow.md +++ b/.kiro/steering/workflow.md @@ -25,55 +25,3 @@ Each spec folder typically contains: - Adding to an existing feature — check the spec to understand design constraints - Investigating unexpected behavior — the spec documents what "correct" looks like - Refactoring — the spec documents which properties must be preserved - ---- - -## Bug Fix Documentation - -When fixing something that is or could be considered a bug, document it with a bug report. - -### Process - -1. Fix the bug and commit to `master` -2. Push to both `origin` and `backup` remotes -3. Stash any uncommitted work: `git stash` -4. Switch to the `ops/records` branch: `git checkout ops/records` -5. Add a bug report under `docs/bug-reports/` with the naming convention `--.md` -6. Commit and push on `ops/records` to both `origin` and `backup` -7. Switch back to `master`: `git checkout master` -8. Restore specs (gitignored on master, wiped by branch switch): `git checkout ops/records -- .kiro/specs/` then `git reset HEAD .kiro/specs/` -9. Pop stash: `git stash pop` - -### Bug Report Format - -Each bug report follows the **Symptom, Cause, Fix** pattern per bug: - -```markdown -# — <Date> - -## Summary -One paragraph describing the scope and root cause category. - -**Commit:** `<hash>` on `master` - ---- - -## Bug N: <Short Title> - -**Symptom:** What the user sees or experiences. - -**Cause:** The underlying technical reason. - -**Fix:** What was changed and why it resolves the issue. - -**Files changed:** -- `path/to/file.js` -``` - -### What Qualifies as a Bug - -- Incorrect data display (wrong values, wrong format) -- UI crashes or blank pages (unhandled exceptions) -- Features that stopped working after a migration or refactor -- Missing scope/filter application that was designed but not wired up -- Regressions from dependency or platform changes (e.g., SQLite to PostgreSQL)