Skip to content

Release v2.3.0: ARM64 support + configurable SSL enforcement#699

Open
aaronlippold wants to merge 192 commits intomasterfrom
v2.3.0
Open

Release v2.3.0: ARM64 support + configurable SSL enforcement#699
aaronlippold wants to merge 192 commits intomasterfrom
v2.3.0

Conversation

@aaronlippold
Copy link
Copy Markdown
Member

Summary

This release adds ARM64/Apple Silicon support and makes SSL enforcement configurable for flexible Kubernetes deployments.

Changes

Multi-Platform Docker Builds

  • Enable ARM64 builds in GitHub Actions workflow
  • Uncomment platforms: linux/amd64,linux/arm64 in push-to-docker.yml
  • Enables deployment on Apple Silicon Macs and ARM-based cloud instances

Configurable SSL Enforcement

  • Add FORCE_SSL environment variable support
  • Make config.force_ssl respect FORCE_SSL setting
  • Make config.assume_ssl respect FORCE_SSL setting (DRY approach)
  • Update all documentation with FORCE_SSL usage examples

Use Cases

Local/Development Kubernetes:

FORCE_SSL=false
  • Allows HTTP access without SSL redirects
  • Suitable for Kind, Minikube, development clusters
  • Works with Helm charts that don't configure ingress TLS

Production (with Ingress + TLS):

FORCE_SSL=true  # Default
  • Enforces HTTPS, secure cookies, HSTS headers
  • Ingress handles SSL termination
  • Standard production deployment pattern

Testing

  • Built and tested locally with ARM64 and AMD64 platforms
  • Verified FORCE_SSL=false allows HTTP operation
  • Verified FORCE_SSL=true maintains production security
  • Deployed successfully to Kind cluster with Helm chart

Compatibility

  • Backward compatible (defaults to true, existing behavior)
  • No breaking changes
  • Works with existing deployment methods

Documentation Updated

  • ENVIRONMENT_VARIABLES.md
  • .env.production.example
  • docs/getting-started/environment-variables.md

Authored by: Aaron Lippoldlippold@gmail.com

Signed-off-by: Aaron Lippold <lippold@gmail.com>
…able

## Changes

### Production Configuration
- Make config.force_ssl configurable via FORCE_SSL env var (defaults to true)
- Make config.assume_ssl respect FORCE_SSL setting (DRY approach)
- Both settings now controlled by single environment variable

### Documentation
- Update ENVIRONMENT_VARIABLES.md with FORCE_SSL usage
- Update .env.production.example with FORCE_SSL documentation
- Update docs/getting-started/environment-variables.md with use cases

## Use Cases

**Local/Dev Kubernetes (no ingress):**
- Set FORCE_SSL=false
- Allows HTTP access without SSL redirects
- Suitable for Kind, Minikube, development clusters

**Production (with ingress + TLS):**
- Set FORCE_SSL=true (default)
- Enforces HTTPS, secure cookies, HSTS headers
- Ingress handles SSL termination

## Benefits
- Helm chart compatibility for local development
- Maintains security defaults for production
- Single configuration point for SSL behavior
- Backward compatible (defaults to true)

Authored by: Aaron Lippold<lippold@gmail.com>
@sonarqubecloud
Copy link
Copy Markdown

- Add health_check gem (3.1.0)
- Add /up endpoint (Rails 8 liveness check)
- Add /health_check endpoints (database, ldap, oidc)
- Add /status endpoint (application status JSON)
- Update all deployment documentation
- Add monitoring guide

Provides Kubernetes-ready health monitoring with proper
liveness, readiness, and startup probe endpoints.

Authored by: Aaron Lippold<lippold@gmail.com>
- Add valid_email2 gem for email validation
- Production SMTP validation (Discourse pattern)
  - Validates email format and blocks example.com domains
  - Validates required SMTP settings when enabled
  - Fails fast with helpful error messages
  - Only validates on server start (not rake tasks)
- User model blocks disposable email providers
- Create membership factory (was missing)
- Fix test isolation with Settings.reload!
- Add comprehensive test coverage (28 new tests)
- Update testing documentation with prerequisites

Prevents production email misconfiguration that triggers
spam filters. Better than GitLab's approach (no validation).

Authored by: Aaron Lippold<lippold@gmail.com>
- Add rails_app_version gem (centralized version management)
- VERSION file as single source of truth
- Automated release workflow (tag-based)
  - Phase 1: Validate (VERSION/package.json consistency)
  - Phase 2: Test (RSpec + RuboCop + Brakeman)
  - Phase 3: Create draft release (CHANGELOG extraction)
  - Phase 4: Build Docker (multi-platform amd64+arm64)
  - Phase 5: Publish release (only if all succeed)
- Version bump rake tasks (bump_patch/minor/major, sync, check)
- Failure isolation prevents pollution
- Rewrite release-process.md with modern workflow

Developer experience:
  rails version:bump_minor
  # Follow printed instructions
  git push origin v2.4.0
  # GitHub Actions handles everything else

Authored by: Aaron Lippold<lippold@gmail.com>
- Update README.md
- Update docker-compose files
- Remove empty vue3-migration.md
- Schema updates from test runs

Authored by: Aaron Lippold<lippold@gmail.com>
- Add official Helm chart as recommended deployment method
- Link to vulcan-helm repository
- Update manual examples to match helm chart tested values:
  - Image version: v2.3.0
  - PostgreSQL: 16-alpine (from 15)
  - Startup probe initialDelaySeconds: 30 (from 0)
- Clarify manual examples are for learning/customization
- Helm chart is source of truth for production

Ensures Vulcan docs and helm chart stay in sync.

Authored by: Aaron Lippold<lippold@gmail.com>
- Add early exit if admin user already exists
- Prevents duplicate data on multiple db:seed runs
- Follows Rails convention: seeds run once, use db:reset for fresh data
- Provides helpful message about db:reset

Fixes issue where running bin/setup multiple times created duplicate
users and projects.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
- Add prometheus_exporter gem for observability
- Expose metrics on port 9394 (/metrics endpoint)
- Track HTTP requests, database queries, process metrics
- Only start prometheus server during Rails server/puma (not rake tasks)
- Prevents 'Address already in use' errors during db:prepare
- Includes comprehensive metrics tests

Metrics available at:
- Development: http://localhost:9394/metrics
- Production: http://app:9394/metrics
- Kubernetes: Scraped by Prometheus Operator

