paraxial 1.4.5 → 1.5.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 +4 -4
- data/lib/paraxial/cli.rb +1 -0
- data/lib/paraxial/severities.rb +57 -0
- data/lib/paraxial/version.rb +1 -1
- data/lib/paraxial.rb +2 -0
- data/lib/rubocop/cop/paraxial/dynamic_regexp.rb +118 -0
- metadata +5 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f24195885218479f2c0b5f6c8963d3ecacdcfa8e59e60bcb731a75750a63c891
|
|
4
|
+
data.tar.gz: e178051703824267b31bd7945e060272c6eada4029a03a5f35294dad5e68848b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db01f4a55e4ae5425d2e3493bc7a374d2922a0f3aa54ac63a7d4b783bd01ad698b5d2818eff2429355484fdba2195410f325b0c8b67a6fb87e5a58175a9b145e
|
|
7
|
+
data.tar.gz: 7482ae35903800d6c5b2c5a54b5ff8223b4d311117d41375914eb5edad35d6b4e548777395fceac31479739ad4479c0c788000ceb286f57645d53c8a4fa7393d
|
data/lib/paraxial/cli.rb
CHANGED
|
@@ -79,6 +79,7 @@ module Paraxial
|
|
|
79
79
|
else
|
|
80
80
|
rubocop = `rubocop --require paraxial --only #{cops} --disable-pending-cops --format json #{rubo_config}`
|
|
81
81
|
end
|
|
82
|
+
rubocop = Paraxial::Severities.annotate(rubocop)
|
|
82
83
|
lockfile = File.read('./Gemfile.lock')
|
|
83
84
|
api_key = ENV['PARAXIAL_API_KEY']
|
|
84
85
|
uri = URI.parse(Paraxial::Helpers.get_paraxial_url + '/api/ruby_scan')
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Paraxial
|
|
6
|
+
# The severity a cop's findings are reported at. The cop that produces a
|
|
7
|
+
# finding is the source of truth for how bad it is, so a new cop is a line
|
|
8
|
+
# here rather than a change to the backend.
|
|
9
|
+
module Severities
|
|
10
|
+
DEFAULT = 'medium'
|
|
11
|
+
|
|
12
|
+
SEVERITIES = {
|
|
13
|
+
'Paraxial/CSRF' => 'medium',
|
|
14
|
+
'Paraxial/Constantize' => 'high',
|
|
15
|
+
'Paraxial/DynamicRegexp' => 'high',
|
|
16
|
+
'Paraxial/HTMLSafe' => 'high',
|
|
17
|
+
'Paraxial/Raw' => 'high',
|
|
18
|
+
'Paraxial/SQL' => 'critical',
|
|
19
|
+
'Paraxial/Send' => 'critical',
|
|
20
|
+
'Paraxial/SkipAuthenticityToken' => 'medium',
|
|
21
|
+
'Paraxial/System' => 'critical',
|
|
22
|
+
'Security/Eval' => 'critical',
|
|
23
|
+
'Security/IoMethods' => 'medium',
|
|
24
|
+
'Security/JSONLoad' => 'medium',
|
|
25
|
+
'Security/MarshalLoad' => 'critical',
|
|
26
|
+
'Security/Open' => 'critical',
|
|
27
|
+
'Security/YAMLLoad' => 'medium'
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def self.for_cop(cop_name)
|
|
31
|
+
SEVERITIES.fetch(cop_name, DEFAULT)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Adds the severity to every offense in rubocop's JSON output, so that a
|
|
35
|
+
# finding is stored with the severity of the cop that reported it.
|
|
36
|
+
def self.annotate(rubocop_json)
|
|
37
|
+
scan = JSON.parse(rubocop_json)
|
|
38
|
+
files = scan['files']
|
|
39
|
+
return rubocop_json unless files.is_a?(Array)
|
|
40
|
+
|
|
41
|
+
files.each do |file|
|
|
42
|
+
offenses = file['offenses']
|
|
43
|
+
next unless offenses.is_a?(Array)
|
|
44
|
+
|
|
45
|
+
offenses.each do |offense|
|
|
46
|
+
offense['paraxial_severity'] = for_cop(offense['cop_name'])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
JSON.generate(scan)
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
# rubocop exited before it wrote any JSON. Upload what it did print and
|
|
53
|
+
# let the backend report the failure, as the agent did before.
|
|
54
|
+
rubocop_json
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/paraxial/version.rb
CHANGED
data/lib/paraxial.rb
CHANGED
|
@@ -11,6 +11,8 @@ require_relative 'rubocop/cop/paraxial/constantize'
|
|
|
11
11
|
require_relative 'rubocop/cop/paraxial/html_safe'
|
|
12
12
|
require_relative 'rubocop/cop/paraxial/raw'
|
|
13
13
|
require_relative 'rubocop/cop/paraxial/sql'
|
|
14
|
+
require_relative 'rubocop/cop/paraxial/dynamic_regexp'
|
|
15
|
+
require_relative 'paraxial/severities'
|
|
14
16
|
require_relative 'paraxial/version'
|
|
15
17
|
require_relative 'paraxial/cli'
|
|
16
18
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Paraxial
|
|
6
|
+
# This cop checks for regular expressions that are compiled from a
|
|
7
|
+
# pattern computed at runtime. Ruby's regexp engine has memory safety
|
|
8
|
+
# bugs (CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, and
|
|
9
|
+
# CVE-2026-110264) that are only reachable when the pattern itself comes
|
|
10
|
+
# from user input. Neither `Regexp.timeout` nor limiting the length of
|
|
11
|
+
# the pattern is a mitigation, the fix is Ruby 4.0.9, 3.4.12, or 3.3.13.
|
|
12
|
+
#
|
|
13
|
+
# `Regexp.escape` neutralizes the metacharacters these bugs are reached
|
|
14
|
+
# through, so a pattern that is fully escaped is not reported.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# Regexp.new(params[:query])
|
|
19
|
+
# Regexp.compile(search_term)
|
|
20
|
+
# Regexp.union(params[:tags])
|
|
21
|
+
#
|
|
22
|
+
# # good
|
|
23
|
+
# Regexp.new('\A[a-z]+\z')
|
|
24
|
+
# Regexp.new(QUERY_PATTERN, Regexp::IGNORECASE)
|
|
25
|
+
# Regexp.new(Regexp.escape(params[:query]))
|
|
26
|
+
#
|
|
27
|
+
# @example EnforcedStyle: params (default)
|
|
28
|
+
# # bad
|
|
29
|
+
# /#{params[:query]}/
|
|
30
|
+
#
|
|
31
|
+
# # good
|
|
32
|
+
# # Interpolation of a value that is not read from the request.
|
|
33
|
+
# /\A#{PREFIX}-#{name}\z/
|
|
34
|
+
#
|
|
35
|
+
# @example EnforcedStyle: all
|
|
36
|
+
# # bad
|
|
37
|
+
# /#{params[:query]}/
|
|
38
|
+
# # Every interpolation is reported, including false positives.
|
|
39
|
+
# /\A#{PREFIX}-#{name}\z/
|
|
40
|
+
#
|
|
41
|
+
# # good
|
|
42
|
+
# /\A#{PREFIX}-[a-z]+\z/
|
|
43
|
+
class DynamicRegexp < Base
|
|
44
|
+
MSG = 'Regexp compiled from user input causes memory corruption in the regexp engine.'
|
|
45
|
+
|
|
46
|
+
RESTRICT_ON_SEND = %i[new compile union].freeze
|
|
47
|
+
|
|
48
|
+
# @!method regexp_constructor?(node)
|
|
49
|
+
def_node_matcher :regexp_constructor?, <<~'PATTERN'
|
|
50
|
+
(send (const {nil? cbase} :Regexp) _ ...)
|
|
51
|
+
PATTERN
|
|
52
|
+
|
|
53
|
+
# @!method regexp_escape?(node)
|
|
54
|
+
def_node_matcher :regexp_escape?, <<~'PATTERN'
|
|
55
|
+
(send (const {nil? cbase} :Regexp) {:escape :quote} ...)
|
|
56
|
+
PATTERN
|
|
57
|
+
|
|
58
|
+
# @!method user_input?(node)
|
|
59
|
+
def_node_matcher :user_input?, '(send nil? {:params :request :cookies})'
|
|
60
|
+
|
|
61
|
+
# Regexp.new, Regexp.compile and Regexp.union build a pattern from
|
|
62
|
+
# their arguments, so a non literal argument is always reported.
|
|
63
|
+
def on_send(node)
|
|
64
|
+
return unless regexp_constructor?(node)
|
|
65
|
+
return if node.arguments.empty?
|
|
66
|
+
return if node.arguments.all? { |argument| developer_controlled?(argument) }
|
|
67
|
+
|
|
68
|
+
add_offense(node)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A regexp literal is only reported for the parts it interpolates.
|
|
72
|
+
def on_regexp(node)
|
|
73
|
+
interpolations = node.children.select(&:begin_type?)
|
|
74
|
+
return if interpolations.empty?
|
|
75
|
+
return if interpolations.all? { |interpolation| allowed_interpolation?(interpolation) }
|
|
76
|
+
|
|
77
|
+
add_offense(node)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def allowed_interpolation?(node)
|
|
83
|
+
if style == :all
|
|
84
|
+
developer_controlled?(node)
|
|
85
|
+
else
|
|
86
|
+
developer_controlled?(node) || !reads_user_input?(node)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A literal, a constant, or a pattern that has been escaped. Regexp
|
|
91
|
+
# literals are left to `on_regexp`, which reports the interpolation
|
|
92
|
+
# itself rather than the constructor it is passed to.
|
|
93
|
+
def developer_controlled?(node)
|
|
94
|
+
case node.type
|
|
95
|
+
when :str, :sym, :int, :float, :regexp, :const
|
|
96
|
+
true
|
|
97
|
+
when :dstr, :begin, :array, :splat
|
|
98
|
+
node.children.all? { |child| developer_controlled?(child) }
|
|
99
|
+
when :send
|
|
100
|
+
regexp_escape?(node)
|
|
101
|
+
else
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def reads_user_input?(node)
|
|
107
|
+
user_input?(node) || node.each_descendant(:send).any? { |send_node| user_input?(send_node) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# `ConfigurableEnforcedStyle` raises when the cop has no configuration,
|
|
111
|
+
# which is the common case for an app that only runs `paraxial scan`.
|
|
112
|
+
def style
|
|
113
|
+
cop_config['EnforcedStyle'].to_s == 'all' ? :all : :params
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: paraxial
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Lubas
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rspec
|
|
@@ -80,7 +79,6 @@ dependencies:
|
|
|
80
79
|
- - ">="
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
81
|
version: '0'
|
|
83
|
-
description:
|
|
84
82
|
email:
|
|
85
83
|
- michael@paraxial.io
|
|
86
84
|
executables:
|
|
@@ -101,10 +99,12 @@ files:
|
|
|
101
99
|
- lib/paraxial/helpers.rb
|
|
102
100
|
- lib/paraxial/initializers/marshal_patch.rb
|
|
103
101
|
- lib/paraxial/initializers/startup.rb
|
|
102
|
+
- lib/paraxial/severities.rb
|
|
104
103
|
- lib/paraxial/version.rb
|
|
105
104
|
- lib/rubocop/cop/paraxial/constantize.rb
|
|
106
105
|
- lib/rubocop/cop/paraxial/csrf.rb
|
|
107
106
|
- lib/rubocop/cop/paraxial/csrf_skip.rb
|
|
107
|
+
- lib/rubocop/cop/paraxial/dynamic_regexp.rb
|
|
108
108
|
- lib/rubocop/cop/paraxial/html_safe.rb
|
|
109
109
|
- lib/rubocop/cop/paraxial/raw.rb
|
|
110
110
|
- lib/rubocop/cop/paraxial/send.rb
|
|
@@ -119,7 +119,6 @@ metadata:
|
|
|
119
119
|
homepage_uri: https://paraxial.io/
|
|
120
120
|
source_code_uri: https://paraxial.io/
|
|
121
121
|
changelog_uri: https://paraxial.io/
|
|
122
|
-
post_install_message:
|
|
123
122
|
rdoc_options: []
|
|
124
123
|
require_paths:
|
|
125
124
|
- lib
|
|
@@ -134,8 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
133
|
- !ruby/object:Gem::Version
|
|
135
134
|
version: '0'
|
|
136
135
|
requirements: []
|
|
137
|
-
rubygems_version:
|
|
138
|
-
signing_key:
|
|
136
|
+
rubygems_version: 4.0.21
|
|
139
137
|
specification_version: 4
|
|
140
138
|
summary: Paraxial.io Ruby Agent
|
|
141
139
|
test_files: []
|