-
Notifications
You must be signed in to change notification settings - Fork 0
Implement CSP reports collection Rails engine with Slack notifications #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
burisu
merged 12 commits into
main
from
copilot/fix-612be0fe-5797-40b7-8fa4-54bd7fa8760b
Oct 2, 2025
Merged
Changes from 5 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
976df1b
Initial plan
Copilot b3ac340
Create Rails engine gem structure with CSP reports collection
Copilot 8e48e55
Add comprehensive test suite with model, controller, and job tests
Copilot cdf1573
Add automatic route mounting, support modern CSP format, and GitHub w…
Copilot d82ec31
Update to Ruby >= 3.2, Rails >= 7.1, and use JSONB for raw_report
Copilot 4e09c83
Add rubocop-codeur and normalize Ruby code style
Copilot e6b7069
Fix code style in test files to match rubocop standards
Copilot aa32cac
Add default Rails::Engine structure
burisu 37d0ca1
Change run command
burisu f66f587
Split commands
burisu 5f30689
Add Gemfile.lock
burisu 83dee91
Fix Gemfile.lock for generic usage
burisu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.2', '3.3'] | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.1" | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,141 @@ | ||
| # 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 | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Ruby >= 3.2 | ||
| - Rails >= 7.1 | ||
| - PostgreSQL (for JSONB support) | ||
|
|
||
| ## 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. | ||
|
|
||
| The CSP reports endpoint is automatically available at `/csp-reports` (no mounting required). | ||
|
|
||
| ## 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 "/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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| 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 | ||
| # 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 | ||
|
|
||
| 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 | ||
|
|
||
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot Don't forget Ruby 3.4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added Ruby 3.4 to the workflow matrix in commit 4e09c83.