Part of v2.3.0 production readiness and monitoring improvements.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
- Fix Node.js installation using official binaries (not NodeSource)
- Add corporate certificate support via certs/ directory
- Expose port 9394 for Prometheus metrics
- Multi-platform support (amd64 + arm64)
- Update monitoring documentation with Prometheus section

These Dockerfiles will be replaced by dockerfile-rails generated version
in next commit, but preserving current improvements.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Add dockerfile-rails gem (Rails community standard from Fly.io).
This will be used to generate optimized, production-ready Dockerfile
following Rails best practices.

Replaces custom Dockerfile maintenance with generated approach.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Silences warning about nkf being removed from default gems in Ruby 3.4.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Replace custom Dockerfiles with Rails community standard (dockerfile-rails gem).

Key changes:
- Single Dockerfile (replaces Dockerfile + Dockerfile.production)
- Multi-stage build with jemalloc optimization
- config/dockerfile.yml - single source of truth for Docker config
- Custom CA certificate support via instruction files
- Prometheus port 9394 exposed via deploy instructions
- docker-compose.yml for full-stack deployment
- bin/docker-entrypoint with db:migrate (not db:prepare)

Configuration (config/dockerfile.yml):
- PostgreSQL support
- jemalloc memory allocator
- Build packages: git, gnupg, zlib1g-dev (for fast_excel gem)
- Custom instructions for CA certs and Prometheus port

Benefits:
- Maintainable (regenerate via: rails generate dockerfile --force)
- Standard Rails approach (not custom solutions)
- Tested on amd64 and arm64
- Works in corporate environments with custom CA

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Following Twelve-Factor App principles, all ports now configurable via ENV.

New environment variables:
- VULCAN_RAILS_PORT (default: 3000, falls back to PORT for compatibility)
- VULCAN_PROMETHEUS_PORT (default: 9394)
- VULCAN_PROMETHEUS_BIND (default: 0.0.0.0)

Changes:
- config/initializers/00_environment_config.rb - centralized env config
- config/initializers/prometheus.rb - use Vulcan::Config for ports
- config/puma.rb - use VULCAN_RAILS_PORT with PORT fallback
- config/environments/production.rb - mailer uses VULCAN_APP_URL
- Procfile.dev - parameterized ports
- ENVIRONMENT_VARIABLES.md - documented all port variables

Tested with custom ports (8080, 9095) - all endpoints functional.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Auto-generated schema from migrations.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Add VULCAN_RAILS_PORT, VULCAN_PROMETHEUS_PORT, VULCAN_PROMETHEUS_BIND
to .env examples as optional (commented) configuration.

Users can now see all available port customization options.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Add support for all CA certificate environment variable standards and
corporate proxy configuration.

Changes:
- Support NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, SSL_CERT_DIR, CURL_CA_BUNDLE, REQUESTS_CA_BUNDLE
- Configure npm and yarn cafile for SSL interception
- Support HTTP_PROXY/HTTPS_PROXY build args
- Works with multiple corporate proxy configurations

Note: Docker builds on MITRE VPN may fail due to network policy blocking
yarn registry. Build off-VPN or in CI/CD (GitHub Actions has no VPN).

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Regenerated after adding comprehensive CA cert and proxy support to
instruction files. No functional changes, just updated from templates.

Authored by: Aaron Lippold <lippold@gmail.com>

Signed-off-by: Aaron Lippold <lippold@gmail.com>
Changes:
- Changed rule_update_params from params.expect to params.require().permit()
- Rails 8's params.expect was preventing nested arrays from being permitted
- Affects checks_attributes, disa_rule_descriptions_attributes, rule_descriptions_attributes
- Added comprehensive request specs to prevent regression

Impact:
- Fixes bug where check text, fix text, and vuln descriptions weren't persisting
- Users reported changes reverting after save - now resolved
- All 273 existing tests pass + 6 new tests for nested attribute updates

Testing:
- Added spec/requests/rules_spec.rb with tests for:
  - Check content updates
  - Fixtext updates
  - DISA rule description updates
  - Status updates
  - Multi-field updates simulating real frontend behavior

Root Cause:
params.expect with nested arrays was marking params as permitted: false,
causing nested attributes to be filtered out and arrive as nil in the controller.
Using params.require().permit() properly handles nested array parameters.

Authored by: Aaron Lippold<lippold@gmail.com>
Changes:
- Fixed create_rule_satisfactions to handle satisfied_by lists with or without trailing periods
- Changed from .delete('.') to .sub(/\.\s*\z/, '') to only remove trailing period
- Added comprehensive tests for parser edge cases

Tests Added:
- Parses satisfied_by list with trailing period
- Parses satisfied_by list without trailing period
- Parses satisfied_by list with extra whitespace
- Tests for satisfied_by relationship and status preservation (3 tests)

Impact:
- Spreadsheet imports no longer fail when satisfied_by lists don't end with period
- More robust parsing handles various formatting styles
- Confirms backend status logic is correct (status IS preserved)

All 280 tests passing.

Authored by: Aaron Lippold<lippold@gmail.com>
Problem:
When adding a satisfied_by relationship to a control, the UI would not properly
update to show fields appropriate for "Applicable - Configurable" status. Fields
would disappear or tooltips would show incorrectly.

Root Cause:
Multiple computed properties and template conditions were checking only
rule.status, not considering rule.satisfied_by.length. Since rules with
satisfied_by relationships should behave like "Applicable - Configurable"
status, these checks were incomplete.

Changes:
- BasicRuleForm.vue: Updated disaDescriptionFormFields and checkFormFields computed
  properties to check satisfied_by.length > 0 || status == "Applicable - Configurable"
- AdvancedRuleForm.vue: Updated template v-if conditions for disa descriptions,
  checks, and field visibility warnings
- RuleForm.vue: Updated CheckForm v-if condition to include satisfied_by check
- CheckForm.vue: Updated tooltips computed property to check satisfied_by

Impact:
- UI now correctly shows/hides fields when satisfied_by relationships change
- Check text, vuln discussion fields properly visible after adding satisfied_by
- Tooltips display correctly based on effective status
- Fields no longer disappear unexpectedly after nesting controls

Note:
Backend status logic was already correct (verified with model specs). This fix
addresses only the frontend reactivity issues.

Future Improvement:
Consider adding Vitest/Playwright for Vue component testing to catch these issues.

