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 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
@@ -0,0 +1,3 @@
1
+ module JekyllCSP
2
+ VERSION = "1.0.0".freeze
3
+ end
data/lib/jekyll-csp.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative "jekyll-csp/version"
2
+ require_relative "jekyll-csp/hook"
3
+
4
+ module JekyllCSP
5
+ end
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: []