lex-audit 0.1.5 → 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: c1d91d00dfbeb30d1b21aabe61074afaed15f82b829350a49b8e5113a04ee2d7
4
- data.tar.gz: 176612358acf9c4e66a78c409f7de7ffc9380535926577b33d04634ebc470675
3
+ metadata.gz: 531cdf796a189cb155d128d61af3fd72128d859d780ca21cfe40f50336f08ce4
4
+ data.tar.gz: 9bfbf2fa44d6ccb8c864f5b348b0d8bbc9f5f237dafefb65e12752111a077f54
5
5
  SHA512:
6
- metadata.gz: ca20f1ce1286618f6febe524808842a7fdfaf03986173e122cd244e21ab4ec79fd6ee1df43a764da0fdd44ce06ea99ee22f74eec7af495d318f4b43d014c9aca
7
- data.tar.gz: 197de24600963db0102083ad781773347c8d600df67c3d70be8fb77da228cdd689c4bdc5f69c80e16d01d045c45328b72c54907a9381d88132a6afca1ded948d
6
+ metadata.gz: 4b760a57e8ecd4462d38541552e486034b0fa0c39320a078a38e1d60cea940cc9ae3f5214dc661ba169a5b412ef0f8ad64fb044ef939bc48794aded44a548df1
7
+ data.tar.gz: 1ed526ca7d9fad3b2ff541e0fc791ecb50bfc3ca37452339ac699b730065dbb091ff9b19a6ce11c5dd85cb9b8e2e721219758dce2140988476c75653098e7d32
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.6] - 2026-03-31
6
+
7
+ ### Added
8
+ - `Helpers::VerifiedWrite` module with `verified_write` and `verified_edit` methods
9
+ - Post-write SHA-256 verification catches silent write failures (disk full, permission, NFS stale)
10
+ - Stale edit detection catches external file modifications between read and write
11
+ - `WriteVerificationError` and `StaleEditError` error classes in `errors.rb`
12
+ - Audit trail recording via `Legion::Data::Model::AuditLog` when legion-data is available (best-effort, never breaks the write)
13
+ - Shared `spec/support/audit_log_db.rb` eliminates DB constant conflicts when all specs run together
14
+
5
15
  ## [0.1.5] - 2026-03-30
6
16
 
7
17
  ### Changed
data/lex-audit.gemspec CHANGED
@@ -36,7 +36,6 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency 'legion-settings', '>= 1.3.14'
37
37
  spec.add_dependency 'legion-transport', '>= 1.3.9'
38
38
 
39
- spec.add_development_dependency 'rake'
40
39
  spec.add_development_dependency 'rspec'
41
40
  spec.add_development_dependency 'rubocop'
