jekyll-csp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/jekyll-csp/csp.rb +321 -0
- data/lib/jekyll-csp/hook.rb +27 -0
- data/lib/jekyll-csp/version.rb +3 -0
- data/lib/jekyll-csp.rb +5 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ad7855278a3b719aaaa04add26777010e47ee2c9b25cd736b20e3fde75538e72
|
4
|
+
data.tar.gz: 13323d6f7ee84b5ef4f0a0d6e65ae5324664667fe890c2016bd3884972f0c212
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f4182dbcae069d047c16216eef429c151adb6bdeda1cc7405773ff5bf5ca7c4a8c69e6667f981757b9dae8bdcbe51c844499d818d0c7452c4ea53783f7a4029e
|
7
|
+
data.tar.gz: e0da23509ad6205c73d0e6d47c5521b851aefe8696c6711ce4debeef8480e8cbe0247f8f5df8aea6fc89fe08404965cc8acd04df4b0c7bbb3d4fc9872eb929e7
|
@@ -0,0 +1,321 @@
|
|
1
|
+
require 'jekyll'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'digest'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
##
|
8
|
+
# Provides the ability to generate a content security policy for inline scripts and styles.
|
9
|
+
# Will reuse an existing CSP or generate a new one and insert in HEAD.
|
10
|
+
module CSP
|
11
|
+
##
|
12
|
+
# Provides the ability to generate a content security policy for inline scripts and styles.
|
13
|
+
# Will reuse an existing CSP or generate a new one and insert in HEAD.
|
14
|
+
class Generator
|
15
|
+
def initialize(document_html)
|
16
|
+
@document_html = document_html
|
17
|
+
@nokogiri = Nokogiri::HTML(document_html)
|
18
|
+
|
19
|
+
@csp_tags = {
|
20
|
+
"frame-src" => [],
|
21
|
+
"script-src" => [],
|
22
|
+
"img-src" => [],
|
23
|
+
"style-src" => []
|
24
|
+
}
|
25
|
+
|
26
|
+
config = Jekyll.configuration({})['jekyll_csp']
|
27
|
+
|
28
|
+
@indentation = config['indentation'] || 2
|
29
|
+
@enable_newlines = config['newlines'].to_s ? config['newlines'] : true
|
30
|
+
@debug = config['debug'].to_s ? config['debug'] : false
|
31
|
+
@include_self = config['include_self'].to_s ? config['include_self'] : false
|
32
|
+
|
33
|
+
if @enable_newlines == false
|
34
|
+
@indentation = 0
|
35
|
+
end
|
36
|
+
|
37
|
+
self.write_debug_log(config)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
##
|
42
|
+
# Write a debug log
|
43
|
+
def write_debug_log(content)
|
44
|
+
if @debug
|
45
|
+
Jekyll.logger.warn content
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Generate a CSP entry using the correct indentation and formatting
|
51
|
+
def generate_meta_entry(tag, items)
|
52
|
+
# Remove duplicates
|
53
|
+
items = items.uniq
|
54
|
+
|
55
|
+
# Line separator
|
56
|
+
line_sep = @enable_newlines ? "\n" : ""
|
57
|
+
|
58
|
+
"" \
|
59
|
+
<< line_sep \
|
60
|
+
<< self.get_indent_str(3) \
|
61
|
+
<< tag \
|
62
|
+
<< " " \
|
63
|
+
<< line_sep \
|
64
|
+
<< self.get_indent_str(4) \
|
65
|
+
<< items.join(" " + line_sep + self.get_indent_str(4)) \
|
66
|
+
<< "; "
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Get an indentation string.
|
71
|
+
def get_indent_str(count)
|
72
|
+
" " * (@indentation * count)
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Creates an HTML content security policy meta tag.
|
77
|
+
def generate_convert_security_policy_meta_tag
|
78
|
+
meta_content = ""
|
79
|
+
|
80
|
+
@csp_tags.each do |tag, items|
|
81
|
+
meta_content += self.generate_meta_entry(tag, items)
|
82
|
+
end
|
83
|
+
|
84
|
+
csp = self.get_or_create_csp_tag
|
85
|
+
csp['content'] = meta_content
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_or_create_csp_tag
|
89
|
+
csp = @nokogiri.at_xpath("//meta[translate(@http-equiv, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'content-security-policy']")
|
90
|
+
|
91
|
+
if csp
|
92
|
+
return csp
|
93
|
+
end
|
94
|
+
|
95
|
+
tag = "<meta http-equiv=\"Content-Security-Policy\" content="">"
|
96
|
+
|
97
|
+
if @nokogiri.at("head")
|
98
|
+
self.write_debug_log("Generated content security policy, inserted in HEAD.")
|
99
|
+
@nokogiri.at("head") << tag
|
100
|
+
elsif @nokogiri.at("body")
|
101
|
+
self.write_debug_log("Generated content security policy, inserted in BODY.")
|
102
|
+
@nokogiri.at("body") << tag
|
103
|
+
else
|
104
|
+
self.write_debug_log("Generated content security policy but found no-where to insert it.")
|
105
|
+
end
|
106
|
+
|
107
|
+
csp = @nokogiri.at_xpath("//meta[translate(@http-equiv, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'content-security-policy']")
|
108
|
+
return csp
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Parse an existing content security policy meta tag
|
113
|
+
def parse_existing_meta_element()
|
114
|
+
csp = self.get_or_create_csp_tag
|
115
|
+
|
116
|
+
if csp
|
117
|
+
content = csp.attr('content')
|
118
|
+
content = content.strip! || content
|
119
|
+
policies = content.split(';')
|
120
|
+
|
121
|
+
policies.each do |policy|
|
122
|
+
policy = policy.strip! || policy
|
123
|
+
|
124
|
+
if policy.include? ' '
|
125
|
+
policy_parts = policy.split(' ')
|
126
|
+
|
127
|
+
self.write_debug_log("Found existing CSP meta tag for '" << policy_parts[0] << "', concatenating rather than creating.")
|
128
|
+
|
129
|
+
# If an existing tag doesn't exist, add it assuming the user knows best
|
130
|
+
if !@csp_tags.key?(policy_parts[0])
|
131
|
+
@csp_tags[policy_parts[0]] = []
|
132
|
+
end
|
133
|
+
|
134
|
+
@csp_tags[policy_parts[0]].concat(policy_parts.drop(1))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
@nokogiri.search('meta[http-equiv="Content-Security-Policy"]').each do |el|
|
139
|
+
el.remove
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Initialize some default values
|
146
|
+
def inject_defaults
|
147
|
+
if @include_self == false
|
148
|
+
return
|
149
|
+
end
|
150
|
+
|
151
|
+
@csp_tags.each do |tag, items|
|
152
|
+
items.push("'self'")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# This function converts elements with style="color:red" attributes into inline styles
|
158
|
+
def convert_all_inline_styles_attributes
|
159
|
+
@nokogiri.css('*').each do |find|
|
160
|
+
find_src = find.attr('style')
|
161
|
+
|
162
|
+
if find_src
|
163
|
+
if find.attr('id')
|
164
|
+
element_id = find.attr('id')
|
165
|
+
else
|
166
|
+
hash = Digest::MD5.hexdigest find_src + "#{Random.rand(11)}"
|
167
|
+
element_id = "csp-gen-" + hash
|
168
|
+
find["id"] = element_id
|
169
|
+
end
|
170
|
+
|
171
|
+
new_element = "<style>#" + element_id + " { " + find_src + " } </style>"
|
172
|
+
find.remove_attribute("style")
|
173
|
+
|
174
|
+
if @nokogiri.at('head')
|
175
|
+
@nokogiri.at('head') << new_element
|
176
|
+
self.write_debug_log('Converting style attribute to inline style, inserted into HEAD.')
|
177
|
+
else
|
178
|
+
if @nokogiri.at('body')
|
179
|
+
@nokogiri.at('body') << new_element
|
180
|
+
Jekyll.logger.info
|
181
|
+
self.write_debug_log('Converting style attribute to inline style, inserted into BODY.')
|
182
|
+
else
|
183
|
+
self.write_debug_log('Unable to convert style attribute to inline style, no HEAD or BODY found.')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Find all images
|
192
|
+
def find_images
|
193
|
+
@nokogiri.css('img').each do |find|
|
194
|
+
find_src = find.attr('src')
|
195
|
+
|
196
|
+
if find_src and find_src.start_with?('http', 'https')
|
197
|
+
@csp_tags['img-src'].push find_src.match(/(.*\/)+(.*$)/)[1]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
@nokogiri.css('style').each do |find|
|
202
|
+
finds = find.content.scan(/url\(([^\)]+)\)/)
|
203
|
+
|
204
|
+
finds.each do |innerFind|
|
205
|
+
innerFind = innerFind[0]
|
206
|
+
innerFind = innerFind.tr('\'"', '')
|
207
|
+
if innerFind.start_with?('http', 'https')
|
208
|
+
@csp_tags['img-src'].push self.get_domain(innerFind)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
##
|
216
|
+
# Find all scripts
|
217
|
+
def find_scripts
|
218
|
+
@nokogiri.css('script').each do |find|
|
219
|
+
if find.attr('src')
|
220
|
+
find_src = find.attr('src')
|
221
|
+
|
222
|
+
if find_src and find_src.start_with?('http', 'https')
|
223
|
+
@csp_tags['script-src'].push find_src.match(/(.*\/)+(.*$)/)[1]
|
224
|
+
end
|
225
|
+
|
226
|
+
else
|
227
|
+
@csp_tags['script-src'].push self.generate_sha256_content_hash find.content
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
##
|
233
|
+
# Find all inline stylesheets
|
234
|
+
def find_inline_styles
|
235
|
+
@nokogiri.css('style').each do |find|
|
236
|
+
@csp_tags['style-src'].push self.generate_sha256_content_hash find.content
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
##
|
241
|
+
# Find all linked stylesheets
|
242
|
+
def find_linked_styles
|
243
|
+
@nokogiri.css('link').each do |find|
|
244
|
+
self.write_debug_log(find)
|
245
|
+
find_attr = find.attr('href')
|
246
|
+
|
247
|
+
if find_attr
|
248
|
+
@csp_tags['style-src'].push find_attr
|
249
|
+
else
|
250
|
+
self.write_debug_log("Found linked style with no href." << find)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
##
|
256
|
+
# Find all iframes
|
257
|
+
def find_frames
|
258
|
+
@nokogiri.css('iframe, frame').each do |find|
|
259
|
+
find_src = find.attr('src')
|
260
|
+
|
261
|
+
if find_src and find_src.start_with?('http', 'https')
|
262
|
+
@csp_tags['frame-src'].push find_src
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def get_domain(url)
|
268
|
+
uri = URI.parse(url)
|
269
|
+
"#{uri.scheme}://#{uri.host}"
|
270
|
+
end
|
271
|
+
|
272
|
+
##
|
273
|
+
# Generate a SHA256 hash from content
|
274
|
+
def generate_sha256_content_hash(content)
|
275
|
+
hash = Digest::SHA2.base64digest content
|
276
|
+
"'sha256-#{hash}'"
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Builds an HTML meta tag based on the found inline scripts and style hashes
|
281
|
+
def run
|
282
|
+
self.parse_existing_meta_element
|
283
|
+
self.inject_defaults
|
284
|
+
self.convert_all_inline_styles_attributes
|
285
|
+
|
286
|
+
# Find elements in document
|
287
|
+
self.find_linked_styles
|
288
|
+
self.find_images
|
289
|
+
self.find_inline_styles
|
290
|
+
self.find_scripts
|
291
|
+
self.find_frames
|
292
|
+
|
293
|
+
self.generate_convert_security_policy_meta_tag
|
294
|
+
|
295
|
+
@nokogiri.to_html
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# Write the file contents back.
|
301
|
+
def write_file_contents(dest, content)
|
302
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
303
|
+
File.open(dest, 'w') do |f|
|
304
|
+
f.write(content)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
##
|
309
|
+
# Write document contents
|
310
|
+
def write(dest)
|
311
|
+
dest_path = destination(dest)
|
312
|
+
|
313
|
+
if File.extname(dest_path) == ".html"
|
314
|
+
content_security_policy_generator = Generator.new output
|
315
|
+
self.write_file_contents(dest_path, content_security_policy_generator.run)
|
316
|
+
else
|
317
|
+
self.write_file_contents(dest_path, output)
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'jekyll'
|
2
|
+
require_relative 'csp.rb'
|
3
|
+
|
4
|
+
module Jekyll
|
5
|
+
class Document
|
6
|
+
include CSP
|
7
|
+
|
8
|
+
##
|
9
|
+
# Write document contents
|
10
|
+
def write(dest)
|
11
|
+
super dest
|
12
|
+
trigger_hooks(:post_write)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Page
|
17
|
+
include CSP
|
18
|
+
|
19
|
+
##
|
20
|
+
# Write page contents
|
21
|
+
def write(dest)
|
22
|
+
super dest
|
23
|
+
|
24
|
+
Jekyll::Hooks.trigger hook_owner, :post_write, self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/jekyll-csp.rb
ADDED
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jekyll-csp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- scottstraughan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: jekyll
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.4.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.4.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: digest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.2.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.2.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: nokogiri
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.18.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.18.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Will generate a content-security-policy based on images, scripts, stylesheets,
|
98
|
+
frames andothers on each generated page. This script assumes that all your linked
|
99
|
+
resources as 'safe'.Style attributes will also be converted into <style> elements
|
100
|
+
and SHA256 hashes will begenerated for inline styles/scripts.
|
101
|
+
email:
|
102
|
+
- ''
|
103
|
+
executables: []
|
104
|
+
extensions: []
|
105
|
+
extra_rdoc_files: []
|
106
|
+
files:
|
107
|
+
- lib/jekyll-csp.rb
|
108
|
+
- lib/jekyll-csp/csp.rb
|
109
|
+
- lib/jekyll-csp/hook.rb
|
110
|
+
- lib/jekyll-csp/version.rb
|
111
|
+
homepage: https://github.com/scottstraughan/jekyll-csp
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubygems_version: 3.5.22
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Generate a Content Security Policy HTML meta tag based on found inline scripts,
|
134
|
+
inline styles etc.
|
135
|
+
test_files: []
|