Skip to content

Vulcan v2.3.6 ​

Released: 2026-05-08

Highlights ​

  • UBI9 base image — container is now built on registry.access.redhat.com/ubi9/ubi-minimal:9.7, with Ruby and jemalloc compiled from source. Aligns Vulcan's container with Iron Bank / DISA compliance requirements. Breaking: Docker Compose volume layout changed — see Migration below.
  • Public-comment review workflow — DISA-style comment lifecycle (pending → concur / concur-w/-comment / non-concur / duplicate / informational / needs-clarification → adjudicated), triage modal with response composer, project- and component-level triage tables, per-user "My Comments" page, and a disposition-matrix CSV export.
  • Viewer-can-comment — project viewers can now post comments and reactions. Higher-tier actions (Save / Approve / Request Changes / Lock) remain gated.
  • Comment reactions (👍/👎) on rule comments and replies, with hover/focus/tap-accessible reactor name list.
  • Structured permission-denied responses — 403 Forbidden JSON bodies now include the project administrator contacts so users know exactly who to ask for access.

Migration ​

⚠️ Read this section before upgrading any existing deployment. Two breaking changes ship in v2.3.6.

1. Docker base image (UBI9) ​

The Vulcan Docker image is now based on Red Hat UBI 9 minimal instead of the Debian-based ruby:3.4.9-slim. Ruby is compiled from source. The image is functionally compatible — environment variables, exposed ports, entrypoint, and CMD are unchanged — so most deployments only need to pull the new tag and restart.

If you build custom downstream images that extend the Vulcan image with apt-get package installs, those commands no longer work. UBI9 uses microdnf (or dnf in the full UBI image). Replace your custom layers accordingly.

2. PostgreSQL volume mount path ​

PostgreSQL 18 recommends mounting at /var/lib/postgresql (the parent directory) rather than /var/lib/postgresql/data, so that future pg_upgrade --link operations don't cross mount boundaries. The shipped docker-compose.yml reflects this:

yaml
# v2.3.5 (old)
volumes:
  - vulcan_dbdata:/var/lib/postgresql/data

# v2.3.6 (new)
volumes:
  - vulcan_dbdata:/var/lib/postgresql

Existing deployments with a populated vulcan_dbdata volume must migrate before pulling the new compose file. If you don't, Postgres will see an empty /var/lib/postgresql/data directory inside the volume and run initdb on startup, creating a fresh empty cluster — your existing data will be invisible to the new container.

Recommended migration (dump + restore):

bash
# 1. While still running v2.3.5: dump the database
docker compose exec -T db pg_dumpall -U postgres > vulcan-dbdata-pre-upgrade.sql

# 2. Pull the v2.3.6 release
git pull   # (or update your deployment's docker-compose.yml manually)

# 3. Bring everything down and remove the old volume
docker compose down -v

# 4. Bring up just the database; it will initdb a fresh cluster at the new path
docker compose up -d db
sleep 10   # let Postgres finish initialization

# 5. Restore your data
docker compose exec -T db psql -U postgres < vulcan-dbdata-pre-upgrade.sql

# 6. Bring up the rest of the stack
docker compose up -d

Alternate path (pin the old volume mount): if dumping/restoring isn't workable in your environment, you can keep the old mount path by overriding it locally:

yaml
# docker-compose.override.yml
services:
  db:
    volumes: !override
      - vulcan_dbdata:/var/lib/postgresql/data

Your data stays in place. You give up future pg_upgrade --link support, but pg_dumpall-based major-version upgrades still work fine.

Added ​

  • Comment reactions (👍/👎) on rule comments and replies. Reactions render as counts on each comment in the rule editor pullout, the comment thread, and the triage modal; click the people-icon to see reactor names (works on hover, focus, and tap — accessible to keyboard and touch users). Reactions are merged into the parent comment's Thread Replies cell in the disposition-matrix CSV export (alongside text replies, in chronological order) as [name · timestamp] reacted thumbs-up entries. Audited via vulcan_audited so the toggle history is preserved.
  • Public-comment review workflow — full lifecycle for the DISA STIG public-comment process:
    • comment_phase on Component (open / closed-adjudicating / closed-finalized) drives whether new comments are accepted and whether triage actions are open.
    • Triage states: pending, concur, concur_with_comment, non_concur, duplicate, informational, needs_clarification, adjudicated, withdrawn.
    • Triage / adjudicate / reopen / withdraw / update endpoints under PATCH /reviews/:id/....
    • Component-level (/components/:id/triage) and project-level (/projects/:id/triage) aggregate triage tables with filtering by status / section / rule / author / search.
    • Per-user "My Comments" page on the user profile, scoped to comments the user authored.
    • Comment composer with section-tagging, dedup banner showing related comments on the same rule, and reply-aware threading.
    • Section-comment icons in the rule editor (per-section comment composer entry points).
    • Disposition-matrix CSV export at /components/:id/export/disposition_csv (admin opt-in for include_email).
  • Comment phase fieldset on the Edit Component modal.
  • Comment-period banner on the Component page when a public comment window is open.
  • Per-rule "Comments" column in the rule navigator with badge for pending counts.
  • Per-project "Comments" badge in the projects-list page with smart deep-link to the relevant component.
  • Re-open and re-triage flows for adjudicated comments, including admin force-withdraw.
  • Cross-rule "satisfies" panel comment counts.
  • Mark-as-duplicate action on comments (with canonical-comment picker + circular-duplicate guard).
  • Edit-comment-section action so triagers can re-tag a mis-tagged section.
  • Rate limits on reaction endpoints: 60 toggles/min/user (POST) and 300 hover-fetches/min/user (GET) via Rack::Attack, with IP fallback for unauthenticated traffic.
  • Rate limits on comment-post endpoints, with 4000-character cap on action='comment' Reviews.
  • Review::VALID_ACTIONS allowlist + inclusion validator on Review#action so unknown action strings no longer save silently as state-mutating no-ops.
  • AlertMixin now renders structured permission-denied responses as a "Permission denied" toast that lists the project administrators (name and email) the user should contact for access — no more silent or generic failures on rejected actions.
  • Triage / disposition i18n + vocabulary parity tests in config/locales/en.yml and app/javascript/constants/triageVocabulary.js.
  • Lifecycle columns on reviews (triage_status, triage_set_by_id, triage_set_at, adjudicated_at, adjudicated_by_id, responding_to_review_id, section, duplicate_of_review_id) with appropriate FK constraints and the partial index supporting the triage queue's natural query shape.
  • comment_phase and comment_period_starts_at / comment_period_ends_at columns on components.