Authored by: Aaron Lippold<lippold@gmail.com>
Setup:
- Added Vitest with @vitejs/plugin-vue2 for Vue 2.7 support
- Configuration based on HabitRPG/habitica (production Vue 2 + Vitest project)
- Test pattern: Test computed properties directly without full component mounting
- Avoids complex SFC mounting issues while testing critical logic

Dependencies Added:
- vitest, @vitest/ui, jsdom, happy-dom
- @vitejs/plugin-vue2 (correct Vue 2 plugin)
- @vue/test-utils@1.3.6 (Vue 2 version)
- @vue/server-renderer (aliased to vue-server-renderer@2.7.16)
- postcss-import, postcss-flexbugs-fixes, postcss-preset-env

Test Coverage:
- BasicRuleForm.spec.js (7 tests, all passing)
- Tests checkFormFields computed property with satisfied_by reactivity
- Tests disaDescriptionFormFields computed property with satisfied_by reactivity
- Validates the Vue reactivity bug fixes from previous commit

Scripts Added:
- yarn test:unit - Run tests in watch mode
- yarn test:unit:ui - Run tests with UI
- yarn test:unit:run - Run tests once (CI mode)

All 7 tests passing. Tests verify that computed properties correctly check
satisfied_by.length > 0 in addition to status checks.

Authored by: Aaron Lippold<lippold@gmail.com>
Added Vitest tests for all Vue components modified in reactivity fix:
- AdvancedRuleForm.spec.js (6 tests)
- CheckForm.spec.js (4 tests)
- RuleForm.spec.js (4 tests)

Total: 21 frontend tests, all passing

Tests verify:
- Computed properties react to satisfied_by.length changes
- Fields display correctly when satisfied_by relationships exist
- Tooltips show appropriate messages based on satisfied_by
- Status text returns correct value with satisfied_by

All tests validate the bug fixes for status/field visibility issues
when adding satisfied_by relationships to controls.

Authored by: Aaron Lippold<lippold@gmail.com>
Replaced single-select dropdown with multi-select checkbox list in the
"Also Satisfies" modal, allowing users to add multiple satisfied_by
relationships at once instead of one-at-a-time.

Changes:
- Replaced vue-simple-suggest with Bootstrap-Vue b-form-checkbox-group
- Added search filter for checkbox list (real-time filtering)
- Added scrollable container (400px max-height) for large control lists
- Shows selection count in modal
- Emits multiple addSatisfied:rule events on submit
- Clears selections when modal is hidden

UI Improvements:
- Modal sized to 'lg' for better visibility
- Search box filters checkboxes in real-time (case-insensitive)
- Disabled submit button when no selections
- Shows "X control(s) selected" counter
- "Add X Control(s)" button text updates dynamically

Tests Added:
- RuleEditorHeader.spec.js (3 tests)
- Tests filteredSelectRulesForCheckbox computed property
- Validates search filtering logic

Impact:
Major UX improvement for users managing large SRGs (273 controls).
Can now select and add 10+ satisfied_by relationships in one action
instead of opening modal 10+ times.

All 24 frontend tests + 280 backend tests passing.

Authored by: Aaron Lippold<lippold@gmail.com>
Added individual expand/collapse functionality for parent controls in the
sidebar "All Controls" list when "Nest Satisfied Controls" is enabled.

Changes:
- Added expandedParents object to track expand state per parent (rule.id => boolean)
- Added chevron-down/chevron-right icon next to parent controls with children
- Wrapped nested controls in b-collapse with v-model bound to expandedParents
- Added toggleParentExpand() and isParentExpanded() methods
- Chevron is clickable with @click.stop to prevent row selection

UX Improvements:
- When nesting is enabled, parents start COLLAPSED by default
- Shows only parent controls initially (e.g., 15 parents instead of 273 total)
- Click chevron to expand/collapse individual parent's nested children
- Nested controls indented with ml-3 for visual hierarchy
- Focus on the core controls that need attention

Impact:
Major UX improvement for large SRGs. Users can now focus on just the
parent controls (15) and expand individual parents as needed, instead of
seeing all 273 controls at once.

Pattern:
Uses standard Bootstrap-Vue b-collapse with v-model, same pattern as
RuleSatisfactions component.

All 280 backend tests + 24 frontend tests passing.

Authored by: Aaron Lippold<lippold@gmail.com>
Implemented rich metrics display on component cards showing primary controls,
inherited requirements, and completion progress using Bootstrap-Vue components.

Backend Changes:
- Added primary_controls_count method (total - nested)
- Added rules_summary method returning hash with all metrics
- Included in as_json output for frontend consumption
- Metrics: primary_count, nested_count, locked, under_review, not_under_review,
  changes_requested, plus all status breakdowns

Frontend Changes:
- Added b-list-group showing Primary Controls and Inherited counts
- Added b-progress with multi-segment bar (Locked/Review/Draft)
- Used proper compliance terminology: "Inherited" for nested requirements
- Color-coded: Success (locked), Warning (review), Info (draft)

Display Logic:
- Shows "223 Primary Controls" + "50 Inherited" when nesting exists
- Shows "273 Controls" when no nesting
- Progress bar visualizes completion state
- Inherited only shown when nested_count > 0

Tests Added:
- Backend: 4 tests for parent_rules_count calculation
- Frontend: 4 tests for ComponentCard display logic
- All 28 frontend + 284 backend tests passing

UX Impact:
Users can now see at-a-glance:
- Total scope (273 requirements)
- Work scope (223 primary controls to implement)
- Inheritance (50 covered by parents)
- Progress (75 locked, 50 under review, etc.)

Terminology:
"Inherited" matches compliance industry standard for requirements
covered by parent/common controls.

Authored by: Aaron Lippold<lippold@gmail.com>
Changes to member selection to improve privacy:
- Project.available_members now returns scoped results
- Component.available_members limited to project members
- Added tests for member selection behavior

Also updated dependencies:
- Updated rack to 3.2.4
- Updated rexml to 3.4.4

All 287 tests passing.

Authored by: Aaron Lippold<lippold@gmail.com>
Implemented multiple security improvements based on OWASP best practices
and security audit findings.

File Upload Validation:
- Added validate_spreadsheet_file method to Component model
- Validates file extension (xlsx, xls, csv, ods only)
- Enforces 100MB file size limit
- Prevents malicious file uploads