42
41
  spec.add_development_dependency 'rubocop-rspec'
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Audit
6
+ # Raised when post-write SHA-256 verification fails.
7
+ class WriteVerificationError < StandardError; end
8
+
9
+ # Raised when the file on disk has been modified since the caller read it.
10
+ class StaleEditError < StandardError; end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'legion/extensions/audit/errors'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Audit
9
+ module Helpers
10
+ # Combines file write/edit operations with post-write SHA-256 verification
11
+ # and optional audit trail recording via AuditRecord.
12
+ #
13
+ # Include this module in any class or extension that modifies files and
14
+ # needs tamper-evident confirmation that the write succeeded.
15
+ module VerifiedWrite
16
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
17
+
18
+ # Write +content+ to +path+, then re-read and compare SHA-256 digests.
19
+ #
20
+ # @param path [String] absolute or relative filesystem path
21
+ # @param content [String] content to write
22
+ # @param agent_id [String, nil] identity recorded in the audit trail
23
+ # @param chain_id [String] audit chain identifier
24
+ #
25
+ # @return [Hash] { path:, before_hash:, after_hash:, verified: true }
26
+ # @raise [WriteVerificationError] when re-read digest does not match written digest
27
+ def verified_write(path, content, agent_id: nil, chain_id: 'file_edits')
28
+ before_hash = ::File.exist?(path) ? sha256_file(path) : nil
29
+ expected = sha256_string(content)
30
+
31
+ ::File.write(path, content)
32
+
33
+ actual = sha256_file(path)
34
+ unless actual == expected
35
+ raise WriteVerificationError,
36
+ "write verification failed for #{path}: expected #{expected}, got #{actual}"
37
+ end
38
+
39
+ record_audit(
40
+ path: path,
41
+ action: 'verified_write',
42
+ agent_id: agent_id,
43
+ chain_id: chain_id,
44
+ before_hash: before_hash,
45
+ after_hash: actual
46
+ )
47
+
48
+ { path: path, before_hash: before_hash, after_hash: actual, verified: true }
49
+ end
50
+
51
+ # Apply a string-replacement edit to +path+, with a staleness check before writing
52
+ # and SHA-256 verification after writing.
53
+ #
54
+ # @param path [String] absolute or relative filesystem path
55
+ # @param old_content [String] expected current file content (used for staleness check)
56
+ # @param new_content [String] desired file content after edit
57
+ # @param agent_id [String, nil] identity recorded in the audit trail
58
+ # @param chain_id [String] audit chain identifier
59
+ #
60
+ # @return [Hash] { path:, before_hash:, after_hash:, verified: true }
61
+ # @raise [StaleEditError] when the file has been modified since +old_content+ was read
62
+ # @raise [WriteVerificationError] when re-read digest does not match written digest
63
+ def verified_edit(path, old_content, new_content, agent_id: nil, chain_id: 'file_edits')
64
+ before_hash = sha256_string(old_content)
65
+ on_disk_hash = sha256_file(path)
66
+
67
+ unless on_disk_hash == before_hash
68
+ raise StaleEditError,
69
+ "stale edit detected for #{path}: disk content has changed since old_content was read"
70
+ end
71
+
72
+ verified_write(path, new_content, agent_id: agent_id, chain_id: chain_id)
73
+ end
74
+
75
+ private
76
+
77
+ def sha256_string(str)
78
+ ::Digest::SHA256.hexdigest(str)
79
+ end
80
+
81
+ def sha256_file(path)
82
+ ::Digest::SHA256.file(path).hexdigest
83
+ end
84
+
85
+ def record_audit(path:, action:, agent_id:, chain_id:, before_hash:, after_hash:)
86
+ return unless defined?(Legion::Data::Model::AuditLog)
87
+
88
+ Legion::Data::Model::AuditLog.create(
89
+ event_type: 'file_operation',
90
+ principal_id: agent_id || 'system',
91
+ principal_type: 'system',
92
+ action: action,
93
+ resource: path,
94
+ source: chain_id,
95
+ node: 'local',
96
+ status: 'success',
97
+ detail: ::JSON.generate({ before_hash: before_hash, after_hash: after_hash }),
98
+ record_hash: ::Digest::SHA256.hexdigest("#{before_hash}|#{after_hash}|#{path}"),
99
+ prev_hash: before_hash || ('0' * 64),
100
+ created_at: ::Time.now.utc
101
+ )
102
+ rescue StandardError => e
103
+ # audit recording is best-effort; never let it break the write
104
+ log.warn("[lex-audit] verified_write audit record failed: #{e.message}")
105
+ nil
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Audit
6
- VERSION = '0.1.5'
6
+ VERSION = '0.1.6'
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/extensions/audit/version'
4
+ require 'legion/extensions/audit/errors'
5
+ require 'legion/extensions/audit/helpers/verified_write'
4
6
  require 'legion/extensions/audit/runners/approval_queue'
5
7
 
6
8
  module Legion
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-audit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -107,20 +107,6 @@ dependencies:
107
107
  - - ">="
108
108
  - !ruby/object:Gem::Version
109
109
  version: 1.3.9
110
- - !ruby/object:Gem::Dependency
111
- name: rake
112
- requirement: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '0'
117
- type: :development
118
- prerelease: false
119
- version_requirements: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - ">="
122
- - !ruby/object:Gem::Version
123
- version: '0'
124
110
  - !ruby/object:Gem::Dependency
125
111
  name: rspec
126
112
  requirement: !ruby/object:Gem::Requirement
@@ -213,6 +199,8 @@ files:
213
199
  - lex-audit.gemspec
214
200
  - lib/legion/extensions/audit.rb
215
201
  - lib/legion/extensions/audit/actors/audit_writer.rb
202
+ - lib/legion/extensions/audit/errors.rb
203
+ - lib/legion/extensions/audit/helpers/verified_write.rb
216
204
  - lib/legion/extensions/audit/runners/approval_queue.rb
217
205
  - lib/legion/extensions/audit/runners/audit.rb
218
206
  - lib/legion/extensions/audit/transport/exchanges/audit.rb