From 976df1b91d5777bcdee7f8ec905fe40c571f0f95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:42:13 +0000 Subject: [PATCH 01/12] Initial plan From b3ac340251f2a7f5e4446423211bde1c9450e293 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:48:07 +0000 Subject: [PATCH 02/12] Create Rails engine gem structure with CSP reports collection Co-authored-by: burisu <240595+burisu@users.noreply.github.com> --- .gitignore | 11 ++ Gemfile | 11 ++ MIT-LICENSE | 20 +++ README.md | 146 +++++++++++++++++- Rakefile | 8 + .../reported/csp_reports_controller.rb | 42 +++++ app/jobs/reported/notification_job.rb | 72 +++++++++ app/models/reported/report.rb | 16 ++ config/routes.rb | 3 + .../20240101000000_create_reported_reports.rb | 17 ++ lib/generators/reported/install/README | 33 ++++ .../reported/install/install_generator.rb | 25 +++ .../reported/install/templates/reported.rb | 8 + lib/reported.rb | 11 ++ lib/reported/engine.rb | 10 ++ lib/reported/version.rb | 3 + reported.gemspec | 19 +++ 17 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 MIT-LICENSE create mode 100644 Rakefile create mode 100644 app/controllers/reported/csp_reports_controller.rb create mode 100644 app/jobs/reported/notification_job.rb create mode 100644 app/models/reported/report.rb create mode 100644 config/routes.rb create mode 100644 db/migrate/20240101000000_create_reported_reports.rb create mode 100644 lib/generators/reported/install/README create mode 100644 lib/generators/reported/install/install_generator.rb create mode 100644 lib/generators/reported/install/templates/reported.rb create mode 100644 lib/reported.rb create mode 100644 lib/reported/engine.rb create mode 100644 lib/reported/version.rb create mode 100644 reported.gemspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd23eb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.bundle/ +log/*.log +pkg/ +test/dummy/db/*.sqlite3 +test/dummy/db/*.sqlite3-journal +test/dummy/log/*.log +test/dummy/storage/ +test/dummy/tmp/ +*.gem +.byebug_history +Gemfile.lock diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..b81a79e --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in reported.gemspec +gemspec + +gem "sqlite3" +gem "puma" + +group :development, :test do + gem "rails", "~> 7.0" +end diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 0000000..6f3fdfc --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright 2024 Codeur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 284217b..a865faf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,144 @@ -# reported -CSP reports collection for Rails apps +# Reported + +A Rails engine that collects, stores and notifies on Slack about Content Security Policy (CSP) violation reports. + +## Features + +- Public `/csp-reports` endpoint for browsers to POST CSP violations +- Stores CSP reports in a database table +- Tracks notification status with `notified_at` column +- Optional Slack notifications for CSP violations +- Easy integration with Rails applications + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'reported' +``` + +And then execute: + +```bash +$ bundle install +``` + +Or install it yourself as: + +```bash +$ gem install reported +``` + +## Setup + +1. Run the install generator: + +```bash +$ rails generate reported:install +``` + +This will create an initializer at `config/initializers/reported.rb`. + +2. Run the migrations: + +```bash +$ rails reported:install:migrations +$ rails db:migrate +``` + +This creates the `reported_reports` table. + +3. Mount the engine in your `config/routes.rb`: + +```ruby +Rails.application.routes.draw do + mount Reported::Engine, at: "/reported" + # ... your other routes +end +``` + +This makes the CSP reports endpoint available at `/reported/csp-reports`. + +## Configuration + +### Content Security Policy + +Configure your application's CSP to send reports to the endpoint. In `config/initializers/content_security_policy.rb`: + +```ruby +Rails.application.config.content_security_policy do |policy| + policy.default_src :self, :https + policy.script_src :self, :https + # ... your other CSP directives ... + + # Configure the report URI + policy.report_uri "/reported/csp-reports" +end +``` + +### Slack Notifications + +To enable Slack notifications, configure the initializer at `config/initializers/reported.rb`: + +```ruby +Reported.configuration do |config| + # Enable or disable Slack notifications + config.enabled = true + + # Slack webhook URL for notifications + config.slack_webhook_url = ENV['REPORTED_SLACK_WEBHOOK_URL'] +end +``` + +Get your Slack webhook URL from [Slack API](https://api.slack.com/messaging/webhooks). + +Set the webhook URL as an environment variable: + +```bash +REPORTED_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL +``` + +## Usage + +Once configured, the gem automatically: + +1. Receives CSP violation reports at `/reported/csp-reports` +2. Stores them in the `reported_reports` table +3. Sends notifications to Slack (if enabled) +4. Marks reports as notified with the `notified_at` timestamp + +### Accessing Reports + +You can access reports through the `Reported::Report` model: + +```ruby +# Get all reports +Reported::Report.all + +# Get unnotified reports +Reported::Report.not_notified + +# Get notified reports +Reported::Report.notified + +# Mark a report as notified manually +report = Reported::Report.first +report.mark_as_notified! +``` + +## Database Schema + +The `reported_reports` table includes: + +- `document_uri` - The URI of the document where the violation occurred +- `violated_directive` - The CSP directive that was violated +- `blocked_uri` - The URI that was blocked +- `original_policy` - The complete CSP policy +- `raw_report` - The complete JSON report from the browser +- `notified_at` - Timestamp of when the report was sent to Slack +- `created_at` / `updated_at` - Standard timestamps + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..e7793b5 --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +require "bundler/setup" + +APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) +load "rails/tasks/engine.rake" + +load "rails/tasks/statistics.rake" + +require "bundler/gem_tasks" diff --git a/app/controllers/reported/csp_reports_controller.rb b/app/controllers/reported/csp_reports_controller.rb new file mode 100644 index 0000000..44c2413 --- /dev/null +++ b/app/controllers/reported/csp_reports_controller.rb @@ -0,0 +1,42 @@ +module Reported + class CspReportsController < ActionController::Base + # Skip CSRF token verification for CSP reports + skip_before_action :verify_authenticity_token + + def create + report_data = parse_report_data + + if report_data + report = Report.create!( + document_uri: report_data.dig('csp-report', 'document-uri'), + violated_directive: report_data.dig('csp-report', 'violated-directive'), + blocked_uri: report_data.dig('csp-report', 'blocked-uri'), + original_policy: report_data.dig('csp-report', 'original-policy'), + raw_report: report_data.to_json + ) + + # Send notification if enabled + NotificationJob.perform_later(report.id) if Reported.enabled + + head :no_content + else + head :bad_request + end + rescue => e + Rails.logger.error("Error processing CSP report: #{e.message}") + head :internal_server_error + end + + private + + def parse_report_data + body = request.body.read + return nil if body.blank? + + JSON.parse(body) + rescue JSON::ParserError => e + Rails.logger.error("Error parsing CSP report JSON: #{e.message}") + nil + end + end +end diff --git a/app/jobs/reported/notification_job.rb b/app/jobs/reported/notification_job.rb new file mode 100644 index 0000000..322cf71 --- /dev/null +++ b/app/jobs/reported/notification_job.rb @@ -0,0 +1,72 @@ +require 'net/http' +require 'uri' +require 'json' + +module Reported + class NotificationJob < ActiveJob::Base + queue_as :default + + def perform(report_id) + report = Report.find_by(id: report_id) + return unless report + return if report.notified? + return unless Reported.slack_webhook_url.present? + + send_slack_notification(report) + report.mark_as_notified! + rescue => e + Rails.logger.error("Error sending Slack notification for report #{report_id}: #{e.message}") + raise + end + + private + + def send_slack_notification(report) + uri = URI.parse(Reported.slack_webhook_url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true if uri.scheme == 'https' + + request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') + request.body = notification_payload(report).to_json + + response = http.request(request) + + unless response.code.to_i == 200 + raise "Slack API returned #{response.code}: #{response.body}" + end + end + + def notification_payload(report) + { + text: "CSP Violation Report", + attachments: [ + { + color: "danger", + fields: [ + { + title: "Document URI", + value: report.document_uri || "N/A", + short: false + }, + { + title: "Violated Directive", + value: report.violated_directive || "N/A", + short: true + }, + { + title: "Blocked URI", + value: report.blocked_uri || "N/A", + short: true + }, + { + title: "Reported At", + value: report.created_at.to_s, + short: true + } + ] + } + ] + } + end + end +end diff --git a/app/models/reported/report.rb b/app/models/reported/report.rb new file mode 100644 index 0000000..ea1189a --- /dev/null +++ b/app/models/reported/report.rb @@ -0,0 +1,16 @@ +module Reported + class Report < ApplicationRecord + validates :raw_report, presence: true + + scope :not_notified, -> { where(notified_at: nil) } + scope :notified, -> { where.not(notified_at: nil) } + + def mark_as_notified! + update!(notified_at: Time.current) + end + + def notified? + notified_at.present? + end + end +end diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..f7888f6 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,3 @@ +Reported::Engine.routes.draw do + post 'csp-reports', to: 'csp_reports#create' +end diff --git a/db/migrate/20240101000000_create_reported_reports.rb b/db/migrate/20240101000000_create_reported_reports.rb new file mode 100644 index 0000000..e159443 --- /dev/null +++ b/db/migrate/20240101000000_create_reported_reports.rb @@ -0,0 +1,17 @@ +class CreateReportedReports < ActiveRecord::Migration[6.0] + def change + create_table :reported_reports do |t| + t.string :document_uri + t.string :violated_directive + t.string :blocked_uri + t.text :original_policy + t.text :raw_report, null: false + t.datetime :notified_at + + t.timestamps + end + + add_index :reported_reports, :notified_at + add_index :reported_reports, :created_at + end +end diff --git a/lib/generators/reported/install/README b/lib/generators/reported/install/README new file mode 100644 index 0000000..dcd840a --- /dev/null +++ b/lib/generators/reported/install/README @@ -0,0 +1,33 @@ +=============================================================================== + +Reported has been installed! + +Next steps: + +1. Run the migration to create the reports table: + + rails reported:install:migrations + rails db:migrate + +2. Mount the engine in your routes.rb: + + mount Reported::Engine, at: "/reported" + + This will make the CSP reports endpoint available at /reported/csp-reports + +3. Configure your Content Security Policy to send reports to this endpoint: + + In your config/initializers/content_security_policy.rb: + + Rails.application.config.content_security_policy do |policy| + # ... your CSP directives ... + policy.report_uri "/reported/csp-reports" + end + +4. Configure Slack notifications in config/initializers/reported.rb + +5. Set your Slack webhook URL as an environment variable: + + REPORTED_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL + +=============================================================================== diff --git a/lib/generators/reported/install/install_generator.rb b/lib/generators/reported/install/install_generator.rb new file mode 100644 index 0000000..e5b8fcb --- /dev/null +++ b/lib/generators/reported/install/install_generator.rb @@ -0,0 +1,25 @@ +require 'rails/generators' + +module Reported + module Generators + class InstallGenerator < Rails::Generators::Base + source_root File.expand_path('templates', __dir__) + + desc "Creates Reported initializer for your application" + + def copy_initializer + template "reported.rb", "config/initializers/reported.rb" + + puts "Reported initializer created at config/initializers/reported.rb" + end + + def show_readme + readme "README" if behavior == :invoke + end + + def self.next_migration_number(dirname) + Time.now.utc.strftime("%Y%m%d%H%M%S") + end + end + end +end diff --git a/lib/generators/reported/install/templates/reported.rb b/lib/generators/reported/install/templates/reported.rb new file mode 100644 index 0000000..dea52fe --- /dev/null +++ b/lib/generators/reported/install/templates/reported.rb @@ -0,0 +1,8 @@ +Reported.configuration do |config| + # Enable or disable Slack notifications + config.enabled = true + + # Slack webhook URL for notifications + # Get your webhook URL from https://api.slack.com/messaging/webhooks + config.slack_webhook_url = ENV['REPORTED_SLACK_WEBHOOK_URL'] +end diff --git a/lib/reported.rb b/lib/reported.rb new file mode 100644 index 0000000..853b82b --- /dev/null +++ b/lib/reported.rb @@ -0,0 +1,11 @@ +require "reported/version" +require "reported/engine" + +module Reported + mattr_accessor :slack_webhook_url + mattr_accessor :enabled + + def self.configuration + yield self if block_given? + end +end diff --git a/lib/reported/engine.rb b/lib/reported/engine.rb new file mode 100644 index 0000000..e026817 --- /dev/null +++ b/lib/reported/engine.rb @@ -0,0 +1,10 @@ +module Reported + class Engine < ::Rails::Engine + isolate_namespace Reported + + config.generators do |g| + g.test_framework :test_unit + g.fixture_replacement :factory_bot, dir: 'spec/factories' + end + end +end diff --git a/lib/reported/version.rb b/lib/reported/version.rb new file mode 100644 index 0000000..ed88c26 --- /dev/null +++ b/lib/reported/version.rb @@ -0,0 +1,3 @@ +module Reported + VERSION = "0.1.0" +end diff --git a/reported.gemspec b/reported.gemspec new file mode 100644 index 0000000..ab2a493 --- /dev/null +++ b/reported.gemspec @@ -0,0 +1,19 @@ +require_relative "lib/reported/version" + +Gem::Specification.new do |spec| + spec.name = "reported" + spec.version = Reported::VERSION + spec.authors = ["Codeur"] + spec.email = ["contact@codeur.com"] + spec.homepage = "https://github.com/codeur/reported" + spec.summary = "CSP reports collection for Rails apps" + spec.description = "A Rails engine that collects, stores and notifies on Slack about CSP violation reports" + spec.license = "MIT" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/codeur/reported" + + spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] + + spec.add_dependency "rails", ">= 6.0" +end From 8e48e555683294873a88bc85c0d61368cefb6a0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:50:27 +0000 Subject: [PATCH 03/12] Add comprehensive test suite with model, controller, and job tests Co-authored-by: burisu <240595+burisu@users.noreply.github.com> --- reported.gemspec | 3 + .../csp_reports_controller_test.rb | 72 ++++++++++++++++++ test/dummy/Rakefile | 3 + test/dummy/config/application.rb | 15 ++++ test/dummy/config/database.yml | 3 + test/dummy/config/environment.rb | 5 ++ test/dummy/config/routes.rb | 3 + test/jobs/notification_job_test.rb | 74 +++++++++++++++++++ test/models/report_test.rb | 48 ++++++++++++ test/test_helper.rb | 31 ++++++++ 10 files changed, 257 insertions(+) create mode 100644 test/controllers/csp_reports_controller_test.rb create mode 100644 test/dummy/Rakefile create mode 100644 test/dummy/config/application.rb create mode 100644 test/dummy/config/database.yml create mode 100644 test/dummy/config/environment.rb create mode 100644 test/dummy/config/routes.rb create mode 100644 test/jobs/notification_job_test.rb create mode 100644 test/models/report_test.rb create mode 100644 test/test_helper.rb diff --git a/reported.gemspec b/reported.gemspec index ab2a493..ec55ad3 100644 --- a/reported.gemspec +++ b/reported.gemspec @@ -16,4 +16,7 @@ Gem::Specification.new do |spec| spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] spec.add_dependency "rails", ">= 6.0" + + spec.add_development_dependency "sqlite3" + spec.add_development_dependency "webmock" end diff --git a/test/controllers/csp_reports_controller_test.rb b/test/controllers/csp_reports_controller_test.rb new file mode 100644 index 0000000..38ce042 --- /dev/null +++ b/test/controllers/csp_reports_controller_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' + +module Reported + class CspReportsControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + setup do + @valid_csp_report = { + "csp-report" => { + "document-uri" => "https://example.com/page", + "violated-directive" => "script-src 'self'", + "blocked-uri" => "https://evil.com/script.js", + "original-policy" => "default-src 'self'; script-src 'self'" + } + } + end + + test "creates report with valid CSP data" do + assert_difference 'Report.count', 1 do + post csp_reports_url, + params: @valid_csp_report.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } + end + + assert_response :no_content + + report = Report.last + assert_equal "https://example.com/page", report.document_uri + assert_equal "script-src 'self'", report.violated_directive + assert_equal "https://evil.com/script.js", report.blocked_uri + end + + test "returns bad_request with invalid JSON" do + assert_no_difference 'Report.count' do + post csp_reports_url, + params: "invalid json{", + headers: { 'CONTENT_TYPE' => 'application/json' } + end + + assert_response :bad_request + end + + test "returns bad_request with empty body" do + assert_no_difference 'Report.count' do + post csp_reports_url, + params: "", + headers: { 'CONTENT_TYPE' => 'application/json' } + end + + assert_response :bad_request + end + + test "stores raw_report as JSON" do + post csp_reports_url, + params: @valid_csp_report.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } + + report = Report.last + parsed = JSON.parse(report.raw_report) + assert_equal @valid_csp_report, parsed + end + + test "does not require CSRF token" do + # This test verifies that external browsers can POST without CSRF token + post csp_reports_url, + params: @valid_csp_report.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } + + assert_response :no_content + end + end +end diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile new file mode 100644 index 0000000..ed646b6 --- /dev/null +++ b/test/dummy/Rakefile @@ -0,0 +1,3 @@ +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb new file mode 100644 index 0000000..f73331a --- /dev/null +++ b/test/dummy/config/application.rb @@ -0,0 +1,15 @@ +require_relative '../../test_helper' +require 'rails/all' + +Bundler.require(*Rails.groups) +require "reported" + +module Dummy + class Application < Rails::Application + config.load_defaults Rails::VERSION::STRING.to_f + config.eager_load = false + + # For compatibility with tests + config.active_storage.service = :test if config.respond_to?(:active_storage) + end +end diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml new file mode 100644 index 0000000..ff8c5d6 --- /dev/null +++ b/test/dummy/config/database.yml @@ -0,0 +1,3 @@ +test: + adapter: sqlite3 + database: db/test.sqlite3 diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb new file mode 100644 index 0000000..8fb6325 --- /dev/null +++ b/test/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application +require_relative 'application' + +# Initialize the Rails application +Rails.application.initialize! diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb new file mode 100644 index 0000000..f3154b5 --- /dev/null +++ b/test/dummy/config/routes.rb @@ -0,0 +1,3 @@ +Rails.application.routes.draw do + mount Reported::Engine => "/reported" +end diff --git a/test/jobs/notification_job_test.rb b/test/jobs/notification_job_test.rb new file mode 100644 index 0000000..1da19e1 --- /dev/null +++ b/test/jobs/notification_job_test.rb @@ -0,0 +1,74 @@ +require 'test_helper' + +module Reported + class NotificationJobTest < ActiveJob::TestCase + setup do + @report = Report.create!( + document_uri: "https://example.com/page", + violated_directive: "script-src 'self'", + blocked_uri: "https://evil.com/script.js", + raw_report: '{"csp-report": {}}' + ) + Reported.slack_webhook_url = "https://hooks.slack.com/services/TEST/WEBHOOK/URL" + end + + test "sends notification to Slack" do + stub_request(:post, Reported.slack_webhook_url) + .to_return(status: 200, body: "ok") + + NotificationJob.perform_now(@report.id) + + assert_requested :post, Reported.slack_webhook_url + end + + test "marks report as notified after successful notification" do + stub_request(:post, Reported.slack_webhook_url) + .to_return(status: 200, body: "ok") + + assert_nil @report.notified_at + + NotificationJob.perform_now(@report.id) + + @report.reload + assert_not_nil @report.notified_at + end + + test "does not send notification if report already notified" do + @report.mark_as_notified! + + stub_request(:post, Reported.slack_webhook_url) + .to_return(status: 200, body: "ok") + + NotificationJob.perform_now(@report.id) + + assert_not_requested :post, Reported.slack_webhook_url + end + + test "does not send notification if webhook URL is not configured" do + Reported.slack_webhook_url = nil + + stub_request(:post, "https://hooks.slack.com/services/TEST/WEBHOOK/URL") + .to_return(status: 200, body: "ok") + + NotificationJob.perform_now(@report.id) + + assert_not_requested :post, "https://hooks.slack.com/services/TEST/WEBHOOK/URL" + end + + test "includes CSP violation details in Slack message" do + stub_request(:post, Reported.slack_webhook_url) + .with { |request| + body = JSON.parse(request.body) + body["text"] == "CSP Violation Report" && + body["attachments"].any? { |a| + a["fields"].any? { |f| f["title"] == "Document URI" && f["value"] == @report.document_uri } + } + } + .to_return(status: 200, body: "ok") + + NotificationJob.perform_now(@report.id) + + assert_requested :post, Reported.slack_webhook_url + end + end +end diff --git a/test/models/report_test.rb b/test/models/report_test.rb new file mode 100644 index 0000000..e799e64 --- /dev/null +++ b/test/models/report_test.rb @@ -0,0 +1,48 @@ +require 'test_helper' + +module Reported + class ReportTest < ActiveSupport::TestCase + test "requires raw_report" do + report = Report.new + assert_not report.valid? + assert_includes report.errors[:raw_report], "can't be blank" + end + + test "can create report with raw_report" do + report = Report.new(raw_report: '{"csp-report": {}}') + assert report.valid? + end + + test "not_notified scope returns reports without notified_at" do + report1 = Report.create!(raw_report: '{"test": 1}') + report2 = Report.create!(raw_report: '{"test": 2}', notified_at: Time.current) + + assert_includes Report.not_notified, report1 + assert_not_includes Report.not_notified, report2 + end + + test "notified scope returns reports with notified_at" do + report1 = Report.create!(raw_report: '{"test": 1}') + report2 = Report.create!(raw_report: '{"test": 2}', notified_at: Time.current) + + assert_not_includes Report.notified, report1 + assert_includes Report.notified, report2 + end + + test "mark_as_notified! sets notified_at" do + report = Report.create!(raw_report: '{"test": 1}') + assert_nil report.notified_at + + report.mark_as_notified! + assert_not_nil report.notified_at + end + + test "notified? returns true when notified_at is set" do + report = Report.create!(raw_report: '{"test": 1}') + assert_not report.notified? + + report.update!(notified_at: Time.current) + assert report.notified? + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..d746fc4 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,31 @@ +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require "minitest/autorun" + +# Placeholder for when tests are run in a Rails application context +# This allows the gem to be tested independently +begin + require File.expand_path("../test/dummy/config/environment", __dir__) +rescue LoadError + # If dummy app doesn't exist, just load the engine + require File.expand_path("../lib/reported", __dir__) +end + +require "rails/test_help" + +# Require webmock for testing HTTP requests +begin + require 'webmock/minitest' + WebMock.disable_net_connect!(allow_localhost: true) +rescue LoadError + # webmock not available +end + +# Load support files +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } + +if ActiveSupport::TestCase.respond_to?(:fixture_path=) + ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) + ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" +end From cdf15739fa61427c896f1a83d3158499fb560d89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:02:25 +0000 Subject: [PATCH 04/12] Add automatic route mounting, support modern CSP format, and GitHub workflows Co-authored-by: burisu <240595+burisu@users.noreply.github.com> --- .github/workflows/test.yml | 31 +++++++++++++ README.md | 13 +----- .../reported/csp_reports_controller.rb | 33 +++++++++++-- config/routes.rb | 3 -- lib/generators/reported/install/README | 8 +--- lib/reported/engine.rb | 7 +++ .../csp_reports_controller_test.rb | 46 +++++++++++++------ test/dummy/config/routes.rb | 2 +- 8 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 config/routes.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8972886 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + ruby-version: ['3.0', '3.1', '3.2'] + rails-version: ['6.1', '7.0', '7.1'] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + + - name: Install dependencies + run: bundle install + + - name: Run tests + run: bundle exec rake test diff --git a/README.md b/README.md index a865faf..beb3aa6 100644 --- a/README.md +++ b/README.md @@ -49,16 +49,7 @@ $ rails db:migrate This creates the `reported_reports` table. -3. Mount the engine in your `config/routes.rb`: - -```ruby -Rails.application.routes.draw do - mount Reported::Engine, at: "/reported" - # ... your other routes -end -``` - -This makes the CSP reports endpoint available at `/reported/csp-reports`. +The CSP reports endpoint is automatically available at `/csp-reports` (no mounting required). ## Configuration @@ -73,7 +64,7 @@ Rails.application.config.content_security_policy do |policy| # ... your other CSP directives ... # Configure the report URI - policy.report_uri "/reported/csp-reports" + policy.report_uri "/csp-reports" end ``` diff --git a/app/controllers/reported/csp_reports_controller.rb b/app/controllers/reported/csp_reports_controller.rb index 44c2413..416d267 100644 --- a/app/controllers/reported/csp_reports_controller.rb +++ b/app/controllers/reported/csp_reports_controller.rb @@ -7,11 +7,14 @@ def create report_data = parse_report_data if report_data + # Extract CSP report data, supporting both old and new formats + csp_data = extract_csp_data(report_data) + report = Report.create!( - document_uri: report_data.dig('csp-report', 'document-uri'), - violated_directive: report_data.dig('csp-report', 'violated-directive'), - blocked_uri: report_data.dig('csp-report', 'blocked-uri'), - original_policy: report_data.dig('csp-report', 'original-policy'), + document_uri: csp_data[:document_uri], + violated_directive: csp_data[:violated_directive], + blocked_uri: csp_data[:blocked_uri], + original_policy: csp_data[:original_policy], raw_report: report_data.to_json ) @@ -38,5 +41,27 @@ def parse_report_data Rails.logger.error("Error parsing CSP report JSON: #{e.message}") nil end + + def extract_csp_data(report_data) + # Support both old format (csp-report) and new format (direct fields) + if report_data['csp-report'] + # Old format: {"csp-report": {...}} + csp_report = report_data['csp-report'] + { + document_uri: csp_report['document-uri'] || csp_report['documentURI'], + violated_directive: csp_report['violated-directive'] || csp_report['violatedDirective'] || csp_report['effective-directive'] || csp_report['effectiveDirective'], + blocked_uri: csp_report['blocked-uri'] || csp_report['blockedURI'], + original_policy: csp_report['original-policy'] || csp_report['originalPolicy'] + } + else + # New format: direct fields or camelCase + { + document_uri: report_data['document-uri'] || report_data['documentURI'] || report_data['document_uri'], + violated_directive: report_data['violated-directive'] || report_data['violatedDirective'] || report_data['effective-directive'] || report_data['effectiveDirective'] || report_data['violated_directive'], + blocked_uri: report_data['blocked-uri'] || report_data['blockedURI'] || report_data['blocked_uri'], + original_policy: report_data['original-policy'] || report_data['originalPolicy'] || report_data['original_policy'] + } + end + end end end diff --git a/config/routes.rb b/config/routes.rb deleted file mode 100644 index f7888f6..0000000 --- a/config/routes.rb +++ /dev/null @@ -1,3 +0,0 @@ -Reported::Engine.routes.draw do - post 'csp-reports', to: 'csp_reports#create' -end diff --git a/lib/generators/reported/install/README b/lib/generators/reported/install/README index dcd840a..a7a5f6a 100644 --- a/lib/generators/reported/install/README +++ b/lib/generators/reported/install/README @@ -9,11 +9,7 @@ Next steps: rails reported:install:migrations rails db:migrate -2. Mount the engine in your routes.rb: - - mount Reported::Engine, at: "/reported" - - This will make the CSP reports endpoint available at /reported/csp-reports +2. The CSP reports endpoint is automatically available at /csp-reports 3. Configure your Content Security Policy to send reports to this endpoint: @@ -21,7 +17,7 @@ Next steps: Rails.application.config.content_security_policy do |policy| # ... your CSP directives ... - policy.report_uri "/reported/csp-reports" + policy.report_uri "/csp-reports" end 4. Configure Slack notifications in config/initializers/reported.rb diff --git a/lib/reported/engine.rb b/lib/reported/engine.rb index e026817..f590457 100644 --- a/lib/reported/engine.rb +++ b/lib/reported/engine.rb @@ -6,5 +6,12 @@ class Engine < ::Rails::Engine g.test_framework :test_unit g.fixture_replacement :factory_bot, dir: 'spec/factories' end + + # Automatically add routes to the main application + initializer "reported.add_routes" do |app| + app.routes.prepend do + post '/csp-reports', to: 'reported/csp_reports#create' + end + end end end diff --git a/test/controllers/csp_reports_controller_test.rb b/test/controllers/csp_reports_controller_test.rb index 38ce042..9dfdf1d 100644 --- a/test/controllers/csp_reports_controller_test.rb +++ b/test/controllers/csp_reports_controller_test.rb @@ -2,10 +2,8 @@ module Reported class CspReportsControllerTest < ActionDispatch::IntegrationTest - include Engine.routes.url_helpers - setup do - @valid_csp_report = { + @valid_csp_report_old_format = { "csp-report" => { "document-uri" => "https://example.com/page", "violated-directive" => "script-src 'self'", @@ -13,12 +11,34 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest "original-policy" => "default-src 'self'; script-src 'self'" } } + + @valid_csp_report_new_format = { + "documentURI" => "https://example.com/page", + "violatedDirective" => "script-src 'self'", + "blockedURI" => "https://evil.com/script.js", + "originalPolicy" => "default-src 'self'; script-src 'self'" + } + end + + test "creates report with valid CSP data (old format)" do + assert_difference 'Report.count', 1 do + post '/csp-reports', + params: @valid_csp_report_old_format.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } + end + + assert_response :no_content + + report = Report.last + assert_equal "https://example.com/page", report.document_uri + assert_equal "script-src 'self'", report.violated_directive + assert_equal "https://evil.com/script.js", report.blocked_uri end - test "creates report with valid CSP data" do + test "creates report with valid CSP data (new format)" do assert_difference 'Report.count', 1 do - post csp_reports_url, - params: @valid_csp_report.to_json, + post '/csp-reports', + params: @valid_csp_report_new_format.to_json, headers: { 'CONTENT_TYPE' => 'application/json' } end @@ -32,7 +52,7 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest test "returns bad_request with invalid JSON" do assert_no_difference 'Report.count' do - post csp_reports_url, + post '/csp-reports', params: "invalid json{", headers: { 'CONTENT_TYPE' => 'application/json' } end @@ -42,7 +62,7 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest test "returns bad_request with empty body" do assert_no_difference 'Report.count' do - post csp_reports_url, + post '/csp-reports', params: "", headers: { 'CONTENT_TYPE' => 'application/json' } end @@ -51,19 +71,19 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest end test "stores raw_report as JSON" do - post csp_reports_url, - params: @valid_csp_report.to_json, + post '/csp-reports', + params: @valid_csp_report_old_format.to_json, headers: { 'CONTENT_TYPE' => 'application/json' } report = Report.last parsed = JSON.parse(report.raw_report) - assert_equal @valid_csp_report, parsed + assert_equal @valid_csp_report_old_format, parsed end test "does not require CSRF token" do # This test verifies that external browsers can POST without CSRF token - post csp_reports_url, - params: @valid_csp_report.to_json, + post '/csp-reports', + params: @valid_csp_report_old_format.to_json, headers: { 'CONTENT_TYPE' => 'application/json' } assert_response :no_content diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index f3154b5..0c865d4 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,3 +1,3 @@ Rails.application.routes.draw do - mount Reported::Engine => "/reported" + # Routes are automatically added by Reported::Engine initializer end From d82ec3121b0c8c1eac16b70cc7ba1340fd53972b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:14:17 +0000 Subject: [PATCH 05/12] Update to Ruby >= 3.2, Rails >= 7.1, and use JSONB for raw_report Co-authored-by: burisu <240595+burisu@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- Gemfile | 2 +- README.md | 6 ++++++ app/controllers/reported/csp_reports_controller.rb | 2 +- .../20240101000000_create_reported_reports.rb | 4 ++-- reported.gemspec | 4 +++- test/controllers/csp_reports_controller_test.rb | 6 +++--- test/jobs/notification_job_test.rb | 2 +- test/models/report_test.rb | 14 +++++++------- 9 files changed, 26 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8972886..e0c6329 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,8 +12,8 @@ jobs: strategy: matrix: - ruby-version: ['3.0', '3.1', '3.2'] - rails-version: ['6.1', '7.0', '7.1'] + ruby-version: ['3.2', '3.3'] + rails-version: ['7.1', '7.2'] steps: - uses: actions/checkout@v3 diff --git a/Gemfile b/Gemfile index b81a79e..4101e8f 100644 --- a/Gemfile +++ b/Gemfile @@ -7,5 +7,5 @@ gem "sqlite3" gem "puma" group :development, :test do - gem "rails", "~> 7.0" + gem "rails", "~> 7.1" end diff --git a/README.md b/README.md index beb3aa6..7c093d4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ A Rails engine that collects, stores and notifies on Slack about Content Securit - Optional Slack notifications for CSP violations - Easy integration with Rails applications +## Requirements + +- Ruby >= 3.2 +- Rails >= 7.1 +- PostgreSQL (for JSONB support) + ## Installation Add this line to your application's Gemfile: diff --git a/app/controllers/reported/csp_reports_controller.rb b/app/controllers/reported/csp_reports_controller.rb index 416d267..68277ef 100644 --- a/app/controllers/reported/csp_reports_controller.rb +++ b/app/controllers/reported/csp_reports_controller.rb @@ -15,7 +15,7 @@ def create violated_directive: csp_data[:violated_directive], blocked_uri: csp_data[:blocked_uri], original_policy: csp_data[:original_policy], - raw_report: report_data.to_json + raw_report: report_data ) # Send notification if enabled diff --git a/db/migrate/20240101000000_create_reported_reports.rb b/db/migrate/20240101000000_create_reported_reports.rb index e159443..bcf6913 100644 --- a/db/migrate/20240101000000_create_reported_reports.rb +++ b/db/migrate/20240101000000_create_reported_reports.rb @@ -1,11 +1,11 @@ -class CreateReportedReports < ActiveRecord::Migration[6.0] +class CreateReportedReports < ActiveRecord::Migration[7.1] def change create_table :reported_reports do |t| t.string :document_uri t.string :violated_directive t.string :blocked_uri t.text :original_policy - t.text :raw_report, null: false + t.jsonb :raw_report, null: false, default: {} t.datetime :notified_at t.timestamps diff --git a/reported.gemspec b/reported.gemspec index ec55ad3..133c28f 100644 --- a/reported.gemspec +++ b/reported.gemspec @@ -15,7 +15,9 @@ Gem::Specification.new do |spec| spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] - spec.add_dependency "rails", ">= 6.0" + spec.required_ruby_version = ">= 3.2.0" + + spec.add_dependency "rails", ">= 7.1" spec.add_development_dependency "sqlite3" spec.add_development_dependency "webmock" diff --git a/test/controllers/csp_reports_controller_test.rb b/test/controllers/csp_reports_controller_test.rb index 9dfdf1d..eac9e86 100644 --- a/test/controllers/csp_reports_controller_test.rb +++ b/test/controllers/csp_reports_controller_test.rb @@ -70,14 +70,14 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest assert_response :bad_request end - test "stores raw_report as JSON" do + test "stores raw_report as JSONB" do post '/csp-reports', params: @valid_csp_report_old_format.to_json, headers: { 'CONTENT_TYPE' => 'application/json' } report = Report.last - parsed = JSON.parse(report.raw_report) - assert_equal @valid_csp_report_old_format, parsed + # With JSONB, raw_report is stored as a hash directly + assert_equal @valid_csp_report_old_format.deep_stringify_keys, report.raw_report end test "does not require CSRF token" do diff --git a/test/jobs/notification_job_test.rb b/test/jobs/notification_job_test.rb index 1da19e1..da2bed1 100644 --- a/test/jobs/notification_job_test.rb +++ b/test/jobs/notification_job_test.rb @@ -7,7 +7,7 @@ class NotificationJobTest < ActiveJob::TestCase document_uri: "https://example.com/page", violated_directive: "script-src 'self'", blocked_uri: "https://evil.com/script.js", - raw_report: '{"csp-report": {}}' + raw_report: {"csp-report" => {}} ) Reported.slack_webhook_url = "https://hooks.slack.com/services/TEST/WEBHOOK/URL" end diff --git a/test/models/report_test.rb b/test/models/report_test.rb index e799e64..db17d73 100644 --- a/test/models/report_test.rb +++ b/test/models/report_test.rb @@ -9,28 +9,28 @@ class ReportTest < ActiveSupport::TestCase end test "can create report with raw_report" do - report = Report.new(raw_report: '{"csp-report": {}}') + report = Report.new(raw_report: {"csp-report" => {}}) assert report.valid? end test "not_notified scope returns reports without notified_at" do - report1 = Report.create!(raw_report: '{"test": 1}') - report2 = Report.create!(raw_report: '{"test": 2}', notified_at: Time.current) + report1 = Report.create!(raw_report: {"test" => 1}) + report2 = Report.create!(raw_report: {"test" => 2}, notified_at: Time.current) assert_includes Report.not_notified, report1 assert_not_includes Report.not_notified, report2 end test "notified scope returns reports with notified_at" do - report1 = Report.create!(raw_report: '{"test": 1}') - report2 = Report.create!(raw_report: '{"test": 2}', notified_at: Time.current) + report1 = Report.create!(raw_report: {"test" => 1}) + report2 = Report.create!(raw_report: {"test" => 2}, notified_at: Time.current) assert_not_includes Report.notified, report1 assert_includes Report.notified, report2 end test "mark_as_notified! sets notified_at" do - report = Report.create!(raw_report: '{"test": 1}') + report = Report.create!(raw_report: {"test" => 1}) assert_nil report.notified_at report.mark_as_notified! @@ -38,7 +38,7 @@ class ReportTest < ActiveSupport::TestCase end test "notified? returns true when notified_at is set" do - report = Report.create!(raw_report: '{"test": 1}') + report = Report.create!(raw_report: {"test" => 1}) assert_not report.notified? report.update!(notified_at: Time.current) From 4e09c838220da8ba7049e1e9ce1a750b4d483589 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:28:40 +0000 Subject: [PATCH 06/12] Add rubocop-codeur and normalize Ruby code style Co-authored-by: burisu <240595+burisu@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .rubocop.yml | 11 +++ .../reported/csp_reports_controller.rb | 77 +++++++++++-------- app/jobs/reported/notification_job.rb | 20 ++--- app/models/reported/report.rb | 2 + .../20240101000000_create_reported_reports.rb | 2 + .../reported/install/install_generator.rb | 10 ++- lib/reported.rb | 2 + lib/reported/engine.rb | 6 +- lib/reported/version.rb | 2 + reported.gemspec | 1 + .../csp_reports_controller_test.rb | 4 +- test/jobs/notification_job_test.rb | 4 +- test/models/report_test.rb | 4 +- test/test_helper.rb | 10 ++- 15 files changed, 103 insertions(+), 54 deletions(-) create mode 100644 .rubocop.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0c6329..0c0ec8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - ruby-version: ['3.2', '3.3'] + ruby-version: ['3.2', '3.3', '3.4'] rails-version: ['7.1', '7.2'] steps: diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..6cb098e --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,11 @@ +require: + - rubocop-codeur + +AllCops: + NewCops: enable + TargetRubyVersion: 3.2 + Exclude: + - 'test/dummy/**/*' + - 'vendor/**/*' + - 'node_modules/**/*' + - 'bin/**/*' diff --git a/app/controllers/reported/csp_reports_controller.rb b/app/controllers/reported/csp_reports_controller.rb index 68277ef..bc0db03 100644 --- a/app/controllers/reported/csp_reports_controller.rb +++ b/app/controllers/reported/csp_reports_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Reported class CspReportsController < ActionController::Base # Skip CSRF token verification for CSP reports @@ -7,31 +9,34 @@ def create report_data = parse_report_data if report_data - # Extract CSP report data, supporting both old and new formats - csp_data = extract_csp_data(report_data) - - report = Report.create!( - document_uri: csp_data[:document_uri], - violated_directive: csp_data[:violated_directive], - blocked_uri: csp_data[:blocked_uri], - original_policy: csp_data[:original_policy], - raw_report: report_data - ) - - # Send notification if enabled - NotificationJob.perform_later(report.id) if Reported.enabled - + create_report(report_data) head :no_content else head :bad_request end - rescue => e + rescue StandardError => e Rails.logger.error("Error processing CSP report: #{e.message}") head :internal_server_error end private + def create_report(report_data) + # Extract CSP report data, supporting both old and new formats + csp_data = extract_csp_data(report_data) + + report = Report.create!( + document_uri: csp_data[:document_uri], + violated_directive: csp_data[:violated_directive], + blocked_uri: csp_data[:blocked_uri], + original_policy: csp_data[:original_policy], + raw_report: report_data + ) + + # Send notification if enabled + NotificationJob.perform_later(report.id) if Reported.enabled + end + def parse_report_data body = request.body.read return nil if body.blank? @@ -44,24 +49,34 @@ def parse_report_data def extract_csp_data(report_data) # Support both old format (csp-report) and new format (direct fields) - if report_data['csp-report'] - # Old format: {"csp-report": {...}} - csp_report = report_data['csp-report'] - { - document_uri: csp_report['document-uri'] || csp_report['documentURI'], - violated_directive: csp_report['violated-directive'] || csp_report['violatedDirective'] || csp_report['effective-directive'] || csp_report['effectiveDirective'], - blocked_uri: csp_report['blocked-uri'] || csp_report['blockedURI'], - original_policy: csp_report['original-policy'] || csp_report['originalPolicy'] - } + if report_data["csp-report"] + extract_old_format(report_data["csp-report"]) else - # New format: direct fields or camelCase - { - document_uri: report_data['document-uri'] || report_data['documentURI'] || report_data['document_uri'], - violated_directive: report_data['violated-directive'] || report_data['violatedDirective'] || report_data['effective-directive'] || report_data['effectiveDirective'] || report_data['violated_directive'], - blocked_uri: report_data['blocked-uri'] || report_data['blockedURI'] || report_data['blocked_uri'], - original_policy: report_data['original-policy'] || report_data['originalPolicy'] || report_data['original_policy'] - } + extract_new_format(report_data) end end + + def extract_old_format(csp_report) + # Old format: {"csp-report": {...}} + { + document_uri: csp_report["document-uri"] || csp_report["documentURI"], + violated_directive: csp_report["violated-directive"] || csp_report["violatedDirective"] || + csp_report["effective-directive"] || csp_report["effectiveDirective"], + blocked_uri: csp_report["blocked-uri"] || csp_report["blockedURI"], + original_policy: csp_report["original-policy"] || csp_report["originalPolicy"] + } + end + + def extract_new_format(report_data) + # New format: direct fields or camelCase + { + document_uri: report_data["document-uri"] || report_data["documentURI"] || report_data["document_uri"], + violated_directive: report_data["violated-directive"] || report_data["violatedDirective"] || + report_data["effective-directive"] || report_data["effectiveDirective"] || + report_data["violated_directive"], + blocked_uri: report_data["blocked-uri"] || report_data["blockedURI"] || report_data["blocked_uri"], + original_policy: report_data["original-policy"] || report_data["originalPolicy"] || report_data["original_policy"] + } + end end end diff --git a/app/jobs/reported/notification_job.rb b/app/jobs/reported/notification_job.rb index 322cf71..c86a906 100644 --- a/app/jobs/reported/notification_job.rb +++ b/app/jobs/reported/notification_job.rb @@ -1,6 +1,8 @@ -require 'net/http' -require 'uri' -require 'json' +# frozen_string_literal: true + +require "net/http" +require "uri" +require "json" module Reported class NotificationJob < ActiveJob::Base @@ -14,7 +16,7 @@ def perform(report_id) send_slack_notification(report) report.mark_as_notified! - rescue => e + rescue StandardError => e Rails.logger.error("Error sending Slack notification for report #{report_id}: #{e.message}") raise end @@ -24,16 +26,16 @@ def perform(report_id) def send_slack_notification(report) uri = URI.parse(Reported.slack_webhook_url) http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if uri.scheme == 'https' + http.use_ssl = true if uri.scheme == "https" - request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') + request = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json") request.body = notification_payload(report).to_json response = http.request(request) - unless response.code.to_i == 200 - raise "Slack API returned #{response.code}: #{response.body}" - end + return if response.code.to_i == 200 + + raise "Slack API returned #{response.code}: #{response.body}" end def notification_payload(report) diff --git a/app/models/reported/report.rb b/app/models/reported/report.rb index ea1189a..48d91c8 100644 --- a/app/models/reported/report.rb +++ b/app/models/reported/report.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Reported class Report < ApplicationRecord validates :raw_report, presence: true diff --git a/db/migrate/20240101000000_create_reported_reports.rb b/db/migrate/20240101000000_create_reported_reports.rb index bcf6913..ab41e89 100644 --- a/db/migrate/20240101000000_create_reported_reports.rb +++ b/db/migrate/20240101000000_create_reported_reports.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateReportedReports < ActiveRecord::Migration[7.1] def change create_table :reported_reports do |t| diff --git a/lib/generators/reported/install/install_generator.rb b/lib/generators/reported/install/install_generator.rb index e5b8fcb..5f8ea5e 100644 --- a/lib/generators/reported/install/install_generator.rb +++ b/lib/generators/reported/install/install_generator.rb @@ -1,15 +1,17 @@ -require 'rails/generators' +# frozen_string_literal: true + +require "rails/generators" module Reported module Generators class InstallGenerator < Rails::Generators::Base - source_root File.expand_path('templates', __dir__) + source_root File.expand_path("templates", __dir__) desc "Creates Reported initializer for your application" def copy_initializer template "reported.rb", "config/initializers/reported.rb" - + puts "Reported initializer created at config/initializers/reported.rb" end @@ -17,7 +19,7 @@ def show_readme readme "README" if behavior == :invoke end - def self.next_migration_number(dirname) + def self.next_migration_number(_dirname) Time.now.utc.strftime("%Y%m%d%H%M%S") end end diff --git a/lib/reported.rb b/lib/reported.rb index 853b82b..5e77884 100644 --- a/lib/reported.rb +++ b/lib/reported.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "reported/version" require "reported/engine" diff --git a/lib/reported/engine.rb b/lib/reported/engine.rb index f590457..1ab9671 100644 --- a/lib/reported/engine.rb +++ b/lib/reported/engine.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + module Reported class Engine < ::Rails::Engine isolate_namespace Reported config.generators do |g| g.test_framework :test_unit - g.fixture_replacement :factory_bot, dir: 'spec/factories' + g.fixture_replacement :factory_bot, dir: "spec/factories" end # Automatically add routes to the main application initializer "reported.add_routes" do |app| app.routes.prepend do - post '/csp-reports', to: 'reported/csp_reports#create' + post "/csp-reports", to: "reported/csp_reports#create" end end end diff --git a/lib/reported/version.rb b/lib/reported/version.rb index ed88c26..031af16 100644 --- a/lib/reported/version.rb +++ b/lib/reported/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Reported VERSION = "0.1.0" end diff --git a/reported.gemspec b/reported.gemspec index 133c28f..7a7f50a 100644 --- a/reported.gemspec +++ b/reported.gemspec @@ -21,4 +21,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "sqlite3" spec.add_development_dependency "webmock" + spec.add_development_dependency "rubocop-codeur" end diff --git a/test/controllers/csp_reports_controller_test.rb b/test/controllers/csp_reports_controller_test.rb index eac9e86..be58af4 100644 --- a/test/controllers/csp_reports_controller_test.rb +++ b/test/controllers/csp_reports_controller_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module Reported class CspReportsControllerTest < ActionDispatch::IntegrationTest diff --git a/test/jobs/notification_job_test.rb b/test/jobs/notification_job_test.rb index da2bed1..09239a0 100644 --- a/test/jobs/notification_job_test.rb +++ b/test/jobs/notification_job_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module Reported class NotificationJobTest < ActiveJob::TestCase diff --git a/test/models/report_test.rb b/test/models/report_test.rb index db17d73..aae2a44 100644 --- a/test/models/report_test.rb +++ b/test/models/report_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module Reported class ReportTest < ActiveSupport::TestCase diff --git a/test/test_helper.rb b/test/test_helper.rb index d746fc4..a0491bd 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Configure Rails Environment ENV["RAILS_ENV"] = "test" @@ -6,17 +8,17 @@ # Placeholder for when tests are run in a Rails application context # This allows the gem to be tested independently begin - require File.expand_path("../test/dummy/config/environment", __dir__) + require File.expand_path("test/dummy/config/environment", __dir__) rescue LoadError # If dummy app doesn't exist, just load the engine - require File.expand_path("../lib/reported", __dir__) + require File.expand_path("lib/reported", __dir__) end require "rails/test_help" # Require webmock for testing HTTP requests begin - require 'webmock/minitest' + require "webmock/minitest" WebMock.disable_net_connect!(allow_localhost: true) rescue LoadError # webmock not available @@ -27,5 +29,5 @@ if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) - ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" + ActiveSupport::TestCase.file_fixture_path = "#{ActiveSupport::TestCase.fixture_path}/files" end From e6b7069b68a35c842870f5cdd698089d388a34d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:32:40 +0000 Subject: [PATCH 07/12] Fix code style in test files to match rubocop standards Co-authored-by: burisu <240595+burisu@users.noreply.github.com> --- .../csp_reports_controller_test.rb | 60 +++++++++---------- test/jobs/notification_job_test.rb | 28 ++++----- test/models/report_test.rb | 22 +++---- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/test/controllers/csp_reports_controller_test.rb b/test/controllers/csp_reports_controller_test.rb index be58af4..30f86f5 100644 --- a/test/controllers/csp_reports_controller_test.rb +++ b/test/controllers/csp_reports_controller_test.rb @@ -23,14 +23,14 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest end test "creates report with valid CSP data (old format)" do - assert_difference 'Report.count', 1 do - post '/csp-reports', - params: @valid_csp_report_old_format.to_json, - headers: { 'CONTENT_TYPE' => 'application/json' } + assert_difference "Report.count", 1 do + post "/csp-reports", + params: @valid_csp_report_old_format.to_json, + headers: { "CONTENT_TYPE" => "application/json" } end - + assert_response :no_content - + report = Report.last assert_equal "https://example.com/page", report.document_uri assert_equal "script-src 'self'", report.violated_directive @@ -38,14 +38,14 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest end test "creates report with valid CSP data (new format)" do - assert_difference 'Report.count', 1 do - post '/csp-reports', - params: @valid_csp_report_new_format.to_json, - headers: { 'CONTENT_TYPE' => 'application/json' } + assert_difference "Report.count", 1 do + post "/csp-reports", + params: @valid_csp_report_new_format.to_json, + headers: { "CONTENT_TYPE" => "application/json" } end - + assert_response :no_content - + report = Report.last assert_equal "https://example.com/page", report.document_uri assert_equal "script-src 'self'", report.violated_directive @@ -53,30 +53,30 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest end test "returns bad_request with invalid JSON" do - assert_no_difference 'Report.count' do - post '/csp-reports', - params: "invalid json{", - headers: { 'CONTENT_TYPE' => 'application/json' } + assert_no_difference "Report.count" do + post "/csp-reports", + params: "invalid json{", + headers: { "CONTENT_TYPE" => "application/json" } end - + assert_response :bad_request end test "returns bad_request with empty body" do - assert_no_difference 'Report.count' do - post '/csp-reports', - params: "", - headers: { 'CONTENT_TYPE' => 'application/json' } + assert_no_difference "Report.count" do + post "/csp-reports", + params: "", + headers: { "CONTENT_TYPE" => "application/json" } end - + assert_response :bad_request end test "stores raw_report as JSONB" do - post '/csp-reports', - params: @valid_csp_report_old_format.to_json, - headers: { 'CONTENT_TYPE' => 'application/json' } - + post "/csp-reports", + params: @valid_csp_report_old_format.to_json, + headers: { "CONTENT_TYPE" => "application/json" } + report = Report.last # With JSONB, raw_report is stored as a hash directly assert_equal @valid_csp_report_old_format.deep_stringify_keys, report.raw_report @@ -84,10 +84,10 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest test "does not require CSRF token" do # This test verifies that external browsers can POST without CSRF token - post '/csp-reports', - params: @valid_csp_report_old_format.to_json, - headers: { 'CONTENT_TYPE' => 'application/json' } - + post "/csp-reports", + params: @valid_csp_report_old_format.to_json, + headers: { "CONTENT_TYPE" => "application/json" } + assert_response :no_content end end diff --git a/test/jobs/notification_job_test.rb b/test/jobs/notification_job_test.rb index 09239a0..f34b678 100644 --- a/test/jobs/notification_job_test.rb +++ b/test/jobs/notification_job_test.rb @@ -9,7 +9,7 @@ class NotificationJobTest < ActiveJob::TestCase document_uri: "https://example.com/page", violated_directive: "script-src 'self'", blocked_uri: "https://evil.com/script.js", - raw_report: {"csp-report" => {}} + raw_report: { "csp-report" => {} } ) Reported.slack_webhook_url = "https://hooks.slack.com/services/TEST/WEBHOOK/URL" end @@ -19,7 +19,7 @@ class NotificationJobTest < ActiveJob::TestCase .to_return(status: 200, body: "ok") NotificationJob.perform_now(@report.id) - + assert_requested :post, Reported.slack_webhook_url end @@ -28,48 +28,48 @@ class NotificationJobTest < ActiveJob::TestCase .to_return(status: 200, body: "ok") assert_nil @report.notified_at - + NotificationJob.perform_now(@report.id) - + @report.reload assert_not_nil @report.notified_at end test "does not send notification if report already notified" do @report.mark_as_notified! - + stub_request(:post, Reported.slack_webhook_url) .to_return(status: 200, body: "ok") NotificationJob.perform_now(@report.id) - + assert_not_requested :post, Reported.slack_webhook_url end test "does not send notification if webhook URL is not configured" do Reported.slack_webhook_url = nil - + stub_request(:post, "https://hooks.slack.com/services/TEST/WEBHOOK/URL") .to_return(status: 200, body: "ok") NotificationJob.perform_now(@report.id) - + assert_not_requested :post, "https://hooks.slack.com/services/TEST/WEBHOOK/URL" end test "includes CSP violation details in Slack message" do stub_request(:post, Reported.slack_webhook_url) - .with { |request| + .with do |request| body = JSON.parse(request.body) body["text"] == "CSP Violation Report" && - body["attachments"].any? { |a| - a["fields"].any? { |f| f["title"] == "Document URI" && f["value"] == @report.document_uri } - } - } + body["attachments"].any? do |a| + a["fields"].any? { |f| f["title"] == "Document URI" && f["value"] == @report.document_uri } + end + end .to_return(status: 200, body: "ok") NotificationJob.perform_now(@report.id) - + assert_requested :post, Reported.slack_webhook_url end end diff --git a/test/models/report_test.rb b/test/models/report_test.rb index aae2a44..9d5f011 100644 --- a/test/models/report_test.rb +++ b/test/models/report_test.rb @@ -11,38 +11,38 @@ class ReportTest < ActiveSupport::TestCase end test "can create report with raw_report" do - report = Report.new(raw_report: {"csp-report" => {}}) + report = Report.new(raw_report: { "csp-report" => {} }) assert report.valid? end test "not_notified scope returns reports without notified_at" do - report1 = Report.create!(raw_report: {"test" => 1}) - report2 = Report.create!(raw_report: {"test" => 2}, notified_at: Time.current) - + report1 = Report.create!(raw_report: { "test" => 1 }) + report2 = Report.create!(raw_report: { "test" => 2 }, notified_at: Time.current) + assert_includes Report.not_notified, report1 assert_not_includes Report.not_notified, report2 end test "notified scope returns reports with notified_at" do - report1 = Report.create!(raw_report: {"test" => 1}) - report2 = Report.create!(raw_report: {"test" => 2}, notified_at: Time.current) - + report1 = Report.create!(raw_report: { "test" => 1 }) + report2 = Report.create!(raw_report: { "test" => 2 }, notified_at: Time.current) + assert_not_includes Report.notified, report1 assert_includes Report.notified, report2 end test "mark_as_notified! sets notified_at" do - report = Report.create!(raw_report: {"test" => 1}) + report = Report.create!(raw_report: { "test" => 1 }) assert_nil report.notified_at - + report.mark_as_notified! assert_not_nil report.notified_at end test "notified? returns true when notified_at is set" do - report = Report.create!(raw_report: {"test" => 1}) + report = Report.create!(raw_report: { "test" => 1 }) assert_not report.notified? - + report.update!(notified_at: Time.current) assert report.notified? end From aa32cacb324dd247ac9a2faac9e1e8f0a3e957cc Mon Sep 17 00:00:00 2001 From: Brice TEXIER Date: Thu, 2 Oct 2025 14:28:00 +0200 Subject: [PATCH 08/12] Add default Rails::Engine structure --- .github/dependabot.yml | 12 +++ .github/workflows/ci.yml | 56 ++++++++++++ .github/workflows/test.yml | 31 ------- .gitignore | 18 ++-- .rubocop.yml | 14 +-- .tool-versions | 1 + Gemfile | 21 +++-- Rakefile | 12 ++- app/assets/images/reported/.keep | 0 .../stylesheets/reported/application.css | 15 +++ app/controllers/concerns/.keep | 0 .../reported/application_controller.rb | 6 ++ .../reported/csp_reports_controller.rb | 42 +++------ app/helpers/reported/application_helper.rb | 6 ++ app/jobs/reported/application_job.rb | 6 ++ app/jobs/reported/notification_job.rb | 34 +++---- app/mailers/reported/application_mailer.rb | 8 ++ app/models/concerns/.keep | 0 app/models/reported/application_record.rb | 7 ++ app/models/reported/report.rb | 4 +- .../layouts/reported/application.html.erb | 17 ++++ bin/rails | 14 +++ bin/rubocop | 8 ++ config/routes.rb | 1 + .../20240101000000_create_reported_reports.rb | 6 +- .../reported/install/install_generator.rb | 16 ++-- .../reported/install/templates/reported.rb | 4 +- lib/reported.rb | 4 +- lib/reported/engine.rb | 6 +- lib/reported/version.rb | 2 +- lib/tasks/reported_tasks.rake | 6 ++ reported.gemspec | 40 ++++---- test/controllers/.keep | 0 .../csp_reports_controller_test.rb | 84 ++++++++--------- test/dummy/Rakefile | 5 + test/dummy/app/assets/images/.keep | 0 .../app/assets/stylesheets/application.css | 15 +++ .../app/controllers/application_controller.rb | 6 ++ test/dummy/app/controllers/concerns/.keep | 0 test/dummy/app/helpers/application_helper.rb | 4 + test/dummy/app/jobs/application_job.rb | 9 ++ test/dummy/app/mailers/application_mailer.rb | 6 ++ test/dummy/app/models/application_record.rb | 5 + test/dummy/app/models/concerns/.keep | 0 .../app/views/layouts/application.html.erb | 27 ++++++ test/dummy/app/views/layouts/mailer.html.erb | 13 +++ test/dummy/app/views/layouts/mailer.text.erb | 1 + test/dummy/app/views/pwa/manifest.json.erb | 22 +++++ test/dummy/app/views/pwa/service-worker.js | 26 ++++++ test/dummy/bin/dev | 4 + test/dummy/bin/rails | 6 ++ test/dummy/bin/rake | 6 ++ test/dummy/bin/setup | 36 ++++++++ test/dummy/config.ru | 8 ++ test/dummy/config/application.rb | 28 ++++-- test/dummy/config/boot.rb | 7 ++ test/dummy/config/cable.yml | 10 ++ test/dummy/config/database.yml | 33 ++++++- test/dummy/config/environment.rb | 6 +- test/dummy/config/environments/development.rb | 71 +++++++++++++++ test/dummy/config/environments/production.rb | 91 +++++++++++++++++++ test/dummy/config/environments/test.rb | 55 +++++++++++ test/dummy/config/initializers/assets.rb | 9 ++ .../initializers/content_security_policy.rb | 27 ++++++ .../initializers/filter_parameter_logging.rb | 10 ++ test/dummy/config/initializers/inflections.rb | 18 ++++ test/dummy/config/locales/en.yml | 31 +++++++ test/dummy/config/puma.rb | 40 ++++++++ test/dummy/config/routes.rb | 4 +- test/dummy/config/storage.yml | 34 +++++++ test/dummy/db/schema.rb | 26 ++++++ test/dummy/log/.keep | 0 test/fixtures/files/.keep | 0 test/helpers/.keep | 0 test/integration/.keep | 0 test/integration/navigation_test.rb | 9 ++ test/jobs/notification_job_test.rb | 40 ++++---- test/mailers/.keep | 0 test/models/.keep | 0 test/models/report_test.rb | 28 +++--- test/reported_test.rb | 9 ++ test/test_helper.rb | 29 +++--- 82 files changed, 1067 insertions(+), 248 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/test.yml create mode 100644 .tool-versions create mode 100644 app/assets/images/reported/.keep create mode 100644 app/assets/stylesheets/reported/application.css create mode 100644 app/controllers/concerns/.keep create mode 100644 app/controllers/reported/application_controller.rb create mode 100644 app/helpers/reported/application_helper.rb create mode 100644 app/jobs/reported/application_job.rb create mode 100644 app/mailers/reported/application_mailer.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/models/reported/application_record.rb create mode 100644 app/views/layouts/reported/application.html.erb create mode 100755 bin/rails create mode 100755 bin/rubocop create mode 100644 config/routes.rb create mode 100644 lib/tasks/reported_tasks.rake create mode 100644 test/controllers/.keep create mode 100644 test/dummy/app/assets/images/.keep create mode 100644 test/dummy/app/assets/stylesheets/application.css create mode 100644 test/dummy/app/controllers/application_controller.rb create mode 100644 test/dummy/app/controllers/concerns/.keep create mode 100644 test/dummy/app/helpers/application_helper.rb create mode 100644 test/dummy/app/jobs/application_job.rb create mode 100644 test/dummy/app/mailers/application_mailer.rb create mode 100644 test/dummy/app/models/application_record.rb create mode 100644 test/dummy/app/models/concerns/.keep create mode 100644 test/dummy/app/views/layouts/application.html.erb create mode 100644 test/dummy/app/views/layouts/mailer.html.erb create mode 100644 test/dummy/app/views/layouts/mailer.text.erb create mode 100644 test/dummy/app/views/pwa/manifest.json.erb create mode 100644 test/dummy/app/views/pwa/service-worker.js create mode 100755 test/dummy/bin/dev create mode 100755 test/dummy/bin/rails create mode 100755 test/dummy/bin/rake create mode 100755 test/dummy/bin/setup create mode 100644 test/dummy/config.ru create mode 100644 test/dummy/config/boot.rb create mode 100644 test/dummy/config/cable.yml create mode 100644 test/dummy/config/environments/development.rb create mode 100644 test/dummy/config/environments/production.rb create mode 100644 test/dummy/config/environments/test.rb create mode 100644 test/dummy/config/initializers/assets.rb create mode 100644 test/dummy/config/initializers/content_security_policy.rb create mode 100644 test/dummy/config/initializers/filter_parameter_logging.rb create mode 100644 test/dummy/config/initializers/inflections.rb create mode 100644 test/dummy/config/locales/en.yml create mode 100644 test/dummy/config/puma.rb create mode 100644 test/dummy/config/storage.yml create mode 100644 test/dummy/db/schema.rb create mode 100644 test/dummy/log/.keep create mode 100644 test/fixtures/files/.keep create mode 100644 test/helpers/.keep create mode 100644 test/integration/.keep create mode 100644 test/integration/navigation_test.rb create mode 100644 test/mailers/.keep create mode 100644 test/models/.keep create mode 100644 test/reported_test.rb diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f0527e6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..42392fa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: redis + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + # - name: Install packages + # run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config + + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 0c0ec8c..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: ubuntu-latest - - strategy: - matrix: - ruby-version: ['3.2', '3.3', '3.4'] - rails-version: ['7.1', '7.2'] - - steps: - - uses: actions/checkout@v3 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby-version }} - bundler-cache: true - - - name: Install dependencies - run: bundle install - - - name: Run tests - run: bundle exec rake test diff --git a/.gitignore b/.gitignore index cd23eb8..5568189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ -.bundle/ -log/*.log -pkg/ -test/dummy/db/*.sqlite3 -test/dummy/db/*.sqlite3-journal -test/dummy/log/*.log -test/dummy/storage/ -test/dummy/tmp/ +/.bundle/ +/doc/ +/log/*.log +/pkg/ +/tmp/ +/test/dummy/db/*.sqlite3 +/test/dummy/db/*.sqlite3-* +/test/dummy/log/*.log +/test/dummy/storage/ +/test/dummy/tmp/ *.gem .byebug_history Gemfile.lock diff --git a/.rubocop.yml b/.rubocop.yml index 6cb098e..268b12b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,3 @@ -require: - - rubocop-codeur - -AllCops: - NewCops: enable - TargetRubyVersion: 3.2 - Exclude: - - 'test/dummy/**/*' - - 'vendor/**/*' - - 'node_modules/**/*' - - 'bin/**/*' +inherit_gem: + rubocop-codeur: + - default.yml diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..ae5ecdb --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.4.2 diff --git a/Gemfile b/Gemfile index 4101e8f..0d9af87 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,20 @@ -source "https://rubygems.org" +# frozen_string_literal: true + +source 'https://rubygems.org' # Specify your gem's dependencies in reported.gemspec gemspec -gem "sqlite3" -gem "puma" +gem 'puma' + +gem 'sqlite3' + +gem 'propshaft' + +# Codeur Ruby styling [https://github.com/codeur/rubocop-codeur/] +gem 'rubocop-codeur', require: false + +# Start debugger with binding.b [https://github.com/ruby/debug] +# gem "debug", ">= 1.0.0" -group :development, :test do - gem "rails", "~> 7.1" -end +gem 'webmock', group: 'test' diff --git a/Rakefile b/Rakefile index e7793b5..5a624a1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,10 @@ -require "bundler/setup" +# frozen_string_literal: true -APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) -load "rails/tasks/engine.rake" +require 'bundler/setup' -load "rails/tasks/statistics.rake" +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) +load 'rails/tasks/engine.rake' -require "bundler/gem_tasks" +load 'rails/tasks/statistics.rake' + +require 'bundler/gem_tasks' diff --git a/app/assets/images/reported/.keep b/app/assets/images/reported/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/reported/application.css b/app/assets/stylesheets/reported/application.css new file mode 100644 index 0000000..0ebd7fe --- /dev/null +++ b/app/assets/stylesheets/reported/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/reported/application_controller.rb b/app/controllers/reported/application_controller.rb new file mode 100644 index 0000000..0464be5 --- /dev/null +++ b/app/controllers/reported/application_controller.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Reported + class ApplicationController < ActionController::Base + end +end diff --git a/app/controllers/reported/csp_reports_controller.rb b/app/controllers/reported/csp_reports_controller.rb index bc0db03..672da5a 100644 --- a/app/controllers/reported/csp_reports_controller.rb +++ b/app/controllers/reported/csp_reports_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Reported - class CspReportsController < ActionController::Base + class CspReportsController < ApplicationController # Skip CSRF token verification for CSP reports skip_before_action :verify_authenticity_token @@ -19,18 +19,18 @@ def create head :internal_server_error end - private + private def create_report(report_data) # Extract CSP report data, supporting both old and new formats csp_data = extract_csp_data(report_data) report = Report.create!( - document_uri: csp_data[:document_uri], + document_uri: csp_data[:document_uri], violated_directive: csp_data[:violated_directive], - blocked_uri: csp_data[:blocked_uri], - original_policy: csp_data[:original_policy], - raw_report: report_data + blocked_uri: csp_data[:blocked_uri], + original_policy: csp_data[:original_policy], + raw_report: report_data ) # Send notification if enabled @@ -49,33 +49,17 @@ def parse_report_data def extract_csp_data(report_data) # Support both old format (csp-report) and new format (direct fields) - if report_data["csp-report"] - extract_old_format(report_data["csp-report"]) - else - extract_new_format(report_data) - end + extract_properties(report_data['csp-report'] || report_data) end - def extract_old_format(csp_report) + def extract_properties(csp_report) # Old format: {"csp-report": {...}} { - document_uri: csp_report["document-uri"] || csp_report["documentURI"], - violated_directive: csp_report["violated-directive"] || csp_report["violatedDirective"] || - csp_report["effective-directive"] || csp_report["effectiveDirective"], - blocked_uri: csp_report["blocked-uri"] || csp_report["blockedURI"], - original_policy: csp_report["original-policy"] || csp_report["originalPolicy"] - } - end - - def extract_new_format(report_data) - # New format: direct fields or camelCase - { - document_uri: report_data["document-uri"] || report_data["documentURI"] || report_data["document_uri"], - violated_directive: report_data["violated-directive"] || report_data["violatedDirective"] || - report_data["effective-directive"] || report_data["effectiveDirective"] || - report_data["violated_directive"], - blocked_uri: report_data["blocked-uri"] || report_data["blockedURI"] || report_data["blocked_uri"], - original_policy: report_data["original-policy"] || report_data["originalPolicy"] || report_data["original_policy"] + document_uri: csp_report['document-uri'] || csp_report['documentURI'], + violated_directive: csp_report['violated-directive'] || csp_report['violatedDirective'] || + csp_report['effective-directive'] || csp_report['effectiveDirective'], + blocked_uri: csp_report['blocked-uri'] || csp_report['blockedURI'], + original_policy: csp_report['original-policy'] || csp_report['originalPolicy'] } end end diff --git a/app/helpers/reported/application_helper.rb b/app/helpers/reported/application_helper.rb new file mode 100644 index 0000000..36d40d7 --- /dev/null +++ b/app/helpers/reported/application_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Reported + module ApplicationHelper + end +end diff --git a/app/jobs/reported/application_job.rb b/app/jobs/reported/application_job.rb new file mode 100644 index 0000000..ba4ef98 --- /dev/null +++ b/app/jobs/reported/application_job.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Reported + class ApplicationJob < ActiveJob::Base + end +end diff --git a/app/jobs/reported/notification_job.rb b/app/jobs/reported/notification_job.rb index c86a906..805f6f0 100644 --- a/app/jobs/reported/notification_job.rb +++ b/app/jobs/reported/notification_job.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true -require "net/http" -require "uri" -require "json" +require 'net/http' +require 'uri' +require 'json' module Reported - class NotificationJob < ActiveJob::Base + class NotificationJob < ApplicationJob queue_as :default def perform(report_id) report = Report.find_by(id: report_id) return unless report return if report.notified? - return unless Reported.slack_webhook_url.present? + return if Reported.slack_webhook_url.blank? send_slack_notification(report) report.mark_as_notified! @@ -21,14 +21,14 @@ def perform(report_id) raise end - private + private def send_slack_notification(report) uri = URI.parse(Reported.slack_webhook_url) http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true if uri.scheme == "https" + http.use_ssl = true if uri.scheme == 'https' - request = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json") + request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') request.body = notification_payload(report).to_json response = http.request(request) @@ -40,28 +40,28 @@ def send_slack_notification(report) def notification_payload(report) { - text: "CSP Violation Report", + text: 'CSP Violation Report', attachments: [ { - color: "danger", + color: 'danger', fields: [ { - title: "Document URI", - value: report.document_uri || "N/A", + title: 'Document URI', + value: report.document_uri || 'N/A', short: false }, { - title: "Violated Directive", - value: report.violated_directive || "N/A", + title: 'Violated Directive', + value: report.violated_directive || 'N/A', short: true }, { - title: "Blocked URI", - value: report.blocked_uri || "N/A", + title: 'Blocked URI', + value: report.blocked_uri || 'N/A', short: true }, { - title: "Reported At", + title: 'Reported At', value: report.created_at.to_s, short: true } diff --git a/app/mailers/reported/application_mailer.rb b/app/mailers/reported/application_mailer.rb new file mode 100644 index 0000000..08f301e --- /dev/null +++ b/app/mailers/reported/application_mailer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Reported + class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/reported/application_record.rb b/app/models/reported/application_record.rb new file mode 100644 index 0000000..3536510 --- /dev/null +++ b/app/models/reported/application_record.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Reported + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end +end diff --git a/app/models/reported/report.rb b/app/models/reported/report.rb index 48d91c8..920b78a 100644 --- a/app/models/reported/report.rb +++ b/app/models/reported/report.rb @@ -2,11 +2,11 @@ module Reported class Report < ApplicationRecord - validates :raw_report, presence: true - scope :not_notified, -> { where(notified_at: nil) } scope :notified, -> { where.not(notified_at: nil) } + validates :raw_report, presence: true + def mark_as_notified! update!(notified_at: Time.current) end diff --git a/app/views/layouts/reported/application.html.erb b/app/views/layouts/reported/application.html.erb new file mode 100644 index 0000000..790462b --- /dev/null +++ b/app/views/layouts/reported/application.html.erb @@ -0,0 +1,17 @@ + + + + Reported + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%= stylesheet_link_tag "reported/application", media: "all" %> + + + +<%= yield %> + + + diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..3e7e6f1 --- /dev/null +++ b/bin/rails @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. + +ENGINE_ROOT = File.expand_path("..", __dir__) +ENGINE_PATH = File.expand_path("../lib/reported/engine", __dir__) +APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) + +# Set up gems listed in the Gemfile. +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) + +require "rails/all" +require "rails/engine/commands" diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000..40330c0 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..8e9b8f9 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/db/migrate/20240101000000_create_reported_reports.rb b/db/migrate/20240101000000_create_reported_reports.rb index ab41e89..1aaf990 100644 --- a/db/migrate/20240101000000_create_reported_reports.rb +++ b/db/migrate/20240101000000_create_reported_reports.rb @@ -7,7 +7,11 @@ def change t.string :violated_directive t.string :blocked_uri t.text :original_policy - t.jsonb :raw_report, null: false, default: {} + if connection.adapter_name.downcase.include?('postgresql') + t.jsonb :raw_report, null: false, default: {} + else + t.json :raw_report, null: false, default: {} + end t.datetime :notified_at t.timestamps diff --git a/lib/generators/reported/install/install_generator.rb b/lib/generators/reported/install/install_generator.rb index 5f8ea5e..b022460 100644 --- a/lib/generators/reported/install/install_generator.rb +++ b/lib/generators/reported/install/install_generator.rb @@ -1,26 +1,22 @@ # frozen_string_literal: true -require "rails/generators" +require 'rails/generators' module Reported module Generators class InstallGenerator < Rails::Generators::Base - source_root File.expand_path("templates", __dir__) + source_root File.expand_path('templates', __dir__) - desc "Creates Reported initializer for your application" + desc 'Creates Reported initializer for your application' def copy_initializer - template "reported.rb", "config/initializers/reported.rb" + template 'reported.rb', 'config/initializers/reported.rb' - puts "Reported initializer created at config/initializers/reported.rb" + Rails.logger.debug 'Reported initializer created at config/initializers/reported.rb' end def show_readme - readme "README" if behavior == :invoke - end - - def self.next_migration_number(_dirname) - Time.now.utc.strftime("%Y%m%d%H%M%S") + readme 'README' if behavior == :invoke end end end diff --git a/lib/generators/reported/install/templates/reported.rb b/lib/generators/reported/install/templates/reported.rb index dea52fe..07c78af 100644 --- a/lib/generators/reported/install/templates/reported.rb +++ b/lib/generators/reported/install/templates/reported.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + Reported.configuration do |config| # Enable or disable Slack notifications config.enabled = true # Slack webhook URL for notifications # Get your webhook URL from https://api.slack.com/messaging/webhooks - config.slack_webhook_url = ENV['REPORTED_SLACK_WEBHOOK_URL'] + config.slack_webhook_url = ENV.fetch('REPORTED_SLACK_WEBHOOK_URL', nil) end diff --git a/lib/reported.rb b/lib/reported.rb index 5e77884..14a514a 100644 --- a/lib/reported.rb +++ b/lib/reported.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require "reported/version" -require "reported/engine" +require 'reported/version' +require 'reported/engine' module Reported mattr_accessor :slack_webhook_url diff --git a/lib/reported/engine.rb b/lib/reported/engine.rb index 1ab9671..9a543d0 100644 --- a/lib/reported/engine.rb +++ b/lib/reported/engine.rb @@ -6,13 +6,13 @@ class Engine < ::Rails::Engine config.generators do |g| g.test_framework :test_unit - g.fixture_replacement :factory_bot, dir: "spec/factories" + g.fixture_replacement :factory_bot, dir: 'spec/factories' end # Automatically add routes to the main application - initializer "reported.add_routes" do |app| + initializer 'reported.add_routes' do |app| app.routes.prepend do - post "/csp-reports", to: "reported/csp_reports#create" + post '/csp-reports', to: 'reported/csp_reports#create' end end end diff --git a/lib/reported/version.rb b/lib/reported/version.rb index 031af16..83e5676 100644 --- a/lib/reported/version.rb +++ b/lib/reported/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Reported - VERSION = "0.1.0" + VERSION = '0.1.0' end diff --git a/lib/tasks/reported_tasks.rake b/lib/tasks/reported_tasks.rake new file mode 100644 index 0000000..ebf2ffa --- /dev/null +++ b/lib/tasks/reported_tasks.rake @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# desc "Explaining what the task does" +# task :reported do +# # Task goes here +# end diff --git a/reported.gemspec b/reported.gemspec index 7a7f50a..3da245b 100644 --- a/reported.gemspec +++ b/reported.gemspec @@ -1,25 +1,31 @@ -require_relative "lib/reported/version" +# frozen_string_literal: true + +require_relative 'lib/reported/version' Gem::Specification.new do |spec| - spec.name = "reported" + spec.name = 'reported' spec.version = Reported::VERSION - spec.authors = ["Codeur"] - spec.email = ["contact@codeur.com"] - spec.homepage = "https://github.com/codeur/reported" - spec.summary = "CSP reports collection for Rails apps" - spec.description = "A Rails engine that collects, stores and notifies on Slack about CSP violation reports" - spec.license = "MIT" + spec.authors = ['Brice TEXIER'] + spec.email = ['brice@codeur.com'] + spec.homepage = 'https://github.com/codeur/reported' + spec.summary = 'CSP reports collection for Rails apps' + spec.description = 'A Rails engine that collects, stores and notifies on Slack about CSP violation reports' + spec.license = 'MIT' + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host" + # to allow pushing to a single host or delete this section to allow pushing to any host. + # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://github.com/codeur/reported" + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = spec.homepage + spec.metadata['changelog_uri'] = 'https://github.com/codeur/reported/blob/main/CHANGELOG.md' + spec.metadata['rubygems_mfa_required'] = 'false' - spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] + spec.files = Dir.chdir(File.expand_path(__dir__)) do + Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md'] + end - spec.required_ruby_version = ">= 3.2.0" - - spec.add_dependency "rails", ">= 7.1" + spec.required_ruby_version = '>= 3.4' - spec.add_development_dependency "sqlite3" - spec.add_development_dependency "webmock" - spec.add_development_dependency "rubocop-codeur" + spec.add_dependency 'rails', '>= 5.2', '< 8' end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/controllers/csp_reports_controller_test.rb b/test/controllers/csp_reports_controller_test.rb index 30f86f5..aedd3e4 100644 --- a/test/controllers/csp_reports_controller_test.rb +++ b/test/controllers/csp_reports_controller_test.rb @@ -1,92 +1,92 @@ # frozen_string_literal: true -require "test_helper" +require 'test_helper' module Reported class CspReportsControllerTest < ActionDispatch::IntegrationTest setup do @valid_csp_report_old_format = { - "csp-report" => { - "document-uri" => "https://example.com/page", - "violated-directive" => "script-src 'self'", - "blocked-uri" => "https://evil.com/script.js", - "original-policy" => "default-src 'self'; script-src 'self'" + 'csp-report' => { + 'document-uri' => 'https://example.com/page', + 'violated-directive' => "script-src 'self'", + 'blocked-uri' => 'https://evil.com/script.js', + 'original-policy' => "default-src 'self'; script-src 'self'" } } @valid_csp_report_new_format = { - "documentURI" => "https://example.com/page", - "violatedDirective" => "script-src 'self'", - "blockedURI" => "https://evil.com/script.js", - "originalPolicy" => "default-src 'self'; script-src 'self'" + 'documentURI' => 'https://example.com/page', + 'violatedDirective' => "script-src 'self'", + 'blockedURI' => 'https://evil.com/script.js', + 'originalPolicy' => "default-src 'self'; script-src 'self'" } end - test "creates report with valid CSP data (old format)" do - assert_difference "Report.count", 1 do - post "/csp-reports", - params: @valid_csp_report_old_format.to_json, - headers: { "CONTENT_TYPE" => "application/json" } + test 'creates report with valid CSP data (old format)' do + assert_difference 'Report.count', 1 do + post '/csp-reports', + params: @valid_csp_report_old_format.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } end assert_response :no_content report = Report.last - assert_equal "https://example.com/page", report.document_uri + assert_equal 'https://example.com/page', report.document_uri assert_equal "script-src 'self'", report.violated_directive - assert_equal "https://evil.com/script.js", report.blocked_uri + assert_equal 'https://evil.com/script.js', report.blocked_uri end - test "creates report with valid CSP data (new format)" do - assert_difference "Report.count", 1 do - post "/csp-reports", - params: @valid_csp_report_new_format.to_json, - headers: { "CONTENT_TYPE" => "application/json" } + test 'creates report with valid CSP data (new format)' do + assert_difference 'Report.count', 1 do + post '/csp-reports', + params: @valid_csp_report_new_format.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } end assert_response :no_content report = Report.last - assert_equal "https://example.com/page", report.document_uri + assert_equal 'https://example.com/page', report.document_uri assert_equal "script-src 'self'", report.violated_directive - assert_equal "https://evil.com/script.js", report.blocked_uri + assert_equal 'https://evil.com/script.js', report.blocked_uri end - test "returns bad_request with invalid JSON" do - assert_no_difference "Report.count" do - post "/csp-reports", - params: "invalid json{", - headers: { "CONTENT_TYPE" => "application/json" } + test 'returns bad_request with invalid JSON' do + assert_no_difference 'Report.count' do + post '/csp-reports', + params: 'invalid json{', + headers: { 'CONTENT_TYPE' => 'application/json' } end assert_response :bad_request end - test "returns bad_request with empty body" do - assert_no_difference "Report.count" do - post "/csp-reports", - params: "", - headers: { "CONTENT_TYPE" => "application/json" } + test 'returns bad_request with empty body' do + assert_no_difference 'Report.count' do + post '/csp-reports', + params: '', + headers: { 'CONTENT_TYPE' => 'application/json' } end assert_response :bad_request end - test "stores raw_report as JSONB" do - post "/csp-reports", - params: @valid_csp_report_old_format.to_json, - headers: { "CONTENT_TYPE" => "application/json" } + test 'stores raw_report as JSONB' do + post '/csp-reports', + params: @valid_csp_report_old_format.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } report = Report.last # With JSONB, raw_report is stored as a hash directly assert_equal @valid_csp_report_old_format.deep_stringify_keys, report.raw_report end - test "does not require CSRF token" do + test 'does not require CSRF token' do # This test verifies that external browsers can POST without CSRF token - post "/csp-reports", - params: @valid_csp_report_old_format.to_json, - headers: { "CONTENT_TYPE" => "application/json" } + post '/csp-reports', + params: @valid_csp_report_old_format.to_json, + headers: { 'CONTENT_TYPE' => 'application/json' } assert_response :no_content end diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile index ed646b6..488c551 100644 --- a/test/dummy/Rakefile +++ b/test/dummy/Rakefile @@ -1,3 +1,8 @@ +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + require_relative 'config/application' Rails.application.load_tasks diff --git a/test/dummy/app/assets/images/.keep b/test/dummy/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/app/assets/stylesheets/application.css b/test/dummy/app/assets/stylesheets/application.css new file mode 100644 index 0000000..0ebd7fe --- /dev/null +++ b/test/dummy/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000..9c1acb3 --- /dev/null +++ b/test/dummy/app/controllers/application_controller.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern +end diff --git a/test/dummy/app/controllers/concerns/.keep b/test/dummy/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/app/helpers/application_helper.rb b/test/dummy/app/helpers/application_helper.rb new file mode 100644 index 0000000..15b06f0 --- /dev/null +++ b/test/dummy/app/helpers/application_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module ApplicationHelper +end diff --git a/test/dummy/app/jobs/application_job.rb b/test/dummy/app/jobs/application_job.rb new file mode 100644 index 0000000..bef3959 --- /dev/null +++ b/test/dummy/app/jobs/application_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/test/dummy/app/mailers/application_mailer.rb b/test/dummy/app/mailers/application_mailer.rb new file mode 100644 index 0000000..d84cb6e --- /dev/null +++ b/test/dummy/app/mailers/application_mailer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb new file mode 100644 index 0000000..08dc537 --- /dev/null +++ b/test/dummy/app/models/application_record.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/test/dummy/app/models/concerns/.keep b/test/dummy/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb new file mode 100644 index 0000000..f25ae92 --- /dev/null +++ b/test/dummy/app/views/layouts/application.html.erb @@ -0,0 +1,27 @@ + + + + <%= content_for(:title) || "Dummy" %> + + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + + + + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app %> + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.html.erb b/test/dummy/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.text.erb b/test/dummy/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/test/dummy/app/views/pwa/manifest.json.erb b/test/dummy/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000..a3c046e --- /dev/null +++ b/test/dummy/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Dummy", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "Dummy.", + "theme_color": "red", + "background_color": "red" +} diff --git a/test/dummy/app/views/pwa/service-worker.js b/test/dummy/app/views/pwa/service-worker.js new file mode 100644 index 0000000..b3a13fb --- /dev/null +++ b/test/dummy/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/test/dummy/bin/dev b/test/dummy/bin/dev new file mode 100755 index 0000000..3925783 --- /dev/null +++ b/test/dummy/bin/dev @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +exec './bin/rails', 'server', *ARGV diff --git a/test/dummy/bin/rails b/test/dummy/bin/rails new file mode 100755 index 0000000..a31728a --- /dev/null +++ b/test/dummy/bin/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/test/dummy/bin/rake b/test/dummy/bin/rake new file mode 100755 index 0000000..c199955 --- /dev/null +++ b/test/dummy/bin/rake @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/test/dummy/bin/setup b/test/dummy/bin/setup new file mode 100755 index 0000000..5e6d2fe --- /dev/null +++ b/test/dummy/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'fileutils' + +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*) + system(*, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system('bundle check') || system!('bundle install') + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + unless ARGV.include?('--skip-server') + puts "\n== Starting development server ==" + $stdout.flush # flush the output before exec(2) so that it displays + exec 'bin/dev' + end +end diff --git a/test/dummy/config.ru b/test/dummy/config.ru new file mode 100644 index 0000000..6dc8321 --- /dev/null +++ b/test/dummy/config.ru @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application +Rails.application.load_server diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index f73331a..8dbd1cd 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -1,15 +1,31 @@ -require_relative '../../test_helper' +# frozen_string_literal: true + +require_relative 'boot' + require 'rails/all' +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) -require "reported" module Dummy class Application < Rails::Application config.load_defaults Rails::VERSION::STRING.to_f - config.eager_load = false - - # For compatibility with tests - config.active_storage.service = :test if config.respond_to?(:active_storage) + + # For compatibility with applications that use this config + config.action_controller.include_all_helpers = false + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") end end diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb new file mode 100644 index 0000000..6d2cba0 --- /dev/null +++ b/test/dummy/config/boot.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/test/dummy/config/cable.yml b/test/dummy/config/cable.yml new file mode 100644 index 0000000..98367f8 --- /dev/null +++ b/test/dummy/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: dummy_production diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml index ff8c5d6..01bebb5 100644 --- a/test/dummy/config/database.yml +++ b/test/dummy/config/database.yml @@ -1,3 +1,32 @@ -test: +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default adapter: sqlite3 - database: db/test.sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# SQLite3 write its data on the local filesystem, as such it requires +# persistent disks. If you are deploying to a managed service, you should +# make sure it provides disk persistence, as many don't. +# +# Similarly, if you deploy your application as a Docker container, you must +# ensure the database is located in a persisted volume. +production: + <<: *default + # database: path/to/persistent/storage/production.sqlite3 diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb index 8fb6325..d5abe55 100644 --- a/test/dummy/config/environment.rb +++ b/test/dummy/config/environment.rb @@ -1,5 +1,7 @@ -# Load the Rails application +# frozen_string_literal: true + +# Load the Rails application. require_relative 'application' -# Initialize the Rails application +# Initialize the Rails application. Rails.application.initialize! diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb new file mode 100644 index 0000000..2270212 --- /dev/null +++ b/test/dummy/config/environments/development.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/integer/time' + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join('tmp', 'caching-dev.txt', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { 'cache-control' => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb new file mode 100644 index 0000000..0d647bb --- /dev/null +++ b/test/dummy/config/environments/production.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/integer/time' + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { 'cache-control' => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [:request_id] + config.logger = ActiveSupport::TaggedLogging.logger($stdout) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info') + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = '/up' + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + # config.cache_store = :mem_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + # config.active_job.queue_adapter = :resque + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: 'example.com' } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [:id] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb new file mode 100644 index 0000000..37fc1f7 --- /dev/null +++ b/test/dummy/config/environments/test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV['CI'].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { 'cache-control' => 'public, max-age=3600' } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: 'example.com' } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/test/dummy/config/initializers/assets.rb b/test/dummy/config/initializers/assets.rb new file mode 100644 index 0000000..019d0bb --- /dev/null +++ b/test/dummy/config/initializers/assets.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/test/dummy/config/initializers/content_security_policy.rb b/test/dummy/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..35ab3fd --- /dev/null +++ b/test/dummy/config/initializers/content_security_policy.rb @@ -0,0 +1,27 @@ +# 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 + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/test/dummy/config/initializers/filter_parameter_logging.rb b/test/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..d81a902 --- /dev/null +++ b/test/dummy/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += %i[ + passw email secret token _key crypt salt certificate otp ssn cvv cvc +] diff --git a/test/dummy/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb new file mode 100644 index 0000000..9e049dc --- /dev/null +++ b/test/dummy/config/initializers/inflections.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/test/dummy/config/locales/en.yml b/test/dummy/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/test/dummy/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/test/dummy/config/puma.rb b/test/dummy/config/puma.rb new file mode 100644 index 0000000..f4aa1fe --- /dev/null +++ b/test/dummy/config/puma.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch('RAILS_MAX_THREADS', 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch('PORT', 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV['PIDFILE'] if ENV['PIDFILE'] diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 0c865d4..f9adc06 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.routes.draw do - # Routes are automatically added by Reported::Engine initializer + mount Reported::Engine => '/reported' end diff --git a/test/dummy/config/storage.yml b/test/dummy/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/test/dummy/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb new file mode 100644 index 0000000..f19c3c7 --- /dev/null +++ b/test/dummy/db/schema.rb @@ -0,0 +1,26 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 2024_01_01_000000) do + create_table "reported_reports", force: :cascade do |t| + t.string "document_uri" + t.string "violated_directive" + t.string "blocked_uri" + t.text "original_policy" + t.json "raw_report", default: {}, null: false + t.datetime "notified_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["created_at"], name: "index_reported_reports_on_created_at" + t.index ["notified_at"], name: "index_reported_reports_on_notified_at" + end +end diff --git a/test/dummy/log/.keep b/test/dummy/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/navigation_test.rb b/test/integration/navigation_test.rb new file mode 100644 index 0000000..91d920f --- /dev/null +++ b/test/integration/navigation_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class NavigationTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/jobs/notification_job_test.rb b/test/jobs/notification_job_test.rb index f34b678..e92224b 100644 --- a/test/jobs/notification_job_test.rb +++ b/test/jobs/notification_job_test.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true -require "test_helper" +require 'test_helper' module Reported class NotificationJobTest < ActiveJob::TestCase setup do @report = Report.create!( - document_uri: "https://example.com/page", + document_uri: 'https://example.com/page', violated_directive: "script-src 'self'", - blocked_uri: "https://evil.com/script.js", - raw_report: { "csp-report" => {} } + blocked_uri: 'https://evil.com/script.js', + raw_report: { 'csp-report' => {} } ) - Reported.slack_webhook_url = "https://hooks.slack.com/services/TEST/WEBHOOK/URL" + Reported.slack_webhook_url = 'https://hooks.slack.com/services/TEST/WEBHOOK/URL' end - test "sends notification to Slack" do + test 'sends notification to Slack' do stub_request(:post, Reported.slack_webhook_url) - .to_return(status: 200, body: "ok") + .to_return(status: 200, body: 'ok') NotificationJob.perform_now(@report.id) assert_requested :post, Reported.slack_webhook_url end - test "marks report as notified after successful notification" do + test 'marks report as notified after successful notification' do stub_request(:post, Reported.slack_webhook_url) - .to_return(status: 200, body: "ok") + .to_return(status: 200, body: 'ok') assert_nil @report.notified_at @@ -35,38 +35,38 @@ class NotificationJobTest < ActiveJob::TestCase assert_not_nil @report.notified_at end - test "does not send notification if report already notified" do + test 'does not send notification if report already notified' do @report.mark_as_notified! stub_request(:post, Reported.slack_webhook_url) - .to_return(status: 200, body: "ok") + .to_return(status: 200, body: 'ok') NotificationJob.perform_now(@report.id) assert_not_requested :post, Reported.slack_webhook_url end - test "does not send notification if webhook URL is not configured" do + test 'does not send notification if webhook URL is not configured' do Reported.slack_webhook_url = nil - stub_request(:post, "https://hooks.slack.com/services/TEST/WEBHOOK/URL") - .to_return(status: 200, body: "ok") + stub_request(:post, 'https://hooks.slack.com/services/TEST/WEBHOOK/URL') + .to_return(status: 200, body: 'ok') NotificationJob.perform_now(@report.id) - assert_not_requested :post, "https://hooks.slack.com/services/TEST/WEBHOOK/URL" + assert_not_requested :post, 'https://hooks.slack.com/services/TEST/WEBHOOK/URL' end - test "includes CSP violation details in Slack message" do + test 'includes CSP violation details in Slack message' do stub_request(:post, Reported.slack_webhook_url) .with do |request| body = JSON.parse(request.body) - body["text"] == "CSP Violation Report" && - body["attachments"].any? do |a| - a["fields"].any? { |f| f["title"] == "Document URI" && f["value"] == @report.document_uri } + body['text'] == 'CSP Violation Report' && + body['attachments'].any? do |a| + a['fields'].any? { |f| f['title'] == 'Document URI' && f['value'] == @report.document_uri } end end - .to_return(status: 200, body: "ok") + .to_return(status: 200, body: 'ok') NotificationJob.perform_now(@report.id) diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/report_test.rb b/test/models/report_test.rb index 9d5f011..3c55439 100644 --- a/test/models/report_test.rb +++ b/test/models/report_test.rb @@ -1,46 +1,46 @@ # frozen_string_literal: true -require "test_helper" +require 'test_helper' module Reported class ReportTest < ActiveSupport::TestCase - test "requires raw_report" do + test 'requires raw_report' do report = Report.new assert_not report.valid? assert_includes report.errors[:raw_report], "can't be blank" end - test "can create report with raw_report" do - report = Report.new(raw_report: { "csp-report" => {} }) + test 'can create report with raw_report' do + report = Report.new(raw_report: { 'csp-report' => {} }) assert report.valid? end - test "not_notified scope returns reports without notified_at" do - report1 = Report.create!(raw_report: { "test" => 1 }) - report2 = Report.create!(raw_report: { "test" => 2 }, notified_at: Time.current) + test 'not_notified scope returns reports without notified_at' do + report1 = Report.create!(raw_report: { 'test' => 1 }) + report2 = Report.create!(raw_report: { 'test' => 2 }, notified_at: Time.current) assert_includes Report.not_notified, report1 assert_not_includes Report.not_notified, report2 end - test "notified scope returns reports with notified_at" do - report1 = Report.create!(raw_report: { "test" => 1 }) - report2 = Report.create!(raw_report: { "test" => 2 }, notified_at: Time.current) + test 'notified scope returns reports with notified_at' do + report1 = Report.create!(raw_report: { 'test' => 1 }) + report2 = Report.create!(raw_report: { 'test' => 2 }, notified_at: Time.current) assert_not_includes Report.notified, report1 assert_includes Report.notified, report2 end - test "mark_as_notified! sets notified_at" do - report = Report.create!(raw_report: { "test" => 1 }) + test 'mark_as_notified! sets notified_at' do + report = Report.create!(raw_report: { 'test' => 1 }) assert_nil report.notified_at report.mark_as_notified! assert_not_nil report.notified_at end - test "notified? returns true when notified_at is set" do - report = Report.create!(raw_report: { "test" => 1 }) + test 'notified? returns true when notified_at is set' do + report = Report.create!(raw_report: { 'test' => 1 }) assert_not report.notified? report.update!(notified_at: Time.current) diff --git a/test/reported_test.rb b/test/reported_test.rb new file mode 100644 index 0000000..4110a46 --- /dev/null +++ b/test/reported_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ReportedTest < ActiveSupport::TestCase + test 'it has a version number' do + assert Reported::VERSION + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index a0491bd..a6f2843 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,24 +1,16 @@ # frozen_string_literal: true # Configure Rails Environment -ENV["RAILS_ENV"] = "test" +ENV['RAILS_ENV'] = 'test' -require "minitest/autorun" - -# Placeholder for when tests are run in a Rails application context -# This allows the gem to be tested independently -begin - require File.expand_path("test/dummy/config/environment", __dir__) -rescue LoadError - # If dummy app doesn't exist, just load the engine - require File.expand_path("lib/reported", __dir__) -end - -require "rails/test_help" +require_relative '../test/dummy/config/environment' +ActiveRecord::Migrator.migrations_paths = [File.expand_path('../test/dummy/db/migrate', __dir__)] +ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) +require 'rails/test_help' # Require webmock for testing HTTP requests begin - require "webmock/minitest" + require 'webmock/minitest' WebMock.disable_net_connect!(allow_localhost: true) rescue LoadError # webmock not available @@ -27,7 +19,10 @@ # Load support files Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } -if ActiveSupport::TestCase.respond_to?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) - ActiveSupport::TestCase.file_fixture_path = "#{ActiveSupport::TestCase.fixture_path}/files" +# Load fixtures from the engine +if ActiveSupport::TestCase.respond_to?(:fixture_paths=) + ActiveSupport::TestCase.fixture_paths = [File.expand_path('fixtures', __dir__)] + ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths + ActiveSupport::TestCase.file_fixture_path = "#{File.expand_path('fixtures', __dir__)}/files" + ActiveSupport::TestCase.fixtures :all end From 37d0ca1cb50f42f2cf87537c1f86887dcb7479c6 Mon Sep 17 00:00:00 2001 From: Brice TEXIER Date: Thu, 2 Oct 2025 15:06:50 +0200 Subject: [PATCH 09/12] Change run command --- .github/workflows/ci.yml | 2 +- config/routes.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 config/routes.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42392fa..628a507 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: env: RAILS_ENV: test # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:test:prepare test + run: bundle exec rails db:test:prepare test - name: Keep screenshots from failed system tests uses: actions/upload-artifact@v4 diff --git a/config/routes.rb b/config/routes.rb deleted file mode 100644 index 8e9b8f9..0000000 --- a/config/routes.rb +++ /dev/null @@ -1 +0,0 @@ -# frozen_string_literal: true From f66f587d6a200d60a2f5a97da539299b8b5c5d80 Mon Sep 17 00:00:00 2001 From: Brice TEXIER Date: Thu, 2 Oct 2025 15:10:16 +0200 Subject: [PATCH 10/12] Split commands --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 628a507..cd8d51a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: env: RAILS_ENV: test # REDIS_URL: redis://localhost:6379/0 - run: bundle exec rails db:test:prepare test + run: bin/rails db:test:prepare && bin/rails test - name: Keep screenshots from failed system tests uses: actions/upload-artifact@v4 From 5f3068930cdd0f06d6a8d4c9476f61a7d682a28c Mon Sep 17 00:00:00 2001 From: Brice TEXIER Date: Thu, 2 Oct 2025 15:14:26 +0200 Subject: [PATCH 11/12] Add Gemfile.lock --- .gitignore | 1 - Gemfile.lock | 276 +++++++++++++++++++++++++++++++++++++++++++++++ reported.gemspec | 2 +- 3 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 Gemfile.lock diff --git a/.gitignore b/.gitignore index 5568189..7787bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,3 @@ /test/dummy/tmp/ *.gem .byebug_history -Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..2ebf914 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,276 @@ +PATH + remote: . + specs: + reported (0.1.0) + rails (>= 7.1, < 8) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.2.2.2) + actionpack (= 7.2.2.2) + activesupport (= 7.2.2.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.2.2.2) + actionpack (= 7.2.2.2) + activejob (= 7.2.2.2) + activerecord (= 7.2.2.2) + activestorage (= 7.2.2.2) + activesupport (= 7.2.2.2) + mail (>= 2.8.0) + actionmailer (7.2.2.2) + actionpack (= 7.2.2.2) + actionview (= 7.2.2.2) + activejob (= 7.2.2.2) + activesupport (= 7.2.2.2) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.2.2) + actionview (= 7.2.2.2) + activesupport (= 7.2.2.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.2) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (7.2.2.2) + actionpack (= 7.2.2.2) + activerecord (= 7.2.2.2) + activestorage (= 7.2.2.2) + activesupport (= 7.2.2.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.2.2.2) + activesupport (= 7.2.2.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.2.2.2) + activesupport (= 7.2.2.2) + globalid (>= 0.3.6) + activemodel (7.2.2.2) + activesupport (= 7.2.2.2) + activerecord (7.2.2.2) + activemodel (= 7.2.2.2) + activesupport (= 7.2.2.2) + timeout (>= 0.4.0) + activestorage (7.2.2.2) + actionpack (= 7.2.2.2) + activejob (= 7.2.2.2) + activerecord (= 7.2.2.2) + activesupport (= 7.2.2.2) + marcel (~> 1.0) + activesupport (7.2.2.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.3) + builder (3.3.0) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + crack (1.0.0) + bigdecimal + rexml + crass (1.0.6) + date (3.4.1) + drb (2.2.3) + erb (5.0.2) + erubi (1.13.1) + globalid (1.3.0) + activesupport (>= 6.1) + hashdiff (1.2.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + io-console (0.8.1) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.15.0) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + mini_mime (1.1.5) + minitest (5.25.5) + net-imap (0.5.11) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + prism (1.5.1) + propshaft (1.3.1) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (7.0.4) + nio4r (~> 2.0) + racc (1.8.1) + rack (3.1.16) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (7.2.2.2) + actioncable (= 7.2.2.2) + actionmailbox (= 7.2.2.2) + actionmailer (= 7.2.2.2) + actionpack (= 7.2.2.2) + actiontext (= 7.2.2.2) + actionview (= 7.2.2.2) + activejob (= 7.2.2.2) + activemodel (= 7.2.2.2) + activerecord (= 7.2.2.2) + activestorage (= 7.2.2.2) + activesupport (= 7.2.2.2) + bundler (>= 1.15.0) + railties (= 7.2.2.2) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.2.2.2) + actionpack (= 7.2.2.2) + activesupport (= 7.2.2.2) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.0) + rdoc (6.14.2) + erb + psych (>= 4.0.0) + regexp_parser (2.11.3) + reline (0.6.2) + io-console (~> 0.5) + rexml (3.4.4) + rubocop (1.81.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-capybara (2.22.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-codeur (0.11.6) + lint_roller (~> 1.1) + rubocop (>= 1.72.0, < 2.0) + rubocop-capybara (~> 2.21) + rubocop-factory_bot (~> 2.26) + rubocop-minitest (~> 0.37) + rubocop-performance (~> 1.24) + rubocop-rails (~> 2.30) + rubocop-factory_bot (2.27.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-minitest (0.38.2) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-performance (1.26.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails (2.33.4) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + sqlite3 (2.7.4-arm64-darwin) + stringio (3.1.7) + thor (1.4.0) + timeout (0.4.3) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + useragent (0.16.11) + webmock (3.25.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.7.3) + +PLATFORMS + arm64-darwin + +DEPENDENCIES + propshaft + puma + reported! + rubocop-codeur + sqlite3 + webmock + +BUNDLED WITH + 2.7.1 diff --git a/reported.gemspec b/reported.gemspec index 3da245b..008746b 100644 --- a/reported.gemspec +++ b/reported.gemspec @@ -27,5 +27,5 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 3.4' - spec.add_dependency 'rails', '>= 5.2', '< 8' + spec.add_dependency 'rails', '>= 7.1', '< 8' end From 83dee91a94c3ef3c0d2f2d5bf1db5ba7c8ed43f5 Mon Sep 17 00:00:00 2001 From: Brice TEXIER Date: Thu, 2 Oct 2025 15:21:33 +0200 Subject: [PATCH 12/12] Fix Gemfile.lock for generic usage --- Gemfile.lock | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2ebf914..42226eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -119,6 +119,7 @@ GEM net-smtp marcel (1.1.0) mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (5.25.5) net-imap (0.5.11) date @@ -130,7 +131,8 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.10-arm64-darwin) + nokogiri (1.18.10) + mini_portile2 (~> 2.8.2) racc (~> 1.4) parallel (1.27.0) parser (3.3.9.0) @@ -241,7 +243,8 @@ GEM rubocop-ast (>= 1.44.0, < 2.0) ruby-progressbar (1.13.0) securerandom (0.4.1) - sqlite3 (2.7.4-arm64-darwin) + sqlite3 (2.7.4) + mini_portile2 (~> 2.8.0) stringio (3.1.7) thor (1.4.0) timeout (0.4.3) @@ -262,7 +265,7 @@ GEM zeitwerk (2.7.3) PLATFORMS - arm64-darwin + ruby DEPENDENCIES propshaft