Security Headers:
- Added config/initializers/security_headers.rb
- X-Frame-Options: SAMEORIGIN (prevent clickjacking)
- X-Content-Type-Options: nosniff (prevent MIME sniffing)
- X-XSS-Protection: enabled
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: restricts geolocation, camera, microphone

Rate Limiting:
- Added rack-attack gem
- Login attempts: 5 per email per 20 seconds
- Login attempts: 20 per IP per minute
- Registration: 3 per IP per hour
- API requests: 300 per IP per 5 minutes
- Safelists localhost and health check endpoints
- Returns 429 with rate limit headers

Dependency Updates:
- Updated Puma 5.6 → 6.6 (Rack 3 compatibility)
- Rack 3 required for latest security patches

Authorization:
- Audited all controllers - authorization present on all actions
- RulesController, ComponentsController, ProjectsController all have
  proper before_action :authorize_* filters

Brakeman: Still clean (0 warnings)
Bundle Audit: Clean (all CVEs patched)

All 287 tests passing.

Authored by: Aaron Lippold<lippold@gmail.com>
Added full test coverage for all security improvements to prevent regression.

File Upload Validation Tests (4 tests):
- Rejects non-spreadsheet files (exe, bat, etc.)
- Accepts valid spreadsheet files (csv, xlsx, xls, ods)
- Rejects files over 100MB
- Accepts files under limit
- Uses Rack::Test::UploadedFile with real fixtures

Security Headers Tests (6 tests):
- Verifies X-Frame-Options header
- Verifies X-Content-Type-Options header
- Verifies X-XSS-Protection header
- Verifies Referrer-Policy header
- Verifies Permissions-Policy header
- Tests on both public and authenticated endpoints

Rate Limiting Tests (9 tests):
- Login throttling by email (5 attempts)
- Case-insensitive email throttling
- Login throttling by IP (20 attempts)
- IP isolation (separate limits per IP)
- Health check safelisting (/up, /status, /health_check)
- General API throttling tracking
- Proper test isolation with cache clearing

Test Pattern:
- Follows Rack::Attack official test patterns
- Uses clear_configuration only after(:all)
- Clears cache before(:each) for isolation
- Tests against actual throttle configuration
- Uses unique IPs/emails to avoid test pollution

Test Fixtures Added:
- spec/fixtures/files/test.csv
- spec/fixtures/files/malicious.exe

All 306 tests passing (287 backend + 9 rate limiting + 6 headers + 4 file upload).

Authored by: Aaron Lippold<lippold@gmail.com>
…tion

Implement generic app banner component for top/bottom of pages.
Replaces classification-specific banner with flexible system.

Changes:
- Add AppBanner.vue with Bootstrap color name support
- Integrate banner into AuthHeader, Navbar, and AppFooter
- Add FooterCopyright component with theme-aware text colors
- Rename ClassificationBanner -> AppBanner (more generic)
- Configure banner via vulcan.yml and environment variables
- Expose banner config to Vue via window.vueAppData

Color system:
- Bootstrap names: success, warning, danger, primary, etc.
- CSS variables: var(--bs-success), var(--custom-color)
- Hex codes: #198754, #ffffff
- Theme-aware: Adapts to light/dark mode automatically

Authored by: Aaron Lippold<lippold@gmail.com>
Add banner settings to admin settings page with live preview.
Document environment variables with usage examples.

Changes:
- Add banner section to admin settings controller API
- Display banner config in SettingsPage.vue with color preview
- Add VULCAN_BANNER_* environment variables to .env.example
- Document banner usage in ENVIRONMENT_VARIABLES.md

Configuration options:
- VULCAN_BANNER_ENABLED: Show/hide banner
- VULCAN_BANNER_TEXT: Banner text
- VULCAN_BANNER_BACKGROUND_COLOR: Bootstrap color or hex
- VULCAN_BANNER_TEXT_COLOR: Bootstrap color or hex

Example use cases:
- Development environment indicators
- Staging/production markers
- Classification levels (UNCLASSIFIED, etc.)
- Public release notices

Authored by: Aaron Lippold<lippold@gmail.com>
Add resend confirmation, unlock account, and password reset APIs.
Display helper links in login form footer.

API additions:
- resendConfirmation(email): Resend email confirmation
- resendUnlock(email): Resend account unlock instructions
- validateResetToken(token): Check password reset token validity
- resetPassword(token, password, passwordConfirmation): Reset password

UI changes:
- Add auth helper links below login form
- Links to /auth/confirmation and /auth/unlock pages

Store/composable updates:
- Add methods to auth store for helper actions
- Expose helper methods via useAuth composable

Authored by: Aaron Lippold<lippold@gmail.com>
Creates a centralized helper that dynamically builds authentication provider
configuration for all enabled providers (LDAP, OIDC, OAuth providers like
GitHub, Google, GitLab, Azure AD).

- Add AuthProvidersHelper module with auth_providers method
- Handle both hash and OpenStruct access patterns for provider objects
- Provide default titles for LDAP/OIDC when not configured
- Include helper in ApplicationController
- Expose provider configuration to Vue via window.vueAppData
- Add comprehensive RSpec tests (10 tests, all passing)

Supports simultaneous multi-provider authentication with proper UX.

Authored by: Aaron Lippold<lippold@gmail.com>
Changes auth helper routes (/auth/confirmation, /auth/unlock,
/auth/reset-password) to use public#index instead of projects#index,
allowing unauthenticated users to access these pages.

- Update routes to use PublicController (skips authentication)
- Add request specs for all three auth helper routes
- Stub Vite helpers in specs to avoid asset compilation in tests
- All 3 tests passing

Fixes issue where "Resend confirmation" and "Unlock account" links
redirected to login page instead of showing the forms.

Authored by: Aaron Lippold<lippold@gmail.com>
Adds comprehensive test coverage for authentication helper UI:

Component Tests:
- EmailConfirmationForm.spec.ts (26 tests)
- AccountUnlockForm.spec.ts (26 tests)

Page Tests:
- EmailConfirmationPage.spec.ts (6 tests)
- AccountUnlockPage.spec.ts (6 tests)
- PasswordResetEditPage.spec.ts (6 tests)

Total: 70 new tests, all passing

Tests cover:
- Component rendering and structure
- Form validation
- Form submission with loading states
- Mock useAuth composable with getter pattern
- Accessibility (labels, ARIA attributes)
- Router links and navigation

