Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
693671a
update to 2.3.3 (#712)
wdower Apr 6, 2026
fe94131
fix: eliminate N+1 SRG query in Rule#as_json
aaronlippold Apr 6, 2026
1a89fd9
fix: exclude xml blobs from search_stigs and search_srgs
aaronlippold Apr 6, 2026
9fa342a
fix: exclude xml from STIG/SRG show HTML to_json
aaronlippold Apr 6, 2026
6b542b0
feat: add Blueprinter with leaf and supporting blueprints
aaronlippold Apr 7, 2026
46226b8
feat: RuleBlueprint with navigator, viewer, editor views
aaronlippold Apr 7, 2026
b34bfc9
feat: StigBlueprint and SrgBlueprint with xml exclusion
aaronlippold Apr 7, 2026
0af8f95
feat: ComponentBlueprint with index, show, editor views
aaronlippold Apr 7, 2026
56f7863
feat: migrate controllers to use Blueprint.render
aaronlippold Apr 7, 2026
dbf737f
chore: migrate remaining controllers, deprecate as_json
aaronlippold Apr 7, 2026
b16289a
feat: complete Blueprinter migration, remove as_json
aaronlippold Apr 7, 2026
843eb46
chore: fix Gemfile string quotes (rubocop)
aaronlippold Apr 7, 2026
9d3e8e6
fix: show all password management options for admins
aaronlippold Apr 7, 2026
b24bb7c
fix: address Copilot review feedback
aaronlippold Apr 7, 2026
431496b
fix: query performance hardening (H2, H3, M1-M4) + backtrace logging
aaronlippold Apr 7, 2026
83ca80e
chore: bump v2.3.4, Ruby 3.4.9 docs, changelog
aaronlippold Apr 7, 2026
ccff67a
fix: resolve Copilot double-encoding review comments
aaronlippold Apr 7, 2026
86bcaed
fix: avoid Project.pluck(:id) for admin access requests
aaronlippold Apr 7, 2026
602d96f
chore: fix SonarCloud S1192 duplicated string literals in specs
aaronlippold Apr 7, 2026
6a93d10
fix: resolve N+1, missing data, and silent query failures in Blueprin…
wdower Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v2.3.4] - 2026-04-07

### Added

- Blueprinter JSON serialization framework with 15 blueprint classes and context-specific views (:index, :show, :editor, :navigator, :viewer)
- blueprinter-activerecord auto-preloader for automatic N+1 prevention
- Oj fast JSON generator (~2x faster than stdlib)
- Rule and Review test factories
- 12 query performance regression tests
- Session auth method tracking (session[:auth_method]) — distinguishes "signed in via" from "account linked to"
- Unlink identity feature with password verification
- VULCAN_AUTO_LINK_USER global setting for automatic provider-to-local account linking
- Admin password management UI: always show all options regardless of SMTP configuration

### Changed

- All controllers migrated from to_json(methods:[]) to Blueprint.render
- All model as_json overrides removed (BaseRule, Rule, Review, Membership)
- Project#details consolidated from 9 COUNT queries to 3 (GROUP BY)
- Project#available_members uses SQL WHERE NOT IN instead of Ruby set subtraction
- Project#available_components uses .select() for column filtering
- Component#reviews uses pluck(:id, :rule_id) instead of loading full rule objects
- Rule creation uses DB lookup instead of parsing multi-MB XML
- UsersController audit query bounded with .limit(200)
- ApplicationController check_access_request_notifications rewritten (N+1 → single query)
- Replaced gitlab_omniauth-ldap with omniauth-ldap 2.3.3 (removes nkf VM crash)
- Ruby 3.4.8 → 3.4.9
- Bumped version to v2.3.4

### Fixed

- OIDC provider conflict: symbol/string comparison bug in User.from_omniauth
- Provider+uid-first lookup pattern (GitLab pattern) prevents provider hijacking
- rescue_from ordering: StandardError defined before ProviderConflictError
- Production /stigs crash (R14/R15 memory, H12 timeout) — SeverityCounts concern auto-excludes xml/binary columns
- VulcanAudit bitwise & → && fix for nil rule
- OmniAuth backtrace logging gated on development only — now logs in all environments
- email_verified OIDC claim hardened with ActiveModel::Type::Boolean.new.cast
- Polymorphic membership_type filter in access request notifications
- JSON.parse round-trip eliminated in component show jbuilder (render_as_hash)
- Slack notification firing on every user update instead of only admin changes
- Polymorphic audit query missing user_type filter
- PROJECT_MEMBER_ADMINS normalized from scalar string to array
- UsersTable typeColumn uses falsy check for undefined provider
- Exception message no longer leaked to client in rescue blocks
- update_columns used for password reset token to skip validations
- Visibility chain (stray public keyword) fixed in registrations controller
- valid_password? bcrypt→PBKDF2 rehash side-effect documented at unlink call site

