Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ gem "activestorage"
gem "rake"
gem "rspec"
gem "rubocop-katalyst"
gem "sentry-rails"
gem "solid_cable"
gem "solid_queue"
14 changes: 12 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
katalyst-thermite (1.0.0)
katalyst-thermite (1.1.0)
activesupport
railties

Expand Down Expand Up @@ -226,6 +226,13 @@ GEM
rubocop-rspec (~> 3.5)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
sentry-rails (7.0.0)
railties (>= 5.2.0)
sentry-ruby (~> 7.0.0)
sentry-ruby (7.0.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
logger
solid_cable (4.0.2)
actioncable (>= 7.2)
activejob (>= 7.2)
Expand Down Expand Up @@ -270,6 +277,7 @@ DEPENDENCIES
rake
rspec
rubocop-katalyst
sentry-rails
solid_cable
solid_queue

Expand Down Expand Up @@ -301,7 +309,7 @@ CHECKSUMS
io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
katalyst-thermite (1.0.0)
katalyst-thermite (1.1.0)
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
Expand Down Expand Up @@ -354,6 +362,8 @@ CHECKSUMS
rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
sentry-rails (7.0.0) sha256=6ac6a010e088632e46710ce42db75aa5281a6b52a547efee10568cc2ed80605a
sentry-ruby (7.0.0) sha256=e9616ff521355a983fad404ca575d8bb1f1949a8cac42a7a893dd02156bcdb1a
solid_cable (4.0.2) sha256=084636a67679ad00d23088b33c84047e614bcf41ee559db24b414d83cdc42d03
solid_queue (1.7.0) sha256=6566b70b801d1c317c81bba7bcdd5677c019afac584a30374b4164002ca356d3
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ Available installers:

```shell
bin/rails thermite:install:active_storage
bin/rails thermite:install:content_security_policy
bin/rails thermite:install:docker
bin/rails thermite:install:permissions_policy
bin/rails thermite:install:sentry
bin/rails thermite:install:solid_cable
bin/rails thermite:install:solid_queue
```
Expand Down
2 changes: 1 addition & 1 deletion katalyst-thermite.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Gem::Specification.new do |spec|
spec.name = "katalyst-thermite"
spec.version = "1.0.0"
spec.version = "1.1.0"
spec.authors = ["Katalyst Interactive"]
spec.email = ["developers@katalyst.com.au"]

Expand Down
21 changes: 21 additions & 0 deletions lib/generators/thermite/install/content_security_policy/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Description:
Installs the Katalyst content security policy configuration.

Generates config/initializers/content_security_policy.rb with a
nonce-based policy controlled per deployment via CONTENT_SECURITY_POLICY:

- enforce (default): the policy is enforced
- report: policy violations are reported but not enforced — use this to
trial policy changes and review violations in Sentry before enforcing
- none: no policy header is sent — set this on existing apps that have
not yet reviewed the policy against their content

Violation reports are delivered to Sentry when SENTRY_DSN is set and the
Sentry initializer registers the report URI (see thermite:install:sentry).

See https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/
and the project's Sentry Project Settings > SDK Setup > Security Headers page
(https://<org>.sentry.io/settings/projects/<project>/security-headers/).

Example:
bin/rails generate thermite:install:content_security_policy
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require "rails"
require "rails/generators"

module Thermite
module Install
class ContentSecurityPolicyGenerator < Rails::Generators::Base
def self.source_root
File.expand_path("templates", __dir__)
end

def copy_files
copy_file "config/initializers/content_security_policy.rb"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header

# Enforced by default. Set CONTENT_SECURITY_POLICY=report per deployment to
# trial policy changes and review violations in Sentry, or =none for existing
# apps that have not yet reviewed the policy against their content.
CSP_MODE = ENV.fetch("CONTENT_SECURITY_POLICY", "enforce").freeze

warn "Unknown configuration CONTENT_SECURITY_POLICY=#{CSP_MODE}" unless CSP_MODE.in?(%w[none report enforce])

return if CSP_MODE == "none"

# Deliver violation reports to Sentry (see config/initializers/sentry.rb)
# https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/
# Sentry's report endpoint is shown under Project Settings > SDK Setup > Security Headers,
# e.g. https://<org>.sentry.io/settings/projects/<project>/security-headers/
reporting = if ENV.key?("SENTRY_DSN")
sentry_uri = "https://#{URI(ENV.fetch('SENTRY_DSN')).hostname}"
[sentry_uri, sentry_uri.gsub("ingest.sentry", "ingest.us.sentry")]
else
[]
end

Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src(:self)
policy.frame_ancestors(:self)

# Permissive policy, many browser plugins use custom fonts
policy.font_src(:self, :data, :https)

# Permissive policy, allow data URLs and https (hotlinking, tracking pixels)
policy.img_src(:self, :data, :https)

# Block legacy embeds, such as flash
policy.object_src(:none)

# Restricted policy, all scripts should have nonce or SRI. strict-dynamic
# allows scripts to load children (CSP-3); unsafe-inline and https are
# fallbacks for legacy browsers, ignored when nonces are supported.
policy.script_src(:self, :strict_dynamic, :wasm_unsafe_eval, :unsafe_inline, :https)

# Permissive policy, browser plugins inject inline styles.
# Safari < 26.0 does not support -attr and -elem (https://bugs.webkit.org/show_bug.cgi?id=276931)
policy.style_src(:self, :unsafe_inline, :https)

# Permissive policy, allows external embeds and user browser plugins
policy.frame_src(:self, :https)

# Permissive policy, many browser plugins use connect
policy.connect_src(:self, :data, :https, *reporting)
end

# Generate nonces for permitted importmap, inline scripts, and inline styles.
# A fresh nonce per request is incompatible with conditional GET caching; apps
# that create a session on every request can use the session id instead, see
# https://guides.rubyonrails.org/security.html#adding-a-nonce
config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
config.content_security_policy_nonce_directives = %w[script-src style-src-elem]

# Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
# if the corresponding directives are specified in `content_security_policy_nonce_directives`.
config.content_security_policy_nonce_auto = true

# Report violations without enforcing the policy.
config.content_security_policy_report_only = true if CSP_MODE == "report"
end
11 changes: 11 additions & 0 deletions lib/generators/thermite/install/permissions_policy/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Description:
Installs the Katalyst permissions policy configuration.

Generates config/initializers/permissions_policy.rb with a restrictive
Permissions-Policy header: camera, microphone, geolocation, payment,
gyroscope, and usb are denied, and fullscreen is limited to the app
itself. Allow specific features (e.g. geolocation, payment) only for apps
that use them. The header applies as soon as it is deployed.

Example:
bin/rails generate thermite:install:permissions_policy
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require "rails"
require "rails/generators"

module Thermite
module Install
class PermissionsPolicyGenerator < Rails::Generators::Base
def self.source_root
File.expand_path("templates", __dir__)
end

def copy_files
copy_file "config/initializers/permissions_policy.rb"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

Rails.application.config.permissions_policy do |policy|
policy.camera :none
policy.fullscreen :self
policy.geolocation :none
policy.gyroscope :none
policy.microphone :none
policy.payment :none
policy.usb :none
end
18 changes: 18 additions & 0 deletions lib/generators/thermite/install/sentry/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Description:
Installs the Katalyst Sentry configuration.

Generates config/initializers/sentry.rb with the standard errors-only
data posture for sentry-ruby/sentry-rails 7.x:

- structured logging disabled (no request/SQL logs sent to Sentry)
- explicit data_collection settings (no user info, cookies, bodies,
query params, or database query values)
- error events sanitized with the app's parameter filter
- CSP violations reported to Sentry when a content security policy is
configured (see thermite:install:content_security_policy and
https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/)
- tracing and profiling off unless enabled per deployment via
SENTRY_TRACES_SAMPLE_RATE / SENTRY_PROFILES_SAMPLE_RATE

Example:
bin/rails generate thermite:install:sentry
46 changes: 46 additions & 0 deletions lib/generators/thermite/install/sentry/sentry_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require "rails"
require "rails/generators"

module Thermite
module Install
class SentryGenerator < Rails::Generators::Base
def self.source_root
File.expand_path("templates", __dir__)
end

# Fail early with a helpful message when sentry-rails isn't available.
def verify_sentry!
require "sentry-rails"
rescue LoadError
raise Thor::Error, <<~MSG.strip
thermite:install:sentry requires the sentry-rails gem, which is not available.
Add it to your Gemfile and run `bundle install`:

gem "sentry-rails"
MSG
end

def copy_files
template "config/initializers/sentry.rb"
end

private

# The environments Sentry should report from: every deployed environment,
# derived from config/environments/*.rb so we don't hard-code names like
# "staging" / "uat".
def enabled_environments
environments
.map { |pathname| pathname.basename(".rb").to_s }
.reject { |environment| %w[development test].include?(environment) }
.sort_by { |environment| environment == "production" ? 1 : 0 }
end

def environments
Pathname(destination_root).join("config/environments").glob("*.rb")
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

# Sanitize error events with the application's parameter filter
filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
sanitize = lambda do |event, _hint|
event.extra = filter.filter(event.extra) if event.extra
event.user = filter.filter(event.user) if event.user
event.contexts = filter.filter(event.contexts) if event.contexts
event
end

ActiveSupport.on_load(:active_record) do
version = Rails.root.join("VERSION")

# DSN is set via SENTRY_DSN in ENV
Sentry.init do |config|
config.enabled_environments = %w[<%= enabled_environments.join(" ") %>]
config.release = File.read(version).strip.split("/").last if version.exist?

# Errors only: structured logging sends a log event for every request and
# SQL query. Collecting logs is a per-client data decision.
config.rails.structured_logging.enabled = false

# Data collection posture, based on sentry-ruby 7's `send_default_pii`
pii_headers = %w[forwarded -ip _ip remote via _user -user]

config.data_collection.user_info = false
config.data_collection.cookies.mode = :off
config.data_collection.http_headers.request.terms = pii_headers
config.data_collection.http_headers.response.terms = pii_headers
config.data_collection.http_bodies = []
config.data_collection.url_query_params.mode = :off
config.data_collection.database_query_data = false
config.data_collection.queues = false

config.before_send = sanitize

# Report CSP violations to Sentry (requires a content security policy)
# https://docs.sentry.io/platforms/ruby/guides/rails/security-policy-reporting/
if (policy = Rails.application.config.content_security_policy).present? && config.csp_report_uri.present?
policy.report_uri(config.csp_report_uri)
end

# Tracing and profiling, off unless enabled per deployment.
# Profiling requires the vernier gem.
config.traces_sample_rate = ENV.fetch("SENTRY_TRACES_SAMPLE_RATE", "0").to_f
config.profiles_sample_rate = ENV.fetch("SENTRY_PROFILES_SAMPLE_RATE", "0").to_f
config.profiler_class = Sentry::Vernier::Profiler
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "rails_helper"

require "prism"

require "generators/thermite/install/content_security_policy/content_security_policy_generator"

RSpec.describe Thermite::Install::ContentSecurityPolicyGenerator do
it "completes successfully" do
expect { run_generator }.not_to raise_error
end

it "writes a syntactically valid initializer" do
run_generator

result = Prism.parse_file(File.join(destination_root, "config/initializers/content_security_policy.rb"))
expect(result.errors).to be_empty
end

it "enforces the policy unless overridden per deployment" do
run_generator

assert_file "config/initializers/content_security_policy.rb",
/ENV\.fetch\("CONTENT_SECURITY_POLICY", "enforce"\)/
end

it "allows CSP reports to be delivered to Sentry" do
run_generator

assert_file "config/initializers/content_security_policy.rb", /SENTRY_DSN/
end

it "configures nonces for scripts and styles" do
run_generator

assert_file "config/initializers/content_security_policy.rb",
/content_security_policy_nonce_directives = %w\[script-src style-src-elem\]/
end
end
Loading
Loading