Authored by: Aaron Lippold<lippold@gmail.com>
Migrates remaining authentication pages to Vue 3 SPA architecture:

Frontend Changes:
- Update App.vue with auth page routes and navigation
- Add auth routes to Vue Router (confirmation, unlock, password reset)
- Update LoginPage to use router-link for auth helpers
- Enhance PasswordInput with show/hide toggle
- Update command palette config for auth navigation
- Consolidate to single application entrypoint (remove LoginPage.ts)
- Remove obsolete ProfilePage.vue

Backend Changes:
- Update registrations spec to use request spec patterns
- Fix routes and authentication flow

All auth pages now use Vue Router for seamless SPA navigation.
Part of v2.3.0 SPA migration.

Authored by: Aaron Lippold<lippold@gmail.com>
Fixed footer being cut off at bottom of viewport by:
- Removed footer height constraint in application.scss (was forcing 40px)
- Implemented CSS Grid in App.vue (auto 1fr auto pattern)
- Updated LoginPage to use h-100 instead of min-vh-100
- Cleaned up outdated flexbox comments in AppFooter

Layout changes:
- Header: Always visible at top (grid-row: 1)
- Main: Scrollable content area (grid-row: 2, overflow-y: auto)
- Footer: Always visible at bottom (grid-row: 3)

Footer improvements:
- Icons-only layout (GitHub + MITRE SAF on right)
- Better text contrast (text-white-50)

Tests:
- Added 6 regression tests (all passing)
- SCSS lint test: Prevents footer height constraint
- CSS Grid snapshot test: Protects layout solution
- Structure tests: Validates HTML elements

Authored by: Aaron Lippold<lippold@gmail.com>
Added patterns to hide internal documentation files:
- *RECOVERY*.md (recovery files from compact sessions)
- *SESSION*.md (session notes and context)
- session.md (current session notes)
- recovery-prompt.md (recovery prompts)

This keeps git status clean by hiding ~120 internal documentation files.

Authored by: Aaron Lippold<lippold@gmail.com>
Increased MITRE SAF logo from 1.25rem to 1.5rem for better
visual balance with GitHub icon in footer.

Authored by: Aaron Lippold<lippold@gmail.com>
Replaced all <a href> links with <router-link> in auth forms for
proper SPA navigation without full page refreshes.

Route changes:
- Added /auth/forgot-password route for ForgotPasswordPage
- Updated navigation guard to include forgot-password as public route

Component updates:
- LoginForm: Use router-link for confirmation/unlock links
- LoginForm: Updated forgot password URL to /auth/forgot-password
- ForgotPasswordForm: Use router-link for back to sign in
- EmailConfirmationForm: Use router-link for back to sign in
- PasswordResetForm: Use router-link for back to sign in
- AccountUnlockForm: Use router-link for back to sign in

Test fixes:
- ForgotPasswordForm.spec.ts: Replace global with globalThis (lint fix)

Benefits:
- No more full page refreshes between auth pages
- Faster navigation (stays within SPA)
- Prepares for Vue transitions between pages
- Better user experience

Authored by: Aaron Lippold<lippold@gmail.com>
Added missing Rails route to serve Vue SPA for forgot password page.
Without this route, direct navigation or page refresh returns 404.

Authored by: Aaron Lippold<lippold@gmail.com>
Changed from standalone app with BApp/AuthHeader/AuthFooter
to SPA-integrated page using PageContainer (matches other auth helpers).

Also updated title from 'Reset Password' to 'Forgot Password?'
to match the link text users clicked.

Fixes duplicate header/footer issue.

Authored by: Aaron Lippold<lippold@gmail.com>
…e → Composable)

ForgotPasswordForm was using inline fetch() instead of following the
established architecture pattern. Refactored to use proper layers:

API Layer:
- Added requestPasswordReset() in auth.api.ts
- Added comprehensive tests (7 test cases, all passing)

Store Layer:
- Added requestPasswordReset() action in auth.store.ts
- Follows withAsyncAction pattern for error handling

Composable Layer:
- Exposed requestPasswordReset() in useAuth composable
- Handles toast notifications for success/error states

Component Layer:
- Refactored ForgotPasswordForm to use useAuth() composable
- Simplified from 46 lines to 21 lines
- Added proper error handling with try/catch
- Updated tests to mock composable instead of fetch (14 tests, all passing)

This brings ForgotPasswordForm in line with other auth forms
(EmailConfirmationForm, AccountUnlockForm, PasswordResetForm).

Tests: 25 API tests + 14 component tests = 39 tests passing

Authored by: Aaron Lippold<lippold@gmail.com>
Implemented app-wide page transition system with a simple, balanced approach:

CSS (application.scss):
- Added .fade-enter-active/.fade-leave-active (200ms opacity transition)
- GPU-accelerated (opacity only, no layout thrashing)
- Respects prefers-reduced-motion (transitions to 0.01ms for accessibility)

Vue (App.vue):
- Wrapped RouterView with <Transition name="fade" mode="out-in">
- Added :key="route.path" to ErrorBoundary for proper remounting
- Works seamlessly with existing ErrorBoundary and Suspense

Design decisions:
- Uniform transition everywhere (not route-aware)
- 200ms duration (balanced - not jarring, not sluggish)
- Simple implementation (15 lines total)
- Easy to extend later if needed

Benefits:
- Smooth navigation throughout the app
- Accessible (reduced motion support)
- Performant (CSS-only, GPU-accelerated)
- Maintainable (single source of truth)

Tests: 6 App.vue tests passing

Authored by: Aaron Lippold<lippold@gmail.com>
The Transition wrapper outside ErrorBoundary/Suspense was causing
pages to disappear during navigation. Moving it inside Suspense and
applying :key to the component directly fixes the issue.

Authored by: Aaron Lippold<lippold@gmail.com>
Transition with mode='out-in' causes blank pages during navigation.
Reverting to no transitions for stability.

Authored by: Aaron Lippold<lippold@gmail.com>
Backend:
- Add consent_banner configuration to vulcan.default.yml
- Create Api::SettingsController#consent_banner endpoint
- Add /api/settings/consent_banner route (public, no auth)
- 5 request specs passing

Frontend:
- Create settings.api.ts with fetchConsentBanner()
- Create settings.store.ts with Pinia store
- Create useConsentBanner composable with localStorage tracking
- 27 frontend tests passing (6 API + 10 store + 11 composable)

