ez_logs_agent 0.1.4 → 0.1.6

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: 3edfe639cbf53d13c7b902ae2795687878c6898affaac8cd1b2d4610d9e357ae
4
- data.tar.gz: ceef7660ed54a8693e8090d03acae9bdc524b4c6993c810f2fda5507a0e691e3
3
+ metadata.gz: 452e5fb397336daeaccc2f5738be03545e5bd318243f30755d5e5205d1c9d2f6
4
+ data.tar.gz: 22b04cd9a432fef3006ffb33c4322c188e0d9bb4914fb07487f1a135ada3877c
5
5
  SHA512:
6
- metadata.gz: db1c93abdea22ac379936498fee9c9d873f676741e704267898890ad83324dd0f2b26cc0e0c12a0713f6e8ed4b1e0f7535baab8650b17b088c666a45947a18b8
7
- data.tar.gz: 50d06b7f6035e01032f4d6066ed9477304851d7273ad25c352719c66be5e6f1ba0f641aad8e8710df754f4bed6b8e205524ae424037af16756af7f9f8297778b
6
+ metadata.gz: 345f3c6929cf6b88bb6d909c6ada301907ccb2aee852957f8da0b278852033978d5116f6ecd67358b33c50921c4fd82e0c65d2cfa1ebb7304d4b9387f8138617
7
+ data.tar.gz: b412f7a99fbc624541062f0fc8ab148576f48350fc0c72636938360d7cc6c17974382b6bcb47800224e315666bd91f992fdb6b4d4218f54efb16c06b2140f08a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.6] — 2026-05-24
6
+
7
+ ### Added
8
+ - Optional `actor.principal` sub-field (`{ id:, label? }`) on the wire,
9
+ carrying the human a `kind: "agent"` or `kind: "hybrid"` actor is
10
+ acting on behalf of. Drops silently if malformed; existing callers
11
+ unaffected. Lets the server narrate agent actions as
12
+ "Claude updated employee, on behalf of Razvan" instead of
13
+ attributing the change to either party alone.
14
+
15
+ ### Internal
16
+ - Dropped a stray gemspec bump to `sidekiq ~> 8.1` that landed via an
17
+ auto-merged dependabot PR without re-resolving `Gemfile.lock`. Dev
18
+ deps are back in sync with the lockfile (`rails ~> 7.0`,
19
+ `sqlite3 ~> 1.6`, `sidekiq ~> 7.0`); no runtime change for customers.
20
+
21
+ ## [0.1.5] — 2026-05-17 — security release
22
+
23
+ ### Security
24
+ - `DatabaseCapturer` no longer captures columns the host app declared
25
+ `encrypts :foo` on. Rails 7+ decrypts attributes in memory before
26
+ `saved_changes` fires, so without this guard the plaintext of every
27
+ encrypted column was landing on the wire and in the EZLogs UI on
28
+ every create / update. The new policy is declarative: at capture
29
+ time we read `record.class.encrypted_attributes` (Rails 7+) and drop
30
+ every name in that set, regardless of column name. If the host app
31
+ encrypted it, we never capture it. Upgrade is strongly recommended
32
+ for any deployment whose models use `encrypts`. Customers running
33
+ 0.1.4 or earlier should also scrub historical events for the
34
+ affected column names — the data leaked in the past will stay in
35
+ the event store until masked.
36
+ - `SENSITIVE_PATTERNS` (the secondary name-based denylist) now also
37
+ matches `private_key`, `public_key`, `signing_key`, `pem`, `cipher`,
38
+ `nonce`, `salt`, `digest`, `signature`, `hmac`. Belt-and-suspenders
39
+ for columns that carry sensitive material but weren't declared
40
+ `encrypts` (legacy code, manual hashing, externally-generated
41
+ material).
42
+
5
43
  ## [0.1.4] — 2026-05-17
6
44
 
7
45
  ### Fixed
@@ -5,9 +5,15 @@ module EzLogsAgent
5
5
  #
6
6
  # Actor schema:
7
7
  # {
