paraxial 1.4.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3206160ed62f0ae982fa82322d02fb097be5261b245e889dc0e3252f1c60006
4
- data.tar.gz: 2b2ba464bbfc69f37d680fe257cfa705827f222f126aab01eb0f5a49b9022162
3
+ metadata.gz: f24195885218479f2c0b5f6c8963d3ecacdcfa8e59e60bcb731a75750a63c891
4
+ data.tar.gz: e178051703824267b31bd7945e060272c6eada4029a03a5f35294dad5e68848b
5
5
  SHA512:
6
- metadata.gz: 873b0f6c3d9347e02bac6aa040ed2343d2fec5ddb04eb1952a9fa9e4e94028dd8f4befa446bddc0dfba488c71a9db9a8d170f0510b6d91b24440168c144026f0
7
- data.tar.gz: c14072cc7a5f02e6e6dfd1c8da1eb36fcc280d5aa5c65352ef12ff645762a93cdc6592ade346924a137233a91cae87fa43e0aac1abbddda49ffeb2ec2cbba002
6
+ metadata.gz: db01f4a55e4ae5425d2e3493bc7a374d2922a0f3aa54ac63a7d4b783bd01ad698b5d2818eff2429355484fdba2195410f325b0c8b67a6fb87e5a58175a9b145e
7
+ data.tar.gz: 7482ae35903800d6c5b2c5a54b5ff8223b4d311117d41375914eb5edad35d6b4e548777395fceac31479739ad4479c0c788000ceb286f57645d53c8a4fa7393d
@@ -13,6 +13,7 @@ module Paraxial
13
13
  get_abr
14
14
  sleep(10)
15
15
  flush_buffer
16
+ break if Paraxial::FreeTier.is_free_tier
16
17
  end
17
18
  end
18
19
  end
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')
@@ -7,7 +7,7 @@ module Paraxial
7
7
  attr_reader :is_free_tier
8
8
 
9
9
  def initialize
10
- @is_free_tier = true
10
+ @is_free_tier = false
11
11
  check_free_tier_status
12
12
  end
13
13
 
@@ -22,7 +22,17 @@ module Paraxial
22
22
  r = Net::HTTP.post(uri, body.to_json, headers)
23
23
  result = JSON.parse(r.body)
24
24
  # Assume the API response contains a field `free_tier` (true/false)
25
- @is_free_tier = result['free_tier']
25
+ # Response Body: {"error":"api_key not valid"}
26
+ if result['free_tier'] == true
27
+ puts "[Paraxial] Free tier site, some functionality is disabled"
28
+ @is_free_tier = true
29
+ elsif result['free_tier'] == false
30
+ puts "[Paraxial] Account active, all features available"
31
+ @is_free_tier = false
32
+ else
33
+ puts "[Paraxial] Account check failed, some functionality is disabled"
34
+ @is_free_tier = true
35
+ end
26
36
  rescue StandardError => e
27
37
  # Handle any errors (network issues, parsing errors, etc.)
28
38
  puts "Error fetching free tier status: #{e.message}"
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Paraxial
4
- VERSION = '1.4.4'
4
+ VERSION = '1.5.0'
5
5
  end
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.4
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: 2025-01-17 00:00:00.000000000 Z
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: 3.5.23
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: []