Skip to content

Commit cdf1573

Browse files
Copilotburisu
andcommitted
Add automatic route mounting, support modern CSP format, and GitHub workflows
Co-authored-by: burisu <240595+burisu@users.noreply.github.com>
1 parent 8e48e55 commit cdf1573

File tree

8 files changed

+105
-38
lines changed

8 files changed

+105
-38
lines changed

.github/workflows/test.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
ruby-version: ['3.0', '3.1', '3.2']
16+
rails-version: ['6.1', '7.0', '7.1']
17+
18+
steps:
19+
- uses: actions/checkout@v3
20+
21+
- name: Set up Ruby
22+
uses: ruby/setup-ruby@v1
23+
with:
24+
ruby-version: ${{ matrix.ruby-version }}
25+
bundler-cache: true
26+
27+
- name: Install dependencies
28+
run: bundle install
29+
30+
- name: Run tests
31+
run: bundle exec rake test

README.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,7 @@ $ rails db:migrate
4949

5050
This creates the `reported_reports` table.
5151

52-
3. Mount the engine in your `config/routes.rb`:
53-
54-
```ruby
55-
Rails.application.routes.draw do
56-
mount Reported::Engine, at: "/reported"
57-
# ... your other routes
58-
end
59-
```
60-
61-
This makes the CSP reports endpoint available at `/reported/csp-reports`.
52+
The CSP reports endpoint is automatically available at `/csp-reports` (no mounting required).
6253

6354
## Configuration
6455

@@ -73,7 +64,7 @@ Rails.application.config.content_security_policy do |policy|
7364
# ... your other CSP directives ...
7465