Architecture: API → Store → Composable → Component (complete except component)

Next: ConsentModal component with markdown rendering

Authored by: Aaron Lippold<lippold@gmail.com>
Component:
- Created ConsentModal.vue with markdown rendering (marked + DOMPurify)
- Uses Bootstrap 5 modal with backdrop="static" (non-dismissible)
- Blurred backdrop via CSS (backdrop-filter: blur(8px))
- XSS protection with DOMPurify sanitization
- 8 component tests passing

Integration:
- Added to App.vue before authentication
- Fetches banner config on mount (public endpoint)
- Shows modal when enabled and not acknowledged
- Blocks access until user clicks "I Agree"
- Exported useConsentBanner from composables index

Tests: 40 total passing (35 frontend + 5 backend)
- API layer: 6 tests
- Store layer: 10 tests
- Composable layer: 11 tests
- Component layer: 8 tests
- Backend API: 5 tests

Architecture complete: API → Store → Composable → Component → App

Authored by: Aaron Lippold<lippold@gmail.com>
- Added VULCAN_CONSENT_BANNER_ENABLED to .env.example
- Added VULCAN_CONSENT_BANNER_VERSION to .env.example
- Added VULCAN_CONSENT_BANNER_CONTENT to .env.example
- Added comprehensive consent banner section to ENVIRONMENT_VARIABLES.md
- Includes markdown formatting guide and version management examples
- Documents default content, custom content examples, and DoD warning banner

Authored by: Aaron Lippold<lippold@gmail.com>
Refactored banner configuration for consistency and added consolidated
settings API endpoint following 12-factor principles.

Config Changes:
- Renamed banner → banner_app for consistent naming
- Renamed consent_banner → banner_consent for grouping
- ENV override for VULCAN_CONSENT_BANNER_CONTENT in controller layer
- Added vite.json configuration

API Changes:
- Added GET /api/settings consolidated endpoint (all banners)
- Preserved GET /api/settings/consent_banner legacy endpoint
- Added ui section to /status endpoint for k8s/ops visibility
- Updated admin settings controller with nested banners structure

Frontend Changes:
- Updated settings.api.ts with new interfaces (IAppBanner, ISettings)
- Updated App.vue to use Settings.banner_app
- Updated ConsentModal.vue props for title and titleAlign
- Added Login2.vue experimental login page

Tests:
- Added 8 backend tests for new /api/settings endpoint
- Updated all consent_banner mocks to banner_consent
- All tests passing (8 backend, 1348 frontend)

Documentation:
- Updated docs/getting-started/configuration.md with banner details
- Simplified ENVIRONMENT_VARIABLES.md (tables + links)
- Updated .env.example with version tracking explanation

Benefits:
- 12-factor compliant (ENV overrides work for k8s)
- Consistent naming (banner_* prefix groups all banners)
- Single API call for all UI settings (less HTTP overhead)
- Ops visibility in status endpoint

Authored by: Aaron Lippold<lippold@gmail.com>
…ord resets, unlocks)

Added Vue 3 SPA pages and Rails API endpoints for Devise auth workflows.
All features fully tested with 9 passing backend tests.

Backend:
- PublicController for unauthenticated landing page
- Users::ConfirmationsController (JSON API for email confirmation)
- Users::PasswordsController (JSON API for password resets)
- Users::UnlocksController (JSON API for account unlocks)

Frontend:
- AccountUnlockPage.vue (request account unlock)
- EmailConfirmationPage.vue (resend confirmation email)
- PasswordResetEditPage.vue (set new password with token)

Utils:
- ident-parser.ts utility for parsing identifiers
- Full test coverage (9 backend tests passing)

Part of v2.3.0 auth migration to Vue 3 SPA architecture.

Authored by: Aaron Lippold<lippold@gmail.com>
Added comprehensive user profile management page and Taskfile.yml
for task automation.

Frontend:
- AccountSettingsPage.vue (338 lines)
  - Profile editing (name, email, Slack ID)
  - Password change with current password validation
  - Account deletion with confirmation
  - OAuth user detection (read-only email for OAuth users)
  - Uses useProfile composable for state management
  - Toast notifications for all actions

Build Tools:
- Taskfile.yml - Task runner configuration
  - Setup, installation, CLI build tasks
  - Version and commit tracking
  - Alternative to Makefile with modern YAML syntax

Part of v2.3.0 user management migration.

Authored by: Aaron Lippold<lippold@gmail.com>
Comment on lines +10 to +74
name: Validate Release
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate tag format
run: |
TAG="${{ github.ref_name }}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Invalid tag format: $TAG"
echo "Expected format: v2.3.0"
exit 1
fi
echo "✅ Valid tag: $TAG"

- name: Extract version from tag
id: version
run: |
VERSION="${{ github.ref_name }}"
VERSION_NUMBER="${VERSION#v}" # Remove 'v' prefix
echo "VERSION=$VERSION_NUMBER" >> $GITHUB_OUTPUT
echo "TAG=$VERSION" >> $GITHUB_OUTPUT

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4.7'
bundler-cache: true

- name: Check version consistency
run: |
# Read version from VERSION file
FILE_VERSION=$(cat VERSION | tr -d '\n')
TAG_VERSION="${{ steps.version.outputs.VERSION }}"

if [[ "$FILE_VERSION" != "$TAG_VERSION" ]]; then
echo "❌ Version mismatch!"
echo " Tag version: $TAG_VERSION"
echo " VERSION file: $FILE_VERSION"
echo ""
echo "Please update VERSION file to match the tag:"
echo " echo '$TAG_VERSION' > VERSION"
echo " bundle exec rails version:sync"
echo " git add VERSION package.json"
echo " git commit --amend --no-edit"
echo " git tag -f ${{ github.ref_name }}"
echo " git push -f origin ${{ github.ref_name }}"
exit 1
fi

# Also check package.json is synced
PACKAGE_VERSION=$(cat package.json | jq -r '.version')
if [[ "$PACKAGE_VERSION" != "$TAG_VERSION" ]]; then
echo "❌ package.json not synced!"
echo " Tag version: $TAG_VERSION"
echo " package.json: $PACKAGE_VERSION"
echo ""
echo "Run: bundle exec rails version:sync"
exit 1
fi

echo "✅ All versions match: $TAG_VERSION"

test:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 3 months ago