8
- # id: String, # REQUIRED, stable identifier
9
- # label: String | nil, # optional, human-readable display
10
- # kind: String | nil # optional, one of human|agent|system|hybrid
8
+ # id: String, # REQUIRED, stable identifier
9
+ # label: String | nil, # optional, human-readable display
10
+ # kind: String | nil, # optional, one of human|agent|system|hybrid
11
+ # principal: { id:, label? } | nil # optional, only meaningful when
12
+ # # kind == "hybrid": the human who
13
+ # # authorized the agent. Drives the
14
+ # # "Claude updated employee, on
15
+ # # behalf of Razvan" framing on the
16
+ # # server's AiExplainer.
11
17
  # }
12
18
  #
13
19
  # This module ensures actors conform to the expected structure
@@ -46,13 +52,36 @@ module EzLogsAgent
46
52
  id = actor[:id] || actor["id"]
47
53
  label = actor[:label] || actor["label"]
48
54
  kind = actor[:kind] || actor["kind"]
55
+ principal = actor[:principal] || actor["principal"]
49
56
 
50
57
  result = { id: id.to_s }
51
58
  result[:label] = label.to_s if label
52
59
  result[:kind] = kind.to_s if kind && VALID_KINDS.include?(kind.to_s)
60
+ sanitized_principal = sanitize_principal(principal)
61
+ result[:principal] = sanitized_principal if sanitized_principal
53
62
 
54
63
  result
55
64
  end
65
+
66
+ private
67
+
68
+ # Sanitize the optional principal sub-structure. Reuses the same
69
+ # "id required, label optional" shape as the top-level actor — but
70
+ # principals are always human, so no `kind` field. Returns nil if
71
+ # the principal is missing or malformed (we drop silently rather
72
+ # than rejecting the whole actor; principal is purely advisory).
73
+ def sanitize_principal(principal)
74
+ return nil if principal.nil?
75
+ return nil unless principal.is_a?(Hash)
76
+
77
+ id = principal[:id] || principal["id"]
78
+ return nil if id.nil? || id.to_s.empty?
79
+
80
+ result = { id: id.to_s }
81
+ label = principal[:label] || principal["label"]
82
+ result[:label] = label.to_s if label
83
+ result
84
+ end
56
85
  end
57
86
  end
58
87
  end
@@ -60,7 +60,16 @@ module EzLogsAgent
60
60
  # Previously we filtered them out, but this loses important context.
61
61
  # FOREIGN_KEY_PATTERN = /_id\z/ # Removed January 2026
62
62
 
63
- # Patterns for sensitive data to ignore
63
+ # Patterns for sensitive data to ignore.
64
+ #
65
+ # The first source of truth is `record.class.encrypted_attributes`
66
+ # (Rails 7+ `encrypts :foo` declaration) — see encrypted_attribute?.
67
+ # If the host app encrypted it, we never capture it.
68
+ #
69
+ # This list is the secondary defense: column names that frequently
70
+ # carry sensitive material even when the host app didn't declare
71
+ # `encrypts` (legacy code, manual hashing, externally-generated
72
+ # material). Matching is substring + case-insensitive.
64
73
  SENSITIVE_PATTERNS = %w[
65
74
  password
66
75
  token
@@ -70,6 +79,16 @@ module EzLogsAgent
70
79
  ssn
71
80
  social_security
72
81
  encrypted
82
+ private_key
83
+ public_key
84
+ signing_key
85
+ pem
86
+ cipher
87
+ nonce
88
+ salt
89
+ digest
90
+ signature
91
+ hmac
73
92
  ].freeze
74
93
 
75
94
 
@@ -276,8 +295,9 @@ module EzLogsAgent
276
295
  changes = model.saved_changes
277
296
  return nil if changes.nil? || changes.empty?
278
297
 
279
- # Find meaningful changes
280
- meaningful_changes = filter_meaningful_changes(changes)
298
+ # Find meaningful changes (excludes encrypted columns + sensitive
299
+ # name patterns — see meaningful_attribute? / encrypted_attribute?)
300
+ meaningful_changes = filter_meaningful_changes(changes, model)
281
301
  return nil if meaningful_changes.empty?
282
302
 
283
303
  # Build context with all meaningful changes
@@ -305,7 +325,7 @@ module EzLogsAgent
305
325
 