Changed ​

  • BREAKING (Docker): Base image changed from ruby:3.4.9-slim (Debian) to registry.access.redhat.com/ubi9/ubi-minimal:9.7 (Red Hat UBI 9). Ruby is now compiled from source in the build stage. jemalloc is compiled from source and re-enabled via LD_PRELOAD.
  • BREAKING (Docker Compose): PostgreSQL 18 volume mount moved from /var/lib/postgresql/data to /var/lib/postgresql. See Migration above.
  • Project viewers can now post comments on rules. Previously the viewer role was strictly read-only; it now grants read + comment access. To restrict commenting, remove the user's project membership.
  • Authorization rejection responses for JSON requests now return a structured 403 Forbidden body ({ error: 'permission_denied', message, admins: [...], toast }) instead of 500 Internal Server Error. The legacy toast shape is kept alongside so existing AlertMixin consumers keep working unchanged.
  • Component "Reviews" slideover replaced by the full ComponentComments triage table (paginated, filterable, sortable). Old slideover UX removed.
  • All Docker-image-relevant CI workflow actions pinned to actions/checkout@v6.0.2; the docker job runs Compose-up healthcheck verification.
  • /rails source-tree permissions in the production Docker image are tightened (source files 440, bin scripts 550, dirs 550); only db log storage tmp are user-writable at runtime.
  • Reviews controller create action now wraps Review.transaction and rescues ActiveRecord::StatementInvalid to return a graceful 422 with a danger toast instead of leaking 500 on DB errors.

Fixed ​

  • rescue_from ordering bug in ApplicationController — NotAuthorizedError was being shadowed by the catch-all StandardError rescue (ActiveSupport::Rescuable matches handlers via reverse_each, so the LAST-declared rescue wins). The dedicated not_authorized handler was effectively dead code in any non-development environment for JSON requests, surfacing every unauthorized action as 500 instead of the proper 401/403. Reordered so the specific rescue wins.
  • Viewer role was incorrectly able to trigger request_review on unlocked, not-under-review rules (Copilot review of PR #717). Per-action role gate now enforced via ACTION_PERMISSIONS map in the Review model.
  • DoubleRenderError in ReviewsController#lock_controls when partial rule-locking failed.
  • Several backend transaction wraps to prevent orphaned-on-mid-cleanup-failure states (rules#destroy, components#destroy, lock_sections, rule_satisfactions).
  • Vulcan audit attribution chain now scopes correctly across imported reviews.
  • Comment composer modal section selector now uses FilterDropdown (eliminates native-select clipping in long lists).
  • Triage table rule + component links use full page loads (avoids the Vue-pack-vs-Turbolinks load-order race that left blank pages).
  • Backend(1) reaction-throttle CI flake — wrapped reaction-throttle specs in freeze_time so the 60-request loop doesn't span a Rack::Attack 60s bucket boundary on slow runners.
  • SonarCloud reliability: removed two string-literal raises in spec helpers (raise StandardError, ...).

Upgrade ​

  1. Read the Migration section above carefully. The Postgres volume change will silently lose data on existing deployments if not handled.
  2. Pull the new image / update your deployment's docker-compose.yml.
  3. Standard upgrade: bundle exec rails db:migrate if running outside Docker; otherwise the entrypoint runs db:prepare automatically.
  4. Rebuild assets: yarn install && yarn build (only relevant for non-Docker source-based deploys).
  5. Run tests: bundle exec parallel_rspec spec/ && yarn test:unit.

Contributors ​

Aaron Lippold (lippold@gmail.com), Will Dower (will@dower.dev), Amndeep Singh Mann (amann@mitre.org), with Claude (Anthropic) assisting on PR-717 and PR-718.

Part of the MITRE Security Automation Framework (SAF)