To fix this, explicitly define minimal GITHUB_TOKEN permissions for jobs that only need to read repository contents. The validate and test jobs simply check out code, run Ruby/Node setup, and execute tests and static analysis; they do not write to GitHub or modify releases. Therefore, adding permissions: contents: read to these jobs is the least-privilege fix that preserves all existing behavior.

Concretely:

  • In .github/workflows/release.yml, under jobs.validate (just below runs-on: ubuntu-24.04), add:
    permissions:
      contents: read
  • In the same file, under jobs.test (just below needs: validate), add:
    permissions:
      contents: read

No other changes, imports, or new definitions are required. The publish-release job already has permissions: contents: write and should be left as-is.

Suggested changeset 1
.github/workflows/release.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,6 +9,8 @@
   validate:
     name: Validate Release
     runs-on: ubuntu-24.04
+    permissions:
+      contents: read
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -75,6 +77,8 @@
     name: Run Test Suite
     runs-on: ubuntu-24.04
     needs: validate
+    permissions:
+      contents: read
 
     services:
       postgres:
EOF
@@ -9,6 +9,8 @@
validate:
name: Validate Release
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -75,6 +77,8 @@
name: Run Test Suite
runs-on: ubuntu-24.04
needs: validate
permissions:
contents: read

services:
postgres:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +75 to +141
name: Run Test Suite
runs-on: ubuntu-24.04
needs: validate

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vulcan_vue_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4.7'
bundler-cache: true

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'

- uses: pnpm/action-setup@v4
with:
version: 10

- name: Install dependencies
run: |
bundle install
pnpm install

- name: Setup database
env:
RAILS_ENV: test
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/vulcan_vue_test
run: |
bundle exec rails db:create
bundle exec rails db:migrate

- name: Run frontend tests
run: pnpm vitest run

- name: Run backend tests
env:
RAILS_ENV: test
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/vulcan_vue_test
run: bundle exec parallel_rspec spec/

- name: Run RuboCop
run: bundle exec rubocop

- name: Run Brakeman security scan
run: bundle exec brakeman --no-pager

create-draft-release:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 3 months ago

To fix the problem, add an explicit permissions block to the test job that grants only the minimal required scope. Since test just checks out the repository and runs tests, contents: read is sufficient. This prevents the job from inheriting potentially broader default token permissions while preserving all current functionality.

Concretely:

  • Edit .github/workflows/release.yml.
  • Under the test job (the one with name: Run Test Suite on line 75), insert:
    permissions:
      contents: read

between runs-on: ubuntu-24.04 and needs: validate. No other jobs need changes: create-draft-release already has permissions: contents: write, and validate does not appear to require write access (you could optionally also add contents: read there, but CodeQL’s reported issue is about the test job, so we’ll focus on that). No imports or external dependencies are required for this change.

Suggested changeset 1
.github/workflows/release.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -74,6 +74,8 @@
   test:
     name: Run Test Suite
     runs-on: ubuntu-24.04
+    permissions:
+      contents: read
     needs: validate
 
     services:
EOF
@@ -74,6 +74,8 @@
test:
name: Run Test Suite
runs-on: ubuntu-24.04
permissions:
contents: read
needs: validate

services:
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +191 to +236
name: Build and Push Docker Images
runs-on: ubuntu-24.04
needs: [validate, test, create-draft-release]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract version from tag
id: version
run: |
VERSION="${{ github.ref_name }}"
echo "TAG=$VERSION" >> $GITHUB_OUTPUT

- name: Build and push multi-platform images
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
target: production
push: true
platforms: linux/amd64,linux/arm64
tags: |
mitre/vulcan:${{ steps.version.outputs.TAG }}
mitre/vulcan:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: mitre/vulcan
short-description: "STIG-ready security guidance documentation platform"
readme-filepath: ./README.md

publish-release:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 3 months ago

To fix the problem, explicitly restrict the GITHUB_TOKEN permissions for the build-docker job by adding a permissions block that grants only read access to repository contents. This ensures the job does not inherit potentially broader repository defaults (such as contents: write), satisfying the least-privilege recommendation and the CodeQL rule.

Concretely, in .github/workflows/release.yml, within the build-docker job definition (lines around 190–195), add:

permissions:
  contents: read

just under needs: [validate, test, create-draft-release] (or directly under runs-on:—order among job-level keys does not matter in YAML). No additional imports or methods are required, and no changes are needed to any steps in the job. This will keep existing functionality intact because the job only needs to check out code and read README.md, which are compatible with contents: read.

Suggested changeset 1
.github/workflows/release.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -191,6 +191,8 @@
     name: Build and Push Docker Images
     runs-on: ubuntu-24.04
     needs: [validate, test, create-draft-release]
+    permissions:
+      contents: read
     steps:
       - name: Checkout
         uses: actions/checkout@v4
EOF
@@ -191,6 +191,8 @@
name: Build and Push Docker Images
runs-on: ubuntu-24.04
needs: [validate, test, create-draft-release]
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
Copilot is powered by AI and may make mistakes. Always verify output.
project_ids = current_user.available_projects.pluck(:id)

Component.where(project_id: project_ids)
.where(*build_ilike_conditions(%w[name prefix]))

Check failure

Code scanning / CodeQL

SQL query built from user-controlled sources High

This SQL query depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

In general, the safest way to fix SQL injection risks in Rails is to avoid manually constructing SQL strings and instead rely on ActiveRecord’s parameterization mechanisms: hashes, arrays with placeholders, or AREL predicates. We should ensure that user-supplied data is only ever used as bound parameters, and never interpolated directly into the query string.

In this case, the primary concern is build_ilike_conditions, which returns a ["sql with ? placeholders", *values] array that is splatted into where. Although the values are safe, the pattern of manually building SQL strings is what triggers the CodeQL alert. The best fix without changing functionality is to refactor build_ilike_conditions so that it no longer returns a raw SQL string, but instead constructs an Arel::Nodes::Node condition tree; we can then pass that node directly to where, which is fully parameterized by Rails/AREL. We keep the existing behavior—OR-ing across both terms and columns, and using %term% ILIKE matching—while letting AREL handle bindings.

