Conversation
- Remove redundant type: declarations in 36 spec files (InferredSpecType) - Use implicit association style in 3 factories (AssociationStyle) - Use have_http_status matcher in stigs_spec (HaveHttpStatus/HttpStatus) - Use have_css matcher in system specs (HaveSelector) Authored by: Aaron Lippold<lippold@gmail.com>
- Add .reload to 5 satisfaction tests to prevent association caching (matches pattern already used by 12 other tests in same context) - Rename jbuilder_index_spec.rb → jbuilder_index.rb to prevent double-loading (support files must not end in _spec.rb) Authored by: Aaron Lippold<lippold@gmail.com>
The srg_id DB column on component rules is nil — the correct value lives in srg_rule.version. The member path worked because as_json overrides it, but the non-member Jbuilder path read the nil column. - Compute srg_id from rule.srg_rule.version in Jbuilder viewer branch - Add satisfies/satisfied_by arrays to non-member view - Add 4 regression tests covering both member and non-member paths Authored by: Aaron Lippold<lippold@gmail.com>
- RuleNavigator: all 3 locations use srg_id with tooltips, nested children ALWAYS show SRG IDs regardless of toggle - RuleSatisfactions: flat srg_id instead of nested srg_rule.version - RulesCodeEditorView: modal uses srg_id, localStorage persists toggles - Rules.vue: satisfaction push uses srg_id field - terminology.js: "Select SRG requirements that this rule satisfies" Authored by: Aaron Lippold<lippold@gmail.com>
Remove Multiselect import/CSS, dead data properties (truncateId, showSRGIdChecked, filteredSelectRules, selectedSatisfiesRuleIds, satisfiesSearchText), dead watch/mounted/beforeUnmount lifecycle, and dead methods (filterRules, addCustomSatisfaction, addMultipleSatisfiedRules, clearSelectedRules, updateShowSRGIdChecked). The real satisfaction modal lives in RulesCodeEditorView.vue. Authored by: Aaron Lippold<lippold@gmail.com>
…ewer RuleList: vertical severity buttons, keyboard navigation with roving tabindex (ArrowUp/Down, Enter/Space), ARIA listbox/option roles, scrollIntoView on focus. RuleOverview: satisfaction display parsed from VulnDiscussion text, truncated SRG ID badges with tooltips, shows "None" when absent. Authored by: Aaron Lippold<lippold@gmail.com>
- RuleOverview: 8 tests for satisfaction display (VulnDiscussion parsing, badges, tooltips, sorted output, empty states) - RuleList: 12 tests for keyboard nav (arrow keys, wrap, Enter/Space), row styling (selected/focused/default), ARIA attributes - RuleEditorHeader: rewritten spec for actual behavior (header display, locked/review state, review actions, readOnly mode) - RuleNavigator: 6 TDD tests for SRG ID display and nested children - Rules.spec: fix stale mock data (version -> srg_id) - terminology.spec: updated assertion for SRG requirements text - rules_spec.rb: backend contract tests for satisfaction shape Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
- Replace random FFaker values with sequential IDs in SRG and STIG factories - Fix rails_helper fixture_paths from spec/fixtures/fixtures to spec/fixtures Authored by: Aaron Lippold<lippold@gmail.com>
- Add 31 tests for RuleSatisfactions component (srg_id display, remove modals, readOnly mode) - Add 10 tests for RulesCodeEditorView satisfaction modal (filterRulesForSatisfies, addMultipleSatisfiedRules, clearSelectedRules) - Verify SRG ID truncation, fallback to prefix-rule_id, and eligible rule filtering Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
- Block rm, git rm, git clean, git checkout ., git reset --hard - Enforced by claude-code-safety-net hooks (cannot be bypassed) Authored by: Aaron Lippold<lippold@gmail.com>
- Strips stale "Satisfies:"/"Satisfied By:" text from vendor_comments and vuln_discussion - Scoped to component Rules only (type='Rule'), leaves SRG/STIG data untouched - 10 tests covering regex patterns, scoping, and SQL strip behavior Authored by: Aaron Lippold<lippold@gmail.com>
…exes - seed_xccdf_spec: 42 tests for title-based and directory-path classification - settings_defaults_spec: 19 tests for config defaults across deployment types - schema_indexes_spec: 4 tests for composite index existence on base_rules Authored by: Aaron Lippold<lippold@gmail.com>
Reusable password input with eye/eye-slash visibility toggle. Uses v-model with internal localValue to prevent Vue #6313 bug where :value prop is cleared when input type changes dynamically. 19 Vitest tests covering rendering, toggle, form integration, accessibility, and Vue #6313 regression. Authored by: Aaron Lippold<lippold@gmail.com>
Register PasswordField in login.js Vue instance and replace bare password_field helpers across all 4 Devise views: - sessions/_local.html.haml (login) - sessions/_ldap.html.haml (LDAP login) - registrations/_form.html.haml (signup: password + confirmation) - passwords/edit.html.haml (reset: password + confirmation) Password reset page also gets Vue mount point (#login div), javascript_include_tag, Bootstrap form-group classes, and btn styling. Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
- Component: add name and title presence validations (prefix already had one) - AdditionalAnswer: null-safe additional_question access + use .present? over .empty? - Membership: guard against nil membership association in permission check - Review: guard against nil user/rule in validate_project_permissions Authored by: Aaron Lippold<lippold@gmail.com>
- Add shoulda-matchers gem to test group - Configure RSpec integration in rails_helper.rb Authored by: Aaron Lippold<lippold@gmail.com>
63 shoulda-matchers tests covering presence, uniqueness, inclusion, numericality, and association validations across Component, Rule, Review, User, Membership, Project, and AdditionalAnswer models. Authored by: Aaron Lippold<lippold@gmail.com>
Component now validates presence of name and title. Updated all spec files that create Components directly to include these required fields, matching what the frontend NewComponentModal already requires. Authored by: Aaron Lippold<lippold@gmail.com>
…mponents Prevent TypeError when name or email is null/undefined in search filter computed properties. Uses (value || '') pattern before .toLowerCase(). Files: MembersModal, MembershipsTable, Project, ProjectsTable, SecurityRequirementsGuidesTable, UsersTable Authored by: Aaron Lippold<lippold@gmail.com>
New specs: MembershipsTable, NewMembership, CommentModal, History, 6 mixins (ConfirmComponentRelease, DisplayedComponent, EmptyObject, FindAndReplace, HistoryGrouping, HumanizedTypes), syntaxHighlighter. Updated: FilterBar (requirement-based rewrite), Stigs. Authored by: Aaron Lippold<lippold@gmail.com>
Prettier double-quotes, semicolons, line-wrapping in 10 spec files. Vue attribute ordering and component option ordering in 7 Vue files. Authored by: Aaron Lippold<lippold@gmail.com>
- Add rubocop:disable for Rails/StrongParametersExpect on require.permit
calls (params.expect breaks nested array attributes)
- Add rubocop:disable for Rails/SkipsModelValidations on soft-delete
update_columns (intentional bypass)
- Use %r{} for regex literals in disa_guide_controller and routes
- Use modifier if for single-line warning log
- Remove extra blank line in rules_spec block body
- Use response.parsed_body instead of JSON.parse(response.body)
Signed-off-by: Will Dower <will@dower.dev>
…ments (B1, B8, B10) - Defensive || [] for additional_answers_attributes in AdditionalAnswerForm - Add onAutoSave callback to useRuleAutosave composable - Change history grouping from per-minute to 5-second interval - Update specs for autosave callback and grouping logic Signed-off-by: Will Dower <will@dower.dev>
- C1: Only show truncation toggle when description > 75 chars - C2: Rename tab to "Generated Control (Read-Only)" - C6: Add inspec to working_copy mode in exportConfig and Registry - C7: Update DiffViewer spec for "Base (older)" / "Compare (newer)" - Update ExportModal spec for working_copy inspec enablement - Add ProjectsTable spec for description truncation Signed-off-by: Will Dower <will@dower.dev>
- Legacy export_helper: generate stub controls for Not Yet Determined rules with impact 0.0 and TODO comment - Auto-populate inspec_control_body on rule creation with commented-out describe block via after_create callback - Add helper specs for NYD stub inclusion and status filtering Signed-off-by: Will Dower <will@dower.dev>
Replace traditional form POST for role changes and data-method delete links with axios calls. Add loading spinner for remove action. UI now updates without full page reload. Signed-off-by: Will Dower <will@dower.dev>
- Add commonmarker gem for server-side markdown rendering (DISA guide) - bin/dev auto-detects available port if 3000 is taken (IPv4+IPv6) - Procfile.dev uses bin/dev for port detection in foreman Signed-off-by: Will Dower <will@dower.dev>
- Move INSPEC_STUB_BODY constant above private section - Remove .freeze on heredoc (immutable in Ruby 3.4) - Fix callback order (after_create before before_destroy) - Add rubocop:disable for update_column in seed callback Signed-off-by: Will Dower <will@dower.dev>
Auto-fix Prettier formatting: multi-line attributes on ComponentCard, RuleRevertModal, RuleActionsToolbar, App navbar, and RulesCodeEditorView. Signed-off-by: Will Dower <will@dower.dev>
…ding - Sanitize attachment filename with File.basename to prevent path traversal - Add authorize_component_access to components#histories action - Seed inspec_control_body after bulk SRG import (bypassed after_create callback) Signed-off-by: Will <will@dower.dev>
Signed-off-by: Will <will@dower.dev>
- CVE-2026-33658 (activestorage DoS): Rails 8.0.4 → 8.0.5 - CVE-2026-32700 (devise email confirm race): devise 4.9.4 → 5.0.3 - CVE-2026-33306 (bcrypt JRuby overflow): bcrypt 3.1.21 → 3.1.22 - Loofah improper URI detection: loofah 2.25.0 → 2.25.1 Devise 5 breaking change (secret key lookup) has no impact — Vulcan uses SECRET_KEY_BASE env var which maps to application.secret_key_base, the only source Devise 5 checks. No credentials.yml or secrets.yml exist. No outstanding user tokens will be invalidated. bundler-audit: 0 vulnerabilities Signed-off-by: Will Dower <will@dower.dev>
- Add RuleRevertModal spec (12 tests): two-phase flow, revert action, reset on hide, button text, modal open/close - Add AdditionalAnswerForm spec (6 tests): defensive || [] init for undefined/null attributes, answer CRUD, URL validation - Add MembershipsTable AJAX specs (5 tests): role change PUT, remove DELETE, confirm cancel, loading state, memberRemoved event Test count: 2008 (was 1985, +23 new tests) Signed-off-by: Will Dower <will@dower.dev>
Upstream merge overwrote the fix from 1dfaad9. Component factory uses bulk import (skips callbacks), so the test must create a rule individually to exercise the after_create seed_inspec_control_body callback. Signed-off-by: Will Dower <will@dower.dev>
Read demo admin password from VULCAN_SEED_ADMIN_PASSWORD env var with fallback default for local dev convenience. Add default value to GitGuardian ignored_matches. Stop printing password in seed output. Document override in .env.example. Signed-off-by: Will Dower <will@dower.dev>
- Fix undefined method: export_inspec → export_inspec_component - Update export_inspec_project tests to filter for inspec.yml entries only, since NYD stub controls now add .rb files to the zip Signed-off-by: Will Dower <will@dower.dev>
- Registry spec: expect [:csv, :excel, :inspec] for working_copy - Authorization coverage: add disa_guide#show and disa_guide#attachment to AUTHENTICATE_ONLY_ACTIONS (uses authorize_logged_in) Signed-off-by: Will Dower <will@dower.dev>
SonarCloud flags workflow-level permissions as a security concern. Move contents:read to build job, pages:write and id-token:write to deploy job, per GitHub Actions security best practices. Signed-off-by: Will Dower <will@dower.dev>
- Dockerfile: add --chmod=755 to COPY to prevent write permissions on copied application files - release.yml: pin all GitHub Actions to full commit SHAs instead of mutable version tags (supply chain security best practice) Signed-off-by: Will Dower <will@dower.dev>
- Extract FIELD_ARTIFACT_DESCRIPTION and NO_FILE_PROVIDED constants - Use RuleConstants::STATUS_APPLICABLE_CONFIGURABLE instead of string - Add default clause to DISA status case statement - Replace pluck(:id) with .ids in controllers (5 occurrences) - Use optional chaining in useRuleAutosave - Use structuredClone instead of _.cloneDeep in Rules.vue - Use globalThis instead of window in App.vue - Merge duplicate html selectors in application.scss - Move require_relative to top of health_check initializer - Refactor seed_component to use keyword args (was 8 positional params) - Use ArgumentError instead of raising a string literal in seeds Signed-off-by: Will Dower <will@dower.dev>
Dockerfile optimizations (1.1GB → 487MB): - Remove unused libvips42 (image_processing gem is disabled) - Strip gem build artifacts (.o, .c, .h) from bundle - Consolidate bootsnap + asset precompile + cleanup into single layer - Remove docs, dotfiles, yarn.lock, package.json from production image - Combine user creation + dir setup + bundle strip into single RUN Compose file rename (convention alignment): - docker-compose.prod.yml → docker-compose.yml (default for deployment) - docker-compose.yml → docker-compose.dev.yml (database for native dev) - docker compose up now runs the full app (the expected default) - bin/setup updated to use -f docker-compose.dev.yml - Add web service to dev compose for optional containerized dev - Update all doc references (README, setup guides, env docs) Signed-off-by: Will Dower <will@dower.dev>
Docker silently binds 0.0.0.0:3000 even when another app occupies the port on a specific address. Use VULCAN_PORT env var (default 3000) so users can override: VULCAN_PORT=3001 docker compose up Signed-off-by: Will Dower <will@dower.dev>
Rails production defaults force_ssl to true, which redirects all HTTP to HTTPS. Without a reverse proxy providing SSL termination, this causes ERR_SSL_PROTOCOL_ERROR. Default to false in compose environment so docker compose up works out of the box. Users enable SSL when configuring caddy/nginx proxy profiles. Signed-off-by: Will Dower <will@dower.dev>
projects#histories was missing authorization, allowing any authenticated user to view project history. Add to authorize_viewer_project before_action to require project membership. Fixes authorization_coverage_spec failure. Signed-off-by: Will Dower <will@dower.dev>
seed_component was refactored from named keyword params to **opts with fetch(). Update spec to check for .fetch(:title) instead of matching title in the method signature. Signed-off-by: Will Dower <will@dower.dev>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 114 out of 797 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def show | ||
| @stig_json = @stig.to_json(methods: %i[stig_rules]) | ||
| # Eager load associations for performance (set_stig loads basic STIG) | ||
| @stig = Stig.includes(stig_rules: %i[disa_rule_descriptions checks]).find_by(id: params[:id]) |
There was a problem hiding this comment.
find_by can return nil, which will cause a 500 when @stig.to_json is called in the HTML responder. Use find(params[:id]) (and let RecordNotFound be handled) or explicitly handle the nil case consistently with set_stig (flash + redirect) to avoid a runtime error.
| @stig = Stig.includes(stig_rules: %i[disa_rule_descriptions checks]).find_by(id: params[:id]) | |
| @stig = Stig.includes(stig_rules: %i[disa_rule_descriptions checks]).find(params[:id]) |
| not_determined_msg = "The following controls are 'Not Yet Determined': #{not_determined_controls}" if not_determined_controls.present? | ||
|
|
||
| # Identify rules that can't be locked due to incomplete data (B10: warn but proceed) | ||
| skipped_ids = Set.new |
There was a problem hiding this comment.
Set is from Ruby's stdlib and typically requires require 'set'. Without it, this can raise NameError: uninitialized constant Set at runtime. Add require 'set' (e.g., at the top of this file) or replace Set usage with an Array + uniq/include? logic.
| rule.update!(locked_fields: fields) | ||
| action_word = locked ? 'Locked' : 'Unlocked' | ||
| rule.audits.create!( | ||
| action: 'update', | ||
| audited_changes: { 'locked_fields' => [old_fields, fields] }, | ||
| user: current_user, | ||
| comment: comment.presence || "#{action_word} sections: #{sections.join(', ')}" | ||
| ) |
There was a problem hiding this comment.
This will likely produce duplicate audit entries: update! on an Audited model already creates an audit record, and then a second audit is created manually. Prefer setting rule.audit_comment = ... before update! (similar to RulesController#section_locks) so a single audit entry is recorded with the desired comment.
| rule.update!(locked_fields: fields) | |
| action_word = locked ? 'Locked' : 'Unlocked' | |
| rule.audits.create!( | |
| action: 'update', | |
| audited_changes: { 'locked_fields' => [old_fields, fields] }, | |
| user: current_user, | |
| comment: comment.presence || "#{action_word} sections: #{sections.join(', ')}" | |
| ) | |
| action_word = locked ? 'Locked' : 'Unlocked' | |
| rule.audit_comment = comment.presence || "#{action_word} sections: #{sections.join(', ')}" | |
| rule.update!(locked_fields: fields) |
|
|
||
| try { | ||
| const response = await axios.delete( | ||
| `/projects/${this.membership_id}/project_access_requests/${requestId}`, |
There was a problem hiding this comment.
This DELETE request does not specify a JSON format. If Rails routes default to HTML here, axios may receive a redirect/HTML response instead of JSON. Use a .json suffix (e.g., /.../${requestId}.json) or set an Accept: application/json header to ensure the controller responds with the JSON branch.
| `/projects/${this.membership_id}/project_access_requests/${requestId}`, | |
| `/projects/${this.membership_id}/project_access_requests/${requestId}.json`, |
| async roleChanged(_event, project_member) { | ||
| try { | ||
| const response = await axios.put(`/memberships/${project_member.id}.json`, { | ||
| membership: { role: project_member.role }, | ||
| }); | ||
| this.alertOrNotifyResponse(response); | ||
| } catch (error) { |
There was a problem hiding this comment.
If the role update request fails, the UI will keep showing the new (unsaved) project_member.role because it’s already mutated via v-model. Capture the previous role before the change and revert it in the catch path (or refetch the memberships) so the client state stays consistent with the server.
| async roleChanged(_event, project_member) { | |
| try { | |
| const response = await axios.put(`/memberships/${project_member.id}.json`, { | |
| membership: { role: project_member.role }, | |
| }); | |
| this.alertOrNotifyResponse(response); | |
| } catch (error) { | |
| async roleChanged(_event, project_member) { | |
| const previousRole = project_member.role_was ?? _event?.target?.defaultValue ?? null; | |
| try { | |
| const response = await axios.put(`/memberships/${project_member.id}.json`, { | |
| membership: { role: project_member.role }, | |
| }); | |
| this.alertOrNotifyResponse(response); | |
| } catch (error) { | |
| project_member.role = previousRole; |
| confirmDelete() { | ||
| this.isDeleting = true; | ||
| this.$emit("deleteComponent", this.component.id); | ||
| }, | ||
| resetDelete() { | ||
| this.isDeleting = false; | ||
| this.showDeleteConfirmation = false; | ||
| }, |
There was a problem hiding this comment.
confirmDelete() sets isDeleting = true, but there’s no call site shown that invokes resetDelete() on failure (or on cancel after confirmation). If the delete request fails and the component remains on screen, the overlay will stay stuck in the “Removing component…” state. Consider emitting an event/callback for delete completion/failure so the parent can tell this card to reset, or handle promise outcomes directly in the card.
- Use explicit string for STATUS_APPLICABLE_CONFIGURABLE (not index) - Use find() instead of find_by() in stigs#show to raise RecordNotFound - Fix duplicate audit in reviews bulk_section_locks (use audit_comment before update! instead of manual audits.create!) - Add .json suffix to access request DELETE for consistent JSON response - Revert role on failed membership update to keep UI consistent - Guard satisfied_by/satisfies arrays before .some()/.push() - Sanitize rendered markdown HTML in DISA guide controller - Fix typo: Sucessfully → Successfully - Fix CONTRIBUTING.md duplicate section heading Signed-off-by: Will Dower <will@dower.dev>
|



Summary
Major feature release with 370 commits covering security hardening, UI redesign, export system overhaul, DISA compliance features, and infrastructure modernization.
Highlights
Upgrades
Security
New Features
Testing
Documentation
Test Plan