woods 1.6.2 → 1.6.4
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/CHANGELOG.md +36 -0
- data/CONTRIBUTING.md +19 -11
- data/README.md +1 -1
- data/exe/woods-console +1 -1
- data/exe/woods-mcp-http +17 -4
- data/lib/woods/cache/cache_middleware.rb +11 -11
- data/lib/woods/console/adapter_family.rb +39 -0
- data/lib/woods/console/credential_index.rb +33 -3
- data/lib/woods/console/credential_scanner.rb +2 -1
- data/lib/woods/console/dispatch_pipeline.rb +22 -4
- data/lib/woods/console/embedded_executor.rb +139 -31
- data/lib/woods/console/model_validator.rb +11 -1
- data/lib/woods/console/rack_middleware.rb +27 -6
- data/lib/woods/console/redactor.rb +46 -14
- data/lib/woods/console/safe_context.rb +56 -8
- data/lib/woods/console/server.rb +2 -1
- data/lib/woods/console/sql_noise_stripper.rb +165 -25
- data/lib/woods/console/sql_output_policy.rb +535 -0
- data/lib/woods/console/sql_table_scanner.rb +163 -52
- data/lib/woods/console/sql_validator.rb +70 -10
- data/lib/woods/console/sqlite_read_guard.rb +46 -0
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/mcp/bearer_auth.rb +29 -10
- data/lib/woods/mcp/http_transport_options.rb +2 -11
- data/lib/woods/mcp/origin_guard.rb +25 -51
- data/lib/woods/mcp/origin_policy.rb +113 -0
- data/lib/woods/railtie.rb +11 -4
- data/lib/woods/version.rb +1 -1
- metadata +9 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec94754e7068662b57f3a9d9b760248189467408bfc98acd9e2485c86547d0d9
|
|
4
|
+
data.tar.gz: 4675c8abe609f22e8829611ede3be68d43e19bb25a72e791203fe6526dfa9a8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '098bf6d0109762c9499908689201d3e4f3b7040621754161696ebc7582d66d2a50fee597102c5248d32d8b1091fbad255d0f4b87dad7f0d72060f56d11c0bf39'
|
|
7
|
+
data.tar.gz: acdd128027e2b0e0f882cd4af56b431151071fb947e3ba9b4f646452e650c20639d21b6b3062a66eaeb81ab1674ee8d975b8de683da36496ff17e5417f0a8813
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.4] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- Enforce configured bearer authentication and allowed origins on every enabled Console HTTP mount before constructing its transport, including manually mounted endpoints.
|
|
15
|
+
- Mask protected collection-valued cells completely and redact overlapping credential matches before rewriting response text.
|
|
16
|
+
- Preserve configured SQL redaction and blocked-table boundaries for supported read queries. Ambiguous protected expressions and unsupported result types refuse clearly. Apply MySQL timeout handling to compatible adapters while retaining the legacy tool API and SQL function policy.
|
|
17
|
+
- Isolate retrieval-context cache entries by retriever instance and rotate only that instance's namespace on reload, preventing shared-backend and stale in-flight cache reuse.
|
|
18
|
+
- Refuse unsupported PostgreSQL escaped identifiers in raw Console SQL before query execution. Ordinary quoted identifiers, literal contents, and comments retain their existing behavior.
|
|
19
|
+
- Share validated Console query projections between execution and typed EAV redaction context, including whitespace normalization and legacy comma-separated selections.
|
|
20
|
+
- Preserve configured Console data protections across database adapters, typed attributes, and SQL result shapes.
|
|
21
|
+
- Refuse SQL relation and CTE column alias lists while output redaction is configured, with an explicit redaction-identity error before execution. Direct unaliased projections remain supported.
|
|
22
|
+
- Apply typed key/value redaction consistently to case-variant and schema-qualified source table names, retaining all matching model types when source schemas are ambiguous.
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
- Clarify supported Console redaction, SQL projection, adapter timeout and HTTP origin behavior, including maintenance-line differences.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Allow Console middleware construction before Woods configuration is initialized, and run each booted Console contract in its own process across the supported Rails CI matrix.
|
|
31
|
+
- Report invalidly encoded HTTP origin settings with bounded startup diagnostics; preserve existing origin defaults and access checks.
|
|
32
|
+
- HTTP startup diagnostics consistently name `WOODS_MCP_HTTP_ALLOWED_ORIGINS` for malformed origins, including under a POSIX (`C`) locale.
|
|
33
|
+
- Point development package links for `1.6.4.alpha` at the `release/1.6.4` branch, matching the maintenance preparation flow. Prepared `1.6.4` links remain unchanged.
|
|
34
|
+
|
|
35
|
+
## [1.6.3] - 2026-09-22
|
|
36
|
+
|
|
37
|
+
### Build
|
|
38
|
+
|
|
39
|
+
- Add the explicit, no-publish preparation cycle for the 1.6.3 security maintenance release; keep version writes task-owned and publication dependent on a reviewed trusted-main SHA pin.
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
|
|
43
|
+
- Apply blocked-table policy to resolved model relations and SQLite table syntax before reads, retain table boundaries across comments, and reject quoted spellings of existing denied SQL functions.
|
|
44
|
+
- Scan the serialized Console response after protected-field redaction so Symbol values and custom JSON output cannot bypass credential scanning.
|
|
45
|
+
|
|
10
46
|
## [1.6.2] - 2026-09-18
|
|
11
47
|
|
|
12
48
|
### Fixed
|
data/CONTRIBUTING.md
CHANGED
|
@@ -128,31 +128,39 @@ By contributing, you agree that your contributions will be licensed under the MI
|
|
|
128
128
|
|
|
129
129
|
## Maintenance release
|
|
130
130
|
|
|
131
|
-
This tree has a one-off, no-publish preparation adapter for the reviewed 1.6.
|
|
132
|
-
security patch while `main` carries the newer 2.
|
|
131
|
+
This tree has a one-off, no-publish preparation adapter for the reviewed 1.6.4
|
|
132
|
+
security patch while `main` carries the newer 2.1 development line. It does not
|
|
133
133
|
establish a permanent stable branch or authorize another maintenance version.
|
|
134
|
-
The approved target is `release/1.6.
|
|
134
|
+
The approved target is `release/1.6.4`, based on immutable `v1.6.3`. Create that
|
|
135
135
|
remote target only after the trusted main maintenance policy is reviewed.
|
|
136
136
|
|
|
137
137
|
From a clean checkout, the only supported transitions are:
|
|
138
138
|
|
|
139
139
|
```sh
|
|
140
|
-
bin/rake "release:reopen[1.6.
|
|
140
|
+
bin/rake "release:reopen[1.6.4.alpha]"
|
|
141
141
|
# Review and commit the generated development-state diff.
|
|
142
|
-
bin/rake "release:prepare[1.6.
|
|
142
|
+
bin/rake "release:prepare[1.6.4]"
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
The tasks never commit, tag, push, dispatch or publish. Never edit VERSION or the
|
|
146
|
-
README maintenance `release-state` banner manually. The
|
|
147
|
-
banner;
|
|
148
|
-
`changelog/<type>_<slug>.md` entries into a dated
|
|
146
|
+
README maintenance `release-state` banner manually. The existing maintenance
|
|
147
|
+
banner must be present; both tasks update it. Prepare also folds classified
|
|
148
|
+
Unreleased notes and optional `changelog/<type>_<slug>.md` entries into a dated
|
|
149
|
+
release heading. Entry files are
|
|
149
150
|
nonempty Markdown without headings; supported types include `fixed`, `security`,
|
|
150
151
|
`build`, and `documentation`. Invalid transitions, dirty trees, malformed notes,
|
|
151
152
|
and missing/duplicate/unknown fences refuse before writes. This legacy profile
|
|
152
153
|
has no v2 migration guide or v2 surface inventory requirement.
|
|
153
154
|
|
|
155
|
+
CI also runs on pushes to the exact `release/1.6.4` branch, so a private-advisory
|
|
156
|
+
merge receives upstream validation before the trusted-main SHA pin. Publication
|
|
157
|
+
still requires a separate green tag-push run. This legacy matrix intentionally
|
|
158
|
+
has no C-locale artifact-reader lane. Its installed maintenance-package and
|
|
159
|
+
backend contracts remain required; do not claim v2 artifact-reader CI coverage
|
|
160
|
+
for this branch.
|
|
161
|
+
|
|
154
162
|
Validate the full suite, lint, booted extraction, real Console credential rotation,
|
|
155
|
-
|
|
163
|
+
installed maintenance package tests, and the real PostgreSQL/MySQL Console policy lane. CI builds one gem plus its SHA-256
|
|
156
164
|
sidecar into `woods-release-<commit SHA>` and tests that artifact on Ruby 3.0/Rails
|
|
157
165
|
6.0 with exactly MCP 0.23.0 and Ruby 4.0/Rails 8.1 with the latest compatible 0.x SDK.
|
|
158
166
|
MCP >=0.23.0 is required for upstream transport security fixes; update Woods and
|
|
@@ -165,12 +173,12 @@ ruby -rrubygems -e 'load Gem.bin_path("rspec-core", "rspec")' -- \
|
|
|
165
173
|
--options /dev/null spec/integration/maintenance_packaged_gem_spec.rb
|
|
166
174
|
```
|
|
167
175
|
|
|
168
|
-
Preparation is not publication; check RubyGems before describing 1.6.
|
|
176
|
+
Preparation is not publication; check RubyGems before describing 1.6.4 as released.
|
|
169
177
|
The legacy automatic tag publisher is disabled, and Bundler's `release`,
|
|
170
178
|
`release:rubygem_push`, and `release:source_control_push` tasks abort. Only a
|
|
171
179
|
maintainer may later tag the reviewed merge commit and dispatch the trusted
|
|
172
180
|
**main** workflow. That workflow must explicitly allow the exact tag, protected
|
|
173
|
-
maintenance branch, immutable 1.6.
|
|
181
|
+
maintenance branch, immutable 1.6.3 base, reviewed final candidate SHA, required
|
|
174
182
|
CI jobs, and immutable artifact. An unpinned candidate remains blocked. First merge a reviewed trusted-main profile update pinning `approved_sha` to that
|
|
175
183
|
exact maintenance merge SHA; only then may the maintainer tag it. The
|
|
176
184
|
workflow publishes the already tested gem bytes; never rebuild or publish locally.
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Woods
|
|
6
6
|
|
|
7
7
|
<!-- release-state:maintenance-banner -->
|
|
8
|
-
> **Woods 1.6.
|
|
8
|
+
> **Woods 1.6.4: prepared maintenance candidate.** This tree remains on the 1.6 maintenance line.
|
|
9
9
|
> Preparation does not establish publication. Check [RubyGems versions](https://rubygems.org/gems/woods/versions)
|
|
10
10
|
> before selecting an install version. See [maintenance release policy](CONTRIBUTING.md#maintenance-release).
|
|
11
11
|
<!-- release-state:end -->
|
data/exe/woods-console
CHANGED
|
@@ -78,7 +78,7 @@ rescue StandardError
|
|
|
78
78
|
next
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
validator = Woods::Console::ModelValidator.new(registry: registry)
|
|
81
|
+
validator = Woods::Console::ModelValidator.new(registry: registry, table_names: model_tables)
|
|
82
82
|
safe_context = Woods::Console::SafeContext.new(connection: ActiveRecord::Base.connection)
|
|
83
83
|
|
|
84
84
|
config = Woods.configuration
|
data/exe/woods-mcp-http
CHANGED
|
@@ -33,10 +33,24 @@ require_relative '../lib/woods/embedding/text_preparer'
|
|
|
33
33
|
require_relative '../lib/woods/embedding/indexer'
|
|
34
34
|
|
|
35
35
|
begin
|
|
36
|
+
raw_origins = ENV.fetch('WOODS_MCP_HTTP_ALLOWED_ORIGINS', '')
|
|
37
|
+
unless raw_origins.valid_encoding?
|
|
38
|
+
invalid_entry = raw_origins.b.split(',').find do |entry|
|
|
39
|
+
!entry.dup.force_encoding(raw_origins.encoding).valid_encoding?
|
|
40
|
+
end
|
|
41
|
+
label = invalid_entry.to_s.b.byteslice(0, 160).inspect
|
|
42
|
+
raise Woods::ConfigurationError, "Invalid WOODS_MCP_HTTP_ALLOWED_ORIGINS entry #{label}: invalid encoding"
|
|
43
|
+
end
|
|
44
|
+
allowed_origins = raw_origins.split(',').map(&:strip).reject(&:empty?)
|
|
45
|
+
begin
|
|
46
|
+
origin_policy = Woods::MCP::OriginPolicy.new(allowed_origins: allowed_origins)
|
|
47
|
+
rescue ArgumentError => e
|
|
48
|
+
raise Woods::ConfigurationError, "#{e.message} (WOODS_MCP_HTTP_ALLOWED_ORIGINS)"
|
|
49
|
+
end
|
|
36
50
|
index_dir = Woods::MCP::Bootstrapper.resolve_index_dir(ARGV)
|
|
37
51
|
retriever, bootstrap_state = Woods::MCP::Bootstrapper.build_retriever(index_dir: index_dir)
|
|
38
52
|
snapshot_store = Woods::MCP::Bootstrapper.build_snapshot_store(index_dir)
|
|
39
|
-
rescue Woods::MCP::BootstrapError => e
|
|
53
|
+
rescue Woods::MCP::BootstrapError, Woods::ConfigurationError => e
|
|
40
54
|
warn "[woods-mcp-http] #{e.class.name.split('::').last}: #{e.message}"
|
|
41
55
|
warn "[woods-mcp-http] details: #{e.details.inspect}" if e.respond_to?(:details) && !e.details.empty?
|
|
42
56
|
exit 2
|
|
@@ -64,15 +78,14 @@ server = Woods::MCP::Server.build(
|
|
|
64
78
|
index_dir: index_dir, retriever: retriever, snapshot_store: snapshot_store,
|
|
65
79
|
bootstrap_state: bootstrap_state, retriever_reloader: retriever_reloader
|
|
66
80
|
)
|
|
67
|
-
allowed_origins = ENV.fetch('WOODS_MCP_HTTP_ALLOWED_ORIGINS', '').split(',').map(&:strip).reject(&:empty?)
|
|
68
81
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
|
|
69
|
-
server, **
|
|
82
|
+
server, **origin_policy.transport_options
|
|
70
83
|
)
|
|
71
84
|
server.transport = transport
|
|
72
85
|
|
|
73
86
|
inner = proc { |env| transport.handle_request(Rack::Request.new(env)) }
|
|
74
87
|
app = token ? Woods::MCP::BearerAuth.new(inner, token: token) : inner
|
|
75
|
-
app = Woods::MCP::OriginGuard.new(app,
|
|
88
|
+
app = Woods::MCP::OriginGuard.new(app, policy: origin_policy)
|
|
76
89
|
|
|
77
90
|
origin_summary = allowed_origins.empty? ? 'loopback' : allowed_origins.join(',')
|
|
78
91
|
auth_mode = token ? 'bearer' : 'none'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'digest'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
require_relative 'cache_store'
|
|
5
6
|
|
|
6
7
|
module Woods
|
|
@@ -403,6 +404,8 @@ module Woods
|
|
|
403
404
|
@retriever = retriever
|
|
404
405
|
@cache_store = cache_store
|
|
405
406
|
@context_ttl = context_ttl
|
|
407
|
+
@context_namespace = SecureRandom.hex(16)
|
|
408
|
+
@context_mutex = Mutex.new
|
|
406
409
|
end
|
|
407
410
|
|
|
408
411
|
# Expose the wrapped stores so the MCP +reload+ tool and
|
|
@@ -414,20 +417,16 @@ module Woods
|
|
|
414
417
|
def metadata_store = @retriever.metadata_store
|
|
415
418
|
def graph_store = @retriever.graph_store
|
|
416
419
|
|
|
417
|
-
#
|
|
418
|
-
# tool after the retriever's stores have been re-hydrated from a fresh
|
|
419
|
-
# embed — otherwise cached results from the old embedding run would
|
|
420
|
-
# linger until their TTL expires and contradict the new stores.
|
|
420
|
+
# Retire this retriever's cached contexts after its corpus reloads.
|
|
421
421
|
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
424
|
-
#
|
|
422
|
+
# Each instance owns an unpredictable namespace. Rotation prevents an
|
|
423
|
+
# in-flight request from repopulating the active cache with old results;
|
|
424
|
+
# retired entries expire by their TTL. Other retrievers and embedding
|
|
425
|
+
# caches remain independent, without any global backend deletion.
|
|
425
426
|
#
|
|
426
427
|
# @return [void]
|
|
427
428
|
def invalidate_context_cache!
|
|
428
|
-
@
|
|
429
|
-
rescue StandardError => e
|
|
430
|
-
warn("[Woods] CachedRetriever context-cache invalidation failed: #{e.message}")
|
|
429
|
+
@context_mutex.synchronize { @context_namespace = SecureRandom.hex(16) }
|
|
431
430
|
end
|
|
432
431
|
|
|
433
432
|
# Execute the retrieval pipeline with context-level caching.
|
|
@@ -474,7 +473,8 @@ module Woods
|
|
|
474
473
|
# @param exclude_types [Array<String, Symbol>, nil]
|
|
475
474
|
# @return [String]
|
|
476
475
|
def context_key(query, budget, types: nil, exclude_types: nil)
|
|
477
|
-
|
|
476
|
+
namespace = @context_mutex.synchronize { @context_namespace }
|
|
477
|
+
Cache.cache_key(:context, namespace, query, budget.to_s, fingerprint(types), fingerprint(exclude_types))
|
|
478
478
|
end
|
|
479
479
|
|
|
480
480
|
def fingerprint(types)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Woods
|
|
4
|
+
module Console
|
|
5
|
+
# Adapter names differ from the SQL grammar and session settings they use.
|
|
6
|
+
module AdapterFamily
|
|
7
|
+
# Classify a live connection by adapter ancestry, database configuration,
|
|
8
|
+
# then its display name. Unknown families remain nil: SQL callers must
|
|
9
|
+
# refuse rather than silently omit dialect-specific safeguards.
|
|
10
|
+
# @param connection [Object] Active Record connection or compatible adapter
|
|
11
|
+
# @return [Symbol, nil] :postgres, :mysql, :sqlite, or unknown
|
|
12
|
+
def self.for(connection)
|
|
13
|
+
ancestry = connection.class.ancestors.filter_map(&:name)
|
|
14
|
+
return :postgres if ancestry.include?('ActiveRecord::ConnectionAdapters::PostgreSQLAdapter')
|
|
15
|
+
return :mysql if ancestry.any? { |name| name.match?(/::(?:AbstractMysql|Mysql2|Trilogy)Adapter\z/) }
|
|
16
|
+
return :sqlite if ancestry.include?('ActiveRecord::ConnectionAdapters::SQLite3Adapter')
|
|
17
|
+
|
|
18
|
+
from_name(configured_adapter(connection)) || from_name(connection.adapter_name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.configured_adapter(connection)
|
|
22
|
+
return unless connection.respond_to?(:pool) && connection.pool.respond_to?(:db_config)
|
|
23
|
+
|
|
24
|
+
connection.pool.db_config.adapter
|
|
25
|
+
end
|
|
26
|
+
private_class_method :configured_adapter
|
|
27
|
+
|
|
28
|
+
def self.from_name(value)
|
|
29
|
+
name = value.to_s.downcase
|
|
30
|
+
return :mysql if name.include?('mysql') || %w[trilogy mariadb].include?(name)
|
|
31
|
+
return :postgres if name.include?('postgre') || %w[postgis cockroachdb redshift].include?(name)
|
|
32
|
+
return :sqlite if name.include?('sqlite')
|
|
33
|
+
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
private_class_method :from_name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -46,7 +46,7 @@ module Woods
|
|
|
46
46
|
# index.redact("token: sk_live_actual_secret_value")
|
|
47
47
|
# # => "token: [REDACTED:credential]"
|
|
48
48
|
#
|
|
49
|
-
class CredentialIndex
|
|
49
|
+
class CredentialIndex # rubocop:disable Metrics/ClassLength
|
|
50
50
|
# Captured at require time so the mtime-check warning has a stable
|
|
51
51
|
# reference point even if the clock skews later. Frozen immediately
|
|
52
52
|
# to prevent accidental mutation.
|
|
@@ -187,7 +187,10 @@ module Woods
|
|
|
187
187
|
def initialize(secrets:)
|
|
188
188
|
filtered = Array(secrets).select { |s| s.is_a?(String) && s.length >= MIN_LENGTH }
|
|
189
189
|
@secrets = filtered.to_set.freeze
|
|
190
|
-
|
|
190
|
+
# Regexp alternatives match in order: a shorter prefix must not consume
|
|
191
|
+
# only the start of a longer indexed credential and expose its suffix.
|
|
192
|
+
longest_first = @secrets.each_with_index.sort_by { |secret, idx| [-secret.length, idx] }.map(&:first)
|
|
193
|
+
@pattern = @secrets.empty? ? nil : Regexp.union(longest_first)
|
|
191
194
|
end
|
|
192
195
|
|
|
193
196
|
# @return [Boolean] true when no secrets were collected (missing key,
|
|
@@ -212,7 +215,34 @@ module Woods
|
|
|
212
215
|
def redact(str)
|
|
213
216
|
return str if empty? || !str.is_a?(String) || !@pattern.match?(str)
|
|
214
217
|
|
|
215
|
-
|
|
218
|
+
offset = 0
|
|
219
|
+
parts = []
|
|
220
|
+
redaction_spans(str).each do |start, finish|
|
|
221
|
+
parts << str[offset...start] << REDACTED
|
|
222
|
+
offset = finish
|
|
223
|
+
end
|
|
224
|
+
parts << str[offset..]
|
|
225
|
+
parts.join
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
# Search from each match's start, rather than its end, so an overlapping
|
|
231
|
+
# credential cannot expose its suffix after an earlier replacement.
|
|
232
|
+
# Union the covered spans before changing the original string.
|
|
233
|
+
def redaction_spans(str)
|
|
234
|
+
spans = []
|
|
235
|
+
offset = 0
|
|
236
|
+
while (match = @pattern.match(str, offset))
|
|
237
|
+
start, finish = match.offset(0)
|
|
238
|
+
if spans.last && start < spans.last[1]
|
|
239
|
+
spans.last[1] = [spans.last[1], finish].max
|
|
240
|
+
else
|
|
241
|
+
spans << [start, finish]
|
|
242
|
+
end
|
|
243
|
+
offset = start + 1
|
|
244
|
+
end
|
|
245
|
+
spans
|
|
216
246
|
end
|
|
217
247
|
end
|
|
218
248
|
end
|
|
@@ -145,7 +145,7 @@ module Woods
|
|
|
145
145
|
|
|
146
146
|
# Scan a value (String, Hash, Array, or any other object) for credentials.
|
|
147
147
|
#
|
|
148
|
-
# Strings are
|
|
148
|
+
# Strings and Symbols are scanned against every active pattern. Hash values and Array
|
|
149
149
|
# elements are walked recursively; keys and non-string scalars
|
|
150
150
|
# (Integer, Float, true/false, nil) pass through untouched.
|
|
151
151
|
#
|
|
@@ -164,6 +164,7 @@ module Woods
|
|
|
164
164
|
def walk(value, counts, index)
|
|
165
165
|
case value
|
|
166
166
|
when String then scan_string(value, counts, index)
|
|
167
|
+
when Symbol then scan_string(value.to_s, counts, index).to_sym
|
|
167
168
|
when Hash then walk_hash(value, counts, index)
|
|
168
169
|
when Array then value.map { |item| walk(item, counts, index) }
|
|
169
170
|
else value
|
|
@@ -77,15 +77,33 @@ module Woods
|
|
|
77
77
|
response = @conn_mgr.send_request(request)
|
|
78
78
|
return error_from_response(response, request) unless response['ok']
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
result = scan_for_credentials(result, request)
|
|
82
|
-
text = @renderer ? @renderer.render_default(result) : JSON.pretty_generate(result)
|
|
83
|
-
success_response(text)
|
|
80
|
+
render_response(response['result'], request)
|
|
84
81
|
rescue ConnectionError => e
|
|
85
82
|
scanned = scan_for_credentials("Connection error: #{e.message}", request)
|
|
86
83
|
error_response(scanned)
|
|
87
84
|
end
|
|
88
85
|
|
|
86
|
+
def render_response(result, request)
|
|
87
|
+
# Remove protected values before custom serializers run, then scan
|
|
88
|
+
# the materialized representation consumed by both response renderers.
|
|
89
|
+
result = @ctx.redact(result)
|
|
90
|
+
result = JSON.parse(JSON.generate(result))
|
|
91
|
+
result = @ctx.redact(result)
|
|
92
|
+
result = scan_for_credentials(result, request)
|
|
93
|
+
text = @renderer ? @renderer.render_default(result) : JSON.pretty_generate(result)
|
|
94
|
+
success_response(text)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
# SDK 0.x includes uncaught exception messages in its tool errors.
|
|
97
|
+
# Serializer/parser/renderer messages may contain unscanned values.
|
|
98
|
+
rendering_error_response(request)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def rendering_error_response(request)
|
|
102
|
+
error_response(scan_for_credentials('Console response could not be serialized safely.', request))
|
|
103
|
+
rescue StandardError
|
|
104
|
+
error_response('[REDACTION_FAILED]')
|
|
105
|
+
end
|
|
106
|
+
|
|
89
107
|
def error_from_response(response, request)
|
|
90
108
|
error_text = "#{response['error_type']}: #{response['error']}"
|
|
91
109
|
error_text = scan_for_credentials(error_text, request)
|