## [v2.3.1] - 2026-03-03

### Added
Expand Down Expand Up @@ -66,7 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Upgraded Ruby from 3.3.9 to 3.4.8 and Puma to 7.2.0
- Upgraded Ruby from 3.3.9 to 3.4.9 and Puma to 7.2.0
- Upgraded Node.js to 24 LTS
- Upgraded PostgreSQL from 12/16 to 18 across Docker, CI, and documentation
- Replaced overcommit with lefthook for git hooks; added pre-push checks for RuboCop, ESLint, and Brakeman
Expand Down Expand Up @@ -312,6 +361,7 @@ For releases prior to v2.1.6, please see the [GitHub releases page](https://gith

---

[v2.3.4]: https://github.com/mitre/vulcan/compare/v2.3.1...v2.3.4
[v2.3.1]: https://github.com/mitre/vulcan/compare/v2.2.0...v2.3.1
[v2.2.1]: https://github.com/mitre/vulcan/compare/v2.2.0...v2.2.1
[v2.2.0]: https://github.com/mitre/vulcan/compare/v2.1.9...v2.2.0
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# =============================================================================

# Make sure versions match .ruby-version
ARG RUBY_VERSION=3.4.8
ARG RUBY_VERSION=3.4.9
ARG NODE_VERSION=24.14.0

# =============================================================================
Expand Down
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,9 @@ gem 'highline', '~> 2.0'
gem 'slack-ruby-client', '1.0.0'
# Slack notification formatting
gem 'slack_block_kit', '0.3.3'

gem 'blueprinter', '~> 1.2'

gem 'blueprinter-activerecord', '~> 1.3'

gem 'oj', '~> 3.16'
10 changes: 10 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ GEM
bigdecimal (3.3.1)
bindata (2.5.1)
bindex (0.8.1)
blueprinter (1.2.1)
blueprinter-activerecord (1.3.0)
activerecord (>= 6.0)
blueprinter (~> 1.0)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.1.0)
Expand Down Expand Up @@ -382,6 +386,9 @@ GEM
rack (>= 1.2, < 4)
snaky_hash (~> 2.0, >= 2.0.3)
version_gem (~> 1.1, >= 1.1.9)
oj (3.16.16)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.3)
hashie (>= 3.4.6)
rack (>= 2.2.3)
Expand Down Expand Up @@ -726,6 +733,8 @@ DEPENDENCIES
activerecord-session_store
amoeba
audited (~> 5.8.0)
blueprinter (~> 1.2)
blueprinter-activerecord (~> 1.3)
bootsnap (>= 1.4.2)
brakeman
bundler-audit
Expand Down Expand Up @@ -753,6 +762,7 @@ DEPENDENCIES
mitre-settingslogic (~> 3.0)
nokogiri
nokogiri-happymapper
oj (~> 3.16)
omniauth (~> 2.1)
omniauth-github
omniauth-ldap (~> 2.3)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ yarn dev # Start dev server
## 🛠️ Technology Stack

### Core Framework
- **Ruby 3.4.8** with **Rails 8.0.2.1**
- **Ruby 3.4.9** with **Rails 8.0.2.1**
- **PostgreSQL 12+** database
- **Node.js 24 LTS** for JavaScript runtime

Expand Down Expand Up @@ -109,7 +109,7 @@ yarn dev # Start dev server

### Prerequisites

- Ruby 3.4.8 (use rbenv or rvm)
- Ruby 3.4.9 (use rbenv or rvm)
- PostgreSQL 12+
- Node.js 24 LTS
- Yarn package manager
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.3.1
v2.3.4
8 changes: 8 additions & 0 deletions app/blueprints/additional_answer_blueprint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

# Serializes AdditionalAnswer for rule editor forms.
class AdditionalAnswerBlueprint < Blueprinter::Base
identifier :id

fields :additional_question_id, :answer
end
13 changes: 13 additions & 0 deletions app/blueprints/check_blueprint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

# Serializes Check records for rule detail views and editors.
class CheckBlueprint < Blueprinter::Base
identifier :id

fields :system, :content_ref_name, :content_ref_href, :content

# Rails accepts_nested_attributes_for expects _destroy key
field :_destroy do |_check, _options|
false
end
end
118 changes: 118 additions & 0 deletions app/blueprints/component_blueprint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# frozen_string_literal: true

# Serializes Component records with context-specific views.
#
# Views:
# :index — listing page (minimal fields + severity_counts)
# :show — non-member read-only view (adds rules, reviews)
# :editor — full editing page (adds histories, memberships, metadata, etc.)
#
# Replaces Component#as_json override and the to_json(methods: [...]) pattern.
# The `admins` method is intentionally excluded — Vue analysis confirmed no
# component page consumer reads component.admins (it's only used on project pages).
class ComponentBlueprint < Blueprinter::Base
identifier :id