306
326
  # Filter to meaningful, non-nil scalar attributes
307
327
  meaningful_attrs = attributes.select do |attribute, value|
308
- meaningful_attribute?(attribute) &&
328
+ meaningful_attribute?(attribute, model) &&
309
329
  scalar?(value) &&
310
330
  !value.nil?
311
331
  end
@@ -324,20 +344,27 @@ module EzLogsAgent
324
344
  # Filters changes to only meaningful business attributes
325
345
  #
326
346
  # @param changes [Hash] The saved_changes hash
347
+ # @param model [ActiveRecord::Base] The model instance (used to
348
+ # consult `record.class.encrypted_attributes` so columns declared
349
+ # `encrypts :foo` are never captured, regardless of their name).
327
350
  # @return [Array<Array>] Array of [attribute, [from, to]] pairs
328
- def filter_meaningful_changes(changes)
351
+ def filter_meaningful_changes(changes, model)
329
352
  changes.select do |attribute, (from, to)|
330
- meaningful_attribute?(attribute) &&
353
+ meaningful_attribute?(attribute, model) &&
331
354
  scalar_values?(from, to) &&
332
355
  values_actually_changed?(from, to)
333
356
  end.to_a
334
357
  end
335
358
 
336
- # Checks if an attribute is meaningful (not technical/ignored)
359
+ # Checks if an attribute is meaningful (not technical/ignored).
337
360
  #
338
361
  # @param attribute [String] The attribute name
362
+ # @param model [ActiveRecord::Base, nil] The model instance — when
363
+ # supplied, columns declared `encrypts :foo` on the model class
364
+ # are dropped regardless of name. Authoritative drop: if the host
365
+ # app encrypted the column, we never capture it.
339
366
  # @return [Boolean]
340
- def meaningful_attribute?(attribute)
367
+ def meaningful_attribute?(attribute, model = nil)
341
368
  attr_str = attribute.to_s
342
369
 
343
370
  # Skip explicitly ignored attributes
@@ -347,13 +374,41 @@ module EzLogsAgent
347
374
  # relationship changes (e.g., assigned_to_id changing from user A to user B)
348
375
  # Previously filtered via FOREIGN_KEY_PATTERN - removed January 2026
349
376
 
350
- # Skip sensitive data
377
+ # Authoritative: drop anything the host app declared `encrypts` on.
378
+ # Rails decrypts at the attribute layer before saved_changes fires,
379
+ # so without this check the plaintext would land on the wire.
380
+ return false if model && encrypted_attribute?(attr_str, model)
381
+
382
+ # Skip name-pattern-sensitive data (legacy / non-encrypts paths).
351
383
  return false if sensitive_attribute?(attr_str)
352
384
 
353
385
  true
354
386
  end
355
387
 
356
- # Checks if attribute name contains sensitive patterns
388
+ # Checks whether the host app declared `encrypts :<attribute>` on
389
+ # this model's class. Available since Rails 7.0 via
390
+ # ActiveRecord::Encryption::EncryptableRecord#encrypted_attributes.
391
+ #
392
+ # Safe across host Rails versions: returns false if the API isn't
393
+ # present (older Rails, non-AR records).
394
+ #
395
+ # @param attribute [String] The attribute name (already to_s'd)
396
+ # @param model [ActiveRecord::Base] The model instance
397
+ # @return [Boolean]
398
+ def encrypted_attribute?(attribute, model)
399
+ klass = model.class
400
+ return false unless klass.respond_to?(:encrypted_attributes)
401
+
402
+ encrypted = klass.encrypted_attributes
403
+ return false if encrypted.nil? || encrypted.empty?
404
+
405
+ encrypted.map(&:to_s).include?(attribute)
406
+ rescue StandardError
407
+ false
408
+ end
409
+
410
+ # Checks if attribute name contains sensitive patterns.
411
+ # Secondary check — see SENSITIVE_PATTERNS comment.
357
412
  #
358
413
  # @param attribute [String] The attribute name
359
414
  # @return [Boolean]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EzLogsAgent
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ez_logs_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - dezsirazvan
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-05-16 00:00:00.000000000 Z
10
+ date: 2026-05-28 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: request_store