7566
# Configure the report URI
76-
policy.report_uri "/reported/csp-reports"
67+
policy.report_uri "/csp-reports"
7768
end
7869
```
7970

app/controllers/reported/csp_reports_controller.rb

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ def create
77
report_data = parse_report_data
88

99
if report_data
10+
# Extract CSP report data, supporting both old and new formats
11+
csp_data = extract_csp_data(report_data)
12+
1013
report = Report.create!(
11-
document_uri: report_data.dig('csp-report', 'document-uri'),
12-
violated_directive: report_data.dig('csp-report', 'violated-directive'),
13-
blocked_uri: report_data.dig('csp-report', 'blocked-uri'),
14-
original_policy: report_data.dig('csp-report', 'original-policy'),
14+
document_uri: csp_data[:document_uri],
15+
violated_directive: csp_data[:violated_directive],
16+
blocked_uri: csp_data[:blocked_uri],
17+
original_policy: csp_data[:original_policy],
1518
raw_report: report_data.to_json
1619
)
1720

@@ -38,5 +41,27 @@ def parse_report_data
3841
Rails.logger.error("Error parsing CSP report JSON: #{e.message}")
3942
nil
4043
end
44+
45+
def extract_csp_data(report_data)
46+
# Support both old format (csp-report) and new format (direct fields)
47+
if report_data['csp-report']
48+
# Old format: {"csp-report": {...}}
49+
csp_report = report_data['csp-report']
50+
{
51+
document_uri: csp_report['document-uri'] || csp_report['documentURI'],
52+
violated_directive: csp_report['violated-directive'] || csp_report['violatedDirective'] || csp_report['effective-directive'] || csp_report['effectiveDirective'],
53+
blocked_uri: csp_report['blocked-uri'] || csp_report['blockedURI'],
54+
original_policy: csp_report['original-policy'] || csp_report['originalPolicy']
55+
}
56+
else
57+
# New format: direct fields or camelCase
58+
{
59+
document_uri: report_data['document-uri'] || report_data['documentURI'] || report_data['document_uri'],
60+
violated_directive: report_data['violated-directive'] || report_data['violatedDirective'] || report_data['effective-directive'] || report_data['effectiveDirective'] || report_data['violated_directive'],
61+
blocked_uri: report_data['blocked-uri'] || report_data['blockedURI'] || report_data['blocked_uri'],
62+
original_policy: report_data['original-policy'] || report_data['originalPolicy'] || report_data['original_policy']
63+
}
64+
end
65+
end
4166
end
4267
end

config/routes.rb

Lines changed: 0 additions & 3 deletions
This file was deleted.

lib/generators/reported/install/README

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,15 @@ Next steps:
99
rails reported:install:migrations
1010
rails db:migrate
1111

12-
2. Mount the engine in your routes.rb:
13-
14-
mount Reported::Engine, at: "/reported"
15-
16-
This will make the CSP reports endpoint available at /reported/csp-reports
12+
2. The CSP reports endpoint is automatically available at /csp-reports
1713

1814
3. Configure your Content Security Policy to send reports to this endpoint:
1915

2016
In your config/initializers/content_security_policy.rb:
2117

2218
Rails.application.config.content_security_policy do |policy|
2319
# ... your CSP directives ...
24-
policy.report_uri "/reported/csp-reports"
20+
policy.report_uri "/csp-reports"
2521
end
2622

2723
4. Configure Slack notifications in config/initializers/reported.rb

lib/reported/engine.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,12 @@ class Engine < ::Rails::Engine
66
g.test_framework :test_unit
77
g.fixture_replacement :factory_bot, dir: 'spec/factories'
88
end
9+
10+
# Automatically add routes to the main application
11+
initializer "reported.add_routes" do |app|
12+
app.routes.prepend do
13+
post '/csp-reports', to: 'reported/csp_reports#create'
14+
end
15+
end
916
end
1017
end

test/controllers/csp_reports_controller_test.rb

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,43 @@
22

33
module Reported
44
class CspReportsControllerTest < ActionDispatch::IntegrationTest
5-
include Engine.routes.url_helpers
6-
75
setup do
8-
@valid_csp_report = {
6+
@valid_csp_report_old_format = {
97
"csp-report" => {
108
"document-uri" => "https://example.com/page",
119
"violated-directive" => "script-src 'self'",
1210
"blocked-uri" => "https://evil.com/script.js",
1311
"original-policy" => "default-src 'self'; script-src 'self'"
1412
}
1513
}
14+
15+
@valid_csp_report_new_format = {
16+
"documentURI" => "https://example.com/page",
17+
"violatedDirective" => "script-src 'self'",
18+
"blockedURI" => "https://evil.com/script.js",
19+
"originalPolicy" => "default-src 'self'; script-src 'self'"
20+
}
21+
end
22+
23+
test "creates report with valid CSP data (old format)" do
24+
assert_difference 'Report.count', 1 do
25+
post '/csp-reports',
26+
params: @valid_csp_report_old_format.to_json,
27+
headers: { 'CONTENT_TYPE' => 'application/json' }
28+
end
29+
30+
assert_response :no_content
31+
32+
report = Report.last
33+
assert_equal "https://example.com/page", report.document_uri
34+
assert_equal "script-src 'self'", report.violated_directive
35+
assert_equal "https://evil.com/script.js", report.blocked_uri
1636
end
1737

18-
test "creates report with valid CSP data" do
38+
test "creates report with valid CSP data (new format)" do
1939
assert_difference 'Report.count', 1 do
20-
post csp_reports_url,
21-
params: @valid_csp_report.to_json,
40+
post '/csp-reports',
41+
params: @valid_csp_report_new_format.to_json,
2242
headers: { 'CONTENT_TYPE' => 'application/json' }
2343
end
2444

@@ -32,7 +52,7 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest
3252

3353
test "returns bad_request with invalid JSON" do
3454
assert_no_difference 'Report.count' do
35-
post csp_reports_url,
55+
post '/csp-reports',
3656
params: "invalid json{",
3757
headers: { 'CONTENT_TYPE' => 'application/json' }
3858
end
@@ -42,7 +62,7 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest
4262

4363
test "returns bad_request with empty body" do
4464
assert_no_difference 'Report.count' do
45-
post csp_reports_url,
65+
post '/csp-reports',
4666
params: "",
4767
headers: { 'CONTENT_TYPE' => 'application/json' }
4868
end
@@ -51,19 +71,19 @@ class CspReportsControllerTest < ActionDispatch::IntegrationTest
5171
end
5272

5373
test "stores raw_report as JSON" do
54-
post csp_reports_url,
55-
params: @valid_csp_report.to_json,
74+
post '/csp-reports',
75+
params: @valid_csp_report_old_format.to_json,
5676
headers: { 'CONTENT_TYPE' => 'application/json' }
5777

5878
report = Report.last
5979
parsed = JSON.parse(report.raw_report)
60-
assert_equal @valid_csp_report, parsed
80+
assert_equal @valid_csp_report_old_format, parsed
6181
end
6282

6383
test "does not require CSRF token" do
6484
# This test verifies that external browsers can POST without CSRF token
65-
post csp_reports_url,
66-
params: @valid_csp_report.to_json,
85+
post '/csp-reports',
86+
params: @valid_csp_report_old_format.to_json,
6787
headers: { 'CONTENT_TYPE' => 'application/json' }
6888

6989
assert_response :no_content

test/dummy/config/routes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Rails.application.routes.draw do
2-
mount Reported::Engine => "/reported"
2+
# Routes are automatically added by Reported::Engine initializer
33
end

0 commit comments

Comments
 (0)