# === Default: fields shared by ALL views ===
fields :name, :prefix, :version, :release

field :based_on_title do |component, _options|
component.based_on&.title
end

field :based_on_version do |component, _options|
component.based_on&.version
end

field :severity_counts do |component, _options|
component.severity_counts_hash
end

# === Index view: listing page ===
view :index do
fields :updated_at, :released
end

# === Related view: related_rules parents (includes project for display name) ===
view :related do
fields :updated_at, :released

field :project do |component, _options|
ProjectBlueprint.render_as_hash(component.project)
end
end

# === Show view: non-member read-only ===
view :show do
fields :title, :description, :admin_name, :admin_email, :released, :updated_at

association :rules, blueprint: RuleBlueprint, view: :viewer do |component, _options|
component.rules
end

# Uses Component#reviews method (not ReviewBlueprint) because it returns
# pre-formatted hashes with `displayed_rule_name` that ReviewBlueprint lacks.
field :reviews do |component, _options|
component.reviews
end
end

# === Editor view: full editing page ===
view :editor do
# All DB columns needed by Vue components
fields :title, :description, :admin_name, :admin_email,
:released, :advanced_fields, :project_id, :component_id,
:security_requirements_guide_id, :memberships_count,
:rules_count, :updated_at, :created_at

field :releasable do |component, _options|
component.releasable
end

field :status_counts do |component, _options|
component.status_counts
end

field :additional_questions do |component, _options|
component.additional_questions.as_json
end

# Rules via RuleBlueprint :editor view
association :rules, blueprint: RuleBlueprint, view: :editor do |component, _options|
component.rules
end

# Uses Component#reviews method (not ReviewBlueprint) because it returns
# pre-formatted hashes with `displayed_rule_name` that ReviewBlueprint lacks.
field :reviews do |component, _options|
component.reviews
end

field :histories do |component, _options|
component.histories
end

# Memberships via MembershipBlueprint (includes name, email from user)
association :memberships, blueprint: MembershipBlueprint do |component, _options|
component.memberships
end

field :metadata do |component, _options|
component.metadata
end

association :inherited_memberships, blueprint: MembershipBlueprint do |component, _options|
component.inherited_memberships
end

# Available members — uses SQL NOT IN (not Ruby subtraction)
association :available_members, blueprint: UserBlueprint do |component, _options|
component.available_members
end

# All users with access — compact (id, name, email only)
association :all_users, blueprint: UserBlueprint do |component, _options|
component.all_users
end
end
end
16 changes: 16 additions & 0 deletions app/blueprints/disa_rule_description_blueprint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

# Serializes DISA rule description fields for rule detail and editor views.
class DisaRuleDescriptionBlueprint < Blueprinter::Base
identifier :id

fields :vuln_discussion, :false_positives, :false_negatives,
:documentable, :mitigations, :severity_override_guidance,
:potential_impacts, :third_party_tools, :mitigation_control,
:responsibility, :ia_controls, :mitigations_available,
:poam_available, :poam

field :_destroy do |_drd, _options|
false
end
end
18 changes: 18 additions & 0 deletions app/blueprints/membership_blueprint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

# Replaces Membership#as_json which added `methods: [:name, :email]`.
class MembershipBlueprint < Blueprinter::Base
identifier :id

fields :user_id, :role, :membership_type, :membership_id

# Delegated from user — avoids N+1 when user is eager-loaded via
# the has_many :memberships, -> { includes :user } scope
field :name do |membership, _options|
membership.user&.name
end

field :email do |membership, _options|
membership.user&.email
end
end
55 changes: 55 additions & 0 deletions app/blueprints/project_blueprint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

# Serializes Project records with context-specific views.
class ProjectBlueprint < Blueprinter::Base
identifier :id

fields :name, :description, :visibility, :memberships_count,
:admin_name, :admin_email, :created_at, :updated_at

view :index do
association :memberships, blueprint: MembershipBlueprint do |project, _options|
project.memberships
end
end

view :show do
field :details do |project, _options|
project.details
end

field :histories do |project, _options|
project.histories
end

field :metadata do |project, _options|
project.project_metadata&.data
end

association :memberships, blueprint: MembershipBlueprint do |project, _options|
project.memberships
end

association :components, blueprint: ComponentBlueprint, view: :index do |project, _options|
project.components
end

association :available_components, blueprint: ComponentBlueprint, view: :index do |project, _options|
project.available_components
end

association :available_members, blueprint: UserBlueprint do |project, _options|
project.available_members
end

association :users, blueprint: UserBlueprint do |project, _options|
project.users
end

field :access_requests do |project, _options|
project.access_requests.eager_load(:user, :project).map do |ar|
{ id: ar.id, user: UserBlueprint.render_as_hash(ar.user), project_id: ar.project_id }
end
end
end
end
Loading
Loading