Top Level Namespace
Defined Under Namespace
Modules: Enumerable, GeneratorHelper, GeneratorMessages, ReactOnRails, ReactOnRailsHelper
Classes: Array, HelloWorldController, HomeController
Constant Summary
collapse
- AUTH_NAME =
Recognizing a callback as authentication only silences the advisory, so this stays deliberately
conservative: an over-broad match turns a false positive into a false negative, which is the more
dangerous direction for a security guardrail.
A trailing !/? is the discriminating signal for the open-ended forms. Devise and friends
generate bang callbacks per scope (authenticate_admin_user!, authenticate_api_user!,
authenticate_customer!), and app-defined auth callbacks follow the same convention, so any
authenticate*/authorize* name that ends in a bang counts. Without a bang the name must come
from the narrow allowlist, which keeps unrelated callbacks such as authenticate_analytics_session
and authorize_for_metrics from reading as authentication.
/
(?:
authenticate(?:_\w+)?[!?] |
authorize(?:_\w+)?[!?] |
authenticate(?:_(?:user|account|admin|member|session))? |
authorize(?:_(?:request|access|user))? |
require_(?:login|user|authentication|authorization) |
verify_authenticat(?:ed|ion)
)[!?]?
/x
- AUTH_CALLBACK_NAME =
"(?:prepend_|append_)?before_action"
%i[on_comment on_embdoc_beg on_embdoc on_embdoc_end].freeze
- STRING_BODY_EVENTS =
String, heredoc, and regexp bodies are data, not executable code. Ripper reports all of them as
:on_tstring_content, so they must be neutralized alongside comments — otherwise a
skip_before_action written inside a heredoc reads as a real callback and produces a false warning.
%i[on_tstring_content].freeze
- SCOPE_SYNTAX =
/(?:%[iw](?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})|\[[^\]]*\]|:[A-Za-z_]\w*[!?]?|["'][^"']+["'])/
- ONLY_SCOPE =
/\A,\s*only:\s*(#{SCOPE_SYNTAX})\z/o
- EXCEPT_SCOPE =
/\A,\s*except:\s*(#{SCOPE_SYNTAX})\z/o
Instance Method Summary
collapse
Instance Method Details
#applies_to_payload?(rest, unknown:) ⇒ Boolean
70
71
72
73
74
75
76
77
78
79
80
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 70
def applies_to_payload?(rest, unknown:)
return true if rest.empty?
if (scope = rest.match(ONLY_SCOPE))
scope[1].match?(/\brsc_payload\b/)
elsif (scope = rest.match(EXCEPT_SCOPE))
!scope[1].match?(/\brsc_payload\b/)
else
unknown
end
end
|
#authenticated_callback_names(lines) ⇒ Object
#authentication_evidence?(lines) ⇒ Boolean
151
152
153
154
155
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 151
def authentication_evidence?(lines)
authenticated_callbacks = authenticated_callback_names(lines)
remove_skipped_callbacks(lines, authenticated_callbacks)
authenticated_callbacks.any?
end
|
#blank_line_starting_text(text, column) ⇒ Object
Blanks only the parts of a string body that begin a source line, because those are the parts that
can masquerade as a statement to the line-oriented callback scanner. Content that starts mid-line
(only: "rsc_payload") is preserved so callback scope parsing keeps working.
113
114
115
116
117
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 113
def blank_line_starting_text(text, column)
text.split("\n", -1).each_with_index.map do |part, index|
index.zero? && column.positive? ? part : blank_text(part)
end.join("\n")
end
|
#blank_text(text) ⇒ Object
106
107
108
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 106
def blank_text(text)
text.gsub(/[^\n]/, " ")
end
|
#code_lines(content) ⇒ Object
Executable lines only. Falls back to a crude comment strip when the file does not lex (so an
unparseable routes file still gets its advisory rather than silently passing).
138
139
140
141
142
143
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 138
def code_lines(content)
tokens = ruby_tokens(content)
return scannable_ruby_lines(tokens) if tokens.any?
content.lines.map { |line| line.sub(/#.*/, "") }
end
|
#heading ⇒ Object
23
24
25
|
# File 'lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb', line 23
def heading
page.first(:css, "h1")
end
|
157
158
159
160
161
162
163
164
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 157
def input_file
input = $stdin.tty? ? "" : $stdin.read
parsed_input = JSON.parse(input)
parsed_path = parsed_input.is_a?(Hash) ? parsed_input.dig("tool_input", "file_path").to_s : ""
parsed_path.empty? ? ARGV.first.to_s : parsed_path
rescue JSON::ParserError
ARGV.first.to_s
end
|
#logical_statements(lines, callback_name) ⇒ Object
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 43
def logical_statements(lines, callback_name)
lines.each_with_index.filter_map do |line, index|
next unless line.match?(/^\s*#{callback_name}\b/)
statement = line.dup
while index + 1 < lines.length &&
(statement.rstrip.end_with?(",", "\\") ||
statement.count("(") > statement.count(")") ||
statement.count("[") > statement.count("]"))
index += 1
statement << lines[index]
end
statement.gsub(/\s+/, " ").strip
end
end
|
#message ⇒ Object
19
20
21
|
# File 'lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb', line 19
def message
page.first(:css, "h3")
end
|
15
16
17
|
# File 'lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb', line 15
def name_input
page.first("input")
end
|
#parse_callback(statement, callback_name) ⇒ Object
59
60
61
62
63
64
65
66
67
68
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 59
def parse_callback(statement, callback_name)
callback = statement.match(
/\A#{callback_name}\s*(?<parenthesized>\()?\s*:?(?<auth>#{AUTH_NAME})(?=\s*(?:,|\)|\z))(?<rest>.*)\z/
)
return unless callback
rest = callback[:rest].strip
rest = rest.delete_suffix(")").strip if callback[:parenthesized]
[callback[:auth], rest]
end
|
#read_file(path) ⇒ Object
179
180
181
182
183
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 179
def read_file(path)
File.read(path)
rescue SystemCallError
nil
end
|
#remove_skipped_callbacks(lines, authenticated_callbacks) ⇒ Object
89
90
91
92
93
94
95
96
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 89
def remove_skipped_callbacks(lines, authenticated_callbacks)
logical_statements(lines, "skip_before_action").each do |statement|
parsed = parse_callback(statement, "skip_before_action")
next unless parsed && authenticated_callbacks.include?(parsed.first)
authenticated_callbacks.delete(parsed.first) if applies_to_payload?(parsed.last, unknown: true)
end
end
|
#renderer_evidence?(tokens) ⇒ Boolean
145
146
147
148
149
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 145
def renderer_evidence?(tokens)
tokens.any? do |_position, event, token, _state|
(event == :on_const && token == "RSCPayloadRenderer") || (event == :on_ident && token == "rsc_payload")
end
end
|
#ruby_tokens(content) ⇒ Object
98
99
100
101
102
103
104
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 98
def ruby_tokens(content)
return [] unless content.valid_encoding?
Ripper.lex(content)
rescue ArgumentError
[]
end
|
#scannable_ruby_lines(tokens) ⇒ Object
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 119
def scannable_ruby_lines(tokens)
heredoc_depth = 0
tokens.each_with_object(+"") do |(position, event, token, _state), scannable|
heredoc_depth += 1 if event == :on_heredoc_beg
heredoc_depth -= 1 if event == :on_heredoc_end
scannable << if COMMENT_EVENTS.include?(event)
blank_text(token)
elsif STRING_BODY_EVENTS.include?(event)
heredoc_depth.positive? ? blank_text(token) : blank_line_starting_text(token, position[1])
else
token
end
end.lines
end
|
#warning_context(relative_path, detail) ⇒ Object
166
167
168
169
170
171
172
173
174
175
176
177
|
# File 'lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb', line 166
def warning_context(relative_path, detail)
[
"⚠️ rsc-app-safety: #{relative_path}",
detail,
"The React on Rails Pro RSC payload route renders any registered server component with",
"caller-supplied props and has NO built-in authentication. Confirm this endpoint is behind your",
"app's auth via ReactOnRailsPro.configure { |config| config.rsc_payload_authorizer = ... } or",
"an authenticated, app-owned controller's before_action, and",
"that server components derive identity from the session, not props. Read the rsc-app-safety",
"skill before shipping."
].join("\n")
end
|