Concretely, in app/controllers/api/search_controller.rb, we will:

  • Replace the current build_ilike_conditions implementation with one that:
    • Returns an Arel::Nodes::Node instead of [sql, *values].
    • Uses arel_table[col].matches("%#{term}%", nil, true) (or matches with Arel::Nodes::NamedFunction.new('LOWER', ...) if needed), combined with .or across columns and terms.
  • Update the two call sites:
    • current_user.available_projects.where(*build_ilike_conditions(%w[name description]))...where(build_ilike_conditions(Project, %w[name description]))
    • Component.where(project_id: project_ids).where(*build_ilike_conditions(%w[name prefix]))...where(build_ilike_conditions(Component, %w[name prefix]))
  • Change the build_ilike_conditions method signature to accept the model class, e.g. def build_ilike_conditions(model_class, columns) and use model_class.arel_table to build column references.

No new imports are needed; AREL is available as part of ActiveRecord. Functionality will remain the same: it still performs an OR across all columns and all search terms, but in a way that is clearly parameterized and friendlier to static analysis.


Suggested changeset 1
app/controllers/api/search_controller.rb

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/controllers/api/search_controller.rb b/app/controllers/api/search_controller.rb
--- a/app/controllers/api/search_controller.rb
+++ b/app/controllers/api/search_controller.rb
@@ -49,7 +49,7 @@
       return [] unless current_user
 
       current_user.available_projects
-                  .where(*build_ilike_conditions(%w[name description]))
+                  .where(build_ilike_conditions(Project, %w[name description]))
                   .limit(limit)
                   .map do |project|
         {
@@ -68,7 +68,7 @@
       project_ids = current_user.available_projects.pluck(:id)
 
       Component.where(project_id: project_ids)
-               .where(*build_ilike_conditions(%w[name prefix]))
+               .where(build_ilike_conditions(Component, %w[name prefix]))
                .includes(:project)
                .limit(limit)
                .map do |component|
@@ -221,18 +221,33 @@
     # Build ILIKE conditions for multiple search terms across multiple columns
     # Returns array suitable for .where(*result)
     #
-    def build_ilike_conditions(columns)
-      conditions = []
-      values = []
+    def build_ilike_conditions(model_class, columns)
+      table = model_class.arel_table
 
+      # Build an OR condition across all search terms and columns using AREL,
+      # so that all user input is passed as bound parameters.
+      combined_condition = nil
+
       @search_terms.each do |term|
-        column_conditions = columns.map { |col| "#{col} ILIKE ?" }.join(' OR ')
-        conditions << "(#{column_conditions})"
-        # Add the term value for each column
-        columns.size.times { values << "%#{term}%" }
+        next if term.blank?
+
+        term_pattern = "%#{term}%"
+
+        # Build (col1 ILIKE ? OR col2 ILIKE ? ...) for this term
+        term_condition = columns.map do |col|
+          # Using matches with case-insensitive collation via ILIKE
+          table[col].matches(term_pattern)
+        end.reduce { |acc, cond| acc.or(cond) }
+
+        combined_condition = if combined_condition
+                               combined_condition.or(term_condition)
+                             else
+                               term_condition
+                             end
       end
 
-      [conditions.join(' OR ')] + values
+      # If there are no search terms, return a no-op condition
+      combined_condition || Arel::Nodes::SqlLiteral.new('1=1')
     end
 
     ##
EOF
@@ -49,7 +49,7 @@
return [] unless current_user

current_user.available_projects
.where(*build_ilike_conditions(%w[name description]))
.where(build_ilike_conditions(Project, %w[name description]))
.limit(limit)
.map do |project|
{
@@ -68,7 +68,7 @@
project_ids = current_user.available_projects.pluck(:id)

Component.where(project_id: project_ids)
.where(*build_ilike_conditions(%w[name prefix]))
.where(build_ilike_conditions(Component, %w[name prefix]))
.includes(:project)
.limit(limit)
.map do |component|
@@ -221,18 +221,33 @@
# Build ILIKE conditions for multiple search terms across multiple columns
# Returns array suitable for .where(*result)
#
def build_ilike_conditions(columns)
conditions = []
values = []
def build_ilike_conditions(model_class, columns)
table = model_class.arel_table

# Build an OR condition across all search terms and columns using AREL,
# so that all user input is passed as bound parameters.
combined_condition = nil

@search_terms.each do |term|
column_conditions = columns.map { |col| "#{col} ILIKE ?" }.join(' OR ')
conditions << "(#{column_conditions})"
# Add the term value for each column
columns.size.times { values << "%#{term}%" }
next if term.blank?

term_pattern = "%#{term}%"

# Build (col1 ILIKE ? OR col2 ILIKE ? ...) for this term
term_condition = columns.map do |col|
# Using matches with case-insensitive collation via ILIKE
table[col].matches(term_pattern)
end.reduce { |acc, cond| acc.or(cond) }

combined_condition = if combined_condition
combined_condition.or(term_condition)
else
term_condition
end
end

[conditions.join(' OR ')] + values
# If there are no search terms, return a no-op condition
combined_condition || Arel::Nodes::SqlLiteral.new('1=1')
end

##
Copilot is powered by AI and may make mistakes. Always verify output.
def search_stig_documents(limit)
return [] unless current_user

Stig.where(*build_ilike_conditions(%w[name title stig_id]))

Check failure

Code scanning / CodeQL

SQL query built from user-controlled sources High

This SQL query depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

def search_srg_documents(limit)
return [] unless current_user

SecurityRequirementsGuide.where(*build_ilike_conditions(%w[name title srg_id]))

Check failure

Code scanning / CodeQL

SQL query built from user-controlled sources High

This SQL query depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

@gitguardian
Copy link
Copy Markdown

gitguardian bot commented Jan 18, 2026

⚠️ GitGuardian has uncovered 4 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
25086139 Triggered Generic Password 5d5ca50 app/javascript/components/auth/tests/PasswordInput.spec.ts View secret
23525922 Triggered Generic Password e32a867 cli/cmd/setup_test.go View secret
23525923 Triggered Generic Password f5ac2e3 app/javascript/composables/tests/useProfile.spec.ts View secret
23525924 Triggered Generic Password bb0a44e docker-compose.dev.yml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Generated by bundler for vite_rails gem. Required for Rails + Vite
integration and setup scripts.

Fixes setup failure when bin/vite is missing after fresh checkout.

Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
Updated all consent banner tests to include new title and titleAlign
configuration properties added in Session 131-132.

Authored by: Aaron Lippold<lippold@gmail.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
100 Security Hotspots
3.3% Duplication on New Code (required ≤ 3%)
C Security Rating on New Code (required ≥ A)
E Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants