moose-inventory 2.1 → 2.1.1

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: 439e23e1ec3d1ed0e83f686ddd062c07215f8575febd515dc65657f3bdc70c45
4
- data.tar.gz: d07bd1e237be3056bf54f338eccc240b74790378d94d1c40d6bc86f2b3b226f9
3
+ metadata.gz: ba11da77ca5c7033211f845076713ac540041e719f2a5a8bf4c75d84c98924db
4
+ data.tar.gz: 1c39004653d1aba885d38e966a91f7bea1c4bc9dc4579b98bdf57e2e333fecc0
5
5
  SHA512:
6
- metadata.gz: 72d9b7d85c843ad90d5b9d41aea47390c5ec6ea9bca1844bf2e7912e9bdeca39214718652e8f3ef44bda1deb83ff54ab0667e5652b81e516f332263ccf5fd83d
7
- data.tar.gz: ddad54b04a60635044cf8d312283bd59fd6c42d8862c5759166ee219a275d2b38469fd358ca839ef1c519903736f932e315fa9f1ad6a8b31e8bfa4e0575f75f6
6
+ metadata.gz: 5f565f1f646917e1cde181f5f068ffca699452327f4b1d72621cabc2b5997c2022c369b312e842ed6346e860d382f277e7dd7c234a182796c8e80cb70118e007
7
+ data.tar.gz: ec127be873c29e00feda3d2cde569d583b4d025c66b70e10c5b288f2d59c7f4b21b35bae851d143b244b513269cebae0108815d163efe0483c6aedb8f86488ad
data/.gitignore CHANGED
@@ -19,3 +19,7 @@ __pycache__/
19
19
  /gems/
20
20
  moose-inventory.spec
21
21
  /.openclaw-security-audit/
22
+
23
+ # Local inventory databases and SQLite sidecar files
24
+ inventory.db
25
+ inventory.db-*
data/BACKLOG.md CHANGED
@@ -99,17 +99,20 @@ _No open process conformance items._
99
99
 
100
100
  # Moose Inventory Architecture Follow-up Backlog
101
101
 
102
- Architecture follow-up status counts: 4 done / 1 open.
102
+ Architecture follow-up status counts: 5 done / 0 open.
103
103
 
104
104
  ## Open
105
105
 
106
- 1. Verify GitHub `release` environment custom `v*` policy behavior on the next real release.
107
- - GitHub accepted a custom deployment policy named `v*`, but the API reports the policy object as `type: branch`.
108
- - On the next intentional `v*` tag release, verify that the release job can deploy to the `release` environment after required approval.
109
- - If GitHub treats the policy as branch-only and blocks tag deployments, adjust the environment policy or document the limitation and rely on the workflow trigger plus tag/version check for tag control.
106
+ _No open architecture follow-up items._
110
107
 
111
108
  ## Done
112
109
 
110
+ 1. Verify GitHub `release` environment custom `v*` policy behavior on the next real release.
111
+ - Verified during the intentional `v2.1` release.
112
+ - Initial release workflow run `26670139178` was rejected because GitHub treated the existing `v*` deployment policy as `type: branch`, which did not allow tag deployments.
113
+ - After explicit Russ approval, replaced the branch-typed policy with `v*` `type: tag`, reran and approved the release deployment, and published `moose-inventory` 2.1 successfully.
114
+ - Documented the verified tag-policy behavior in `docs/release/release-environment-protection.md` and `docs/release/publishing.md`.
115
+
113
116
  1. Expand user database backup/restore guidance beyond SQLite.
114
117
  - Added `docs/maintenance/database-backup-restore-guidance.md` documenting SQLite, MySQL/MariaDB, and PostgreSQL backup/restore boundaries.
115
118
  - Clarified that Moose Inventory can inspect status, run migrations, run doctor checks, back up SQLite files, and export snapshots, but does not run server-backed dump/restore commands, manage grants/users, or implement destructive restore/sync semantics.
@@ -595,14 +598,18 @@ _No open modernization items._
595
598
 
596
599
  # Moose Inventory Code Quality Backlog
597
600
 
598
- Code quality status counts: 66 done / 0 open.
601
+ Code quality status counts: 67 done / 0 open.
599
602
 
600
603
  ## Open
601
604
 
602
- _No open code-quality items._
603
-
604
605
  ## Done
605
606
 
607
+ 1. Sanitize non-syntax Psych safe-load failures during snapshot import.
608
+ - Added a `Psych::Exception` rescue after the syntax-specific parse handler so aliases and disallowed classes are rejected with a single sanitized `ERROR:` line instead of Ruby/Psych stack traces.
609
+ - Added focused CLI regression coverage for alias and disallowed-class payloads during non-mutating `import --preview`.
610
+ - Verified with targeted snapshot-import fuzzing and `MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS=1 ./scripts/check.sh` on 2026-05-30.
611
+ - Evidence: `.openclaw-security-audit/fuzz_snapshot_import_availability_run_2026-05-30T0208Z-after-fix.json`.
612
+
606
613
  1. Add focused specs for `OperationEventSupport` result defaults and event construction.
607
614
  - Added direct unit coverage for default empty event payloads, explicit payload preservation, event emission, default `warning_count: 0`, and explicit warning counts.
608
615
  - Added the new helper spec to the targeted RuboCop gate.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- moose-inventory (2.1)
4
+ moose-inventory (2.1.1)
5
5
  indentation (~> 0)
6
6
  json (>= 2.7, < 3)
7
7
  mysql2 (>= 0.5.7, < 0.6)
@@ -20,7 +20,7 @@ RubyGems has a trusted publisher configured for the existing `moose-inventory` g
20
20
 
21
21
  The release workflow requires the GitHub environment name `release`. Current environment protection evidence is documented in `docs/release/release-environment-protection.md`.
22
22
 
23
- As of 2026-05-29, the GitHub `release` environment has required reviewer protection for `RusDavies`, self-review prevention disabled, admin bypass disabled, and a custom deployment policy named `v*`. Self-review prevention is disabled because OpenClaw/automation pushes use Russ's GitHub account; with `RusDavies` as the only required reviewer, enabling self-review prevention could prevent Russ from approving the deployment. The workflow itself still runs only for pushed `v*` tags and verifies tag/version alignment before publishing. Because GitHub reports the custom deployment policy object as `type: branch`, verify tag-deployment behavior on the next real release and adjust the environment policy if needed.
23
+ As of 2026-05-29, the GitHub `release` environment has required reviewer protection for `RusDavies`, self-review prevention disabled, admin bypass disabled, and a custom deployment policy named `v*` with `type: tag`. Self-review prevention is disabled because OpenClaw/automation pushes use Russ's GitHub account; with `RusDavies` as the only required reviewer, enabling self-review prevention could prevent Russ from approving the deployment. The workflow itself still runs only for pushed `v*` tags and verifies tag/version alignment before publishing. Tag deployment behavior was verified by the successful `v2.1` release workflow after replacing the initial branch-typed `v*` policy with a tag-typed policy.
24
24
 
25
25
  Package provenance hardening beyond RubyGems trusted publishing is evaluated in `docs/release/package-provenance-hardening.md`. Additional checksums, GitHub artifact attestations, signatures, or SBOM publication are future hardening options, not current release blockers.
26
26
 
@@ -9,14 +9,18 @@ This document records the current GitHub `release` environment protection settin
9
9
  Initial confirmation date: 2026-05-29
10
10
  Protection configuration date: 2026-05-29
11
11
  Self-review adjustment date: 2026-05-29
12
+ Tag policy correction/verification date: 2026-05-29
12
13
 
13
14
  Confirmation/configuration commands:
14
15
 
15
16
  ```bash
16
17
  gh api repos/RusDavies/moose-inventory/environments/release --jq '.'
17
18
  gh api -X PUT repos/RusDavies/moose-inventory/environments/release --input /tmp/moose-env-release.json
18
- gh api -X POST repos/RusDavies/moose-inventory/environments/release/deployment-branch-policies -f name='v*'
19
+ gh api -X POST repos/RusDavies/moose-inventory/environments/release/deployment-branch-policies -f name='v*' -f type='tag'
20
+ gh api -X DELETE repos/RusDavies/moose-inventory/environments/release/deployment-branch-policies/50646877
19
21
  gh api repos/RusDavies/moose-inventory/environments/release/deployment-branch-policies --jq '.'
22
+ gh run rerun 26670139178
23
+ gh api -X POST repos/RusDavies/moose-inventory/actions/runs/26670139178/pending_deployments --input /tmp/moose-approve-deployment.json
20
24
  ```
21
25
 
22
26
  Confirmed repository/environment:
@@ -37,7 +41,7 @@ Confirmed repository/environment:
37
41
  | Prevent self-review | Disabled (`prevent_self_review: false`) | Disabled because OpenClaw/automation pushes use Russ's GitHub account. With `RusDavies` as the only required reviewer, self-review prevention could block Russ from approving a release deployment triggered through his own account. |
38
42
  | Wait timer | `0` / none | No arbitrary delay is configured. Human review is the intended release friction. |
39
43
  | Deployment branch/tag policy mode | Custom branch policies enabled (`protected_branches: false`, `custom_branch_policies: true`) | The environment uses an explicit allow-list rather than all branches/tags. |
40
- | Custom deployment policy | `v*` | Intended to align environment deployment eligibility with the release workflow's `v*` tag trigger. GitHub API reports this policy object with `type: branch`; verify behavior on the next real release tag and adjust if GitHub does not apply it to tag deployments as expected. |
44
+ | Custom deployment policy | `v*` with `type: tag` | Aligns environment deployment eligibility with the release workflow's `v*` tag trigger. An earlier `type: branch` policy rejected tag `v2.1`; after explicit human approval, it was replaced with a tag policy and verified by the successful `v2.1` release workflow. |
41
45
  | Admin bypass | Disabled (`can_admins_bypass: false`) | Admins should not be able to bypass environment protection for this environment through the normal bypass setting. |
42
46
 
43
47
  ## Current trusted-publishing path
@@ -48,7 +52,7 @@ The current release path relies on these controls:
48
52
  2. The GitHub `release` environment requires review by `RusDavies` before the release job can proceed.
49
53
  3. Self-review prevention is disabled so `RusDavies` can approve deployments triggered by automation authenticated as Russ's GitHub account.
50
54
  4. Admin bypass is disabled for the `release` environment.
51
- 5. The environment has a custom deployment policy named `v*`.
55
+ 5. The environment has a custom tag deployment policy named `v*`.
52
56
  6. The release workflow verifies that the tag version matches `Moose::Inventory::VERSION`.
53
57
  7. The release workflow installs security tools and runs:
54
58
 
@@ -59,11 +63,15 @@ The current release path relies on these controls:
59
63
  8. RubyGems trusted publishing/OIDC is scoped to repository `RusDavies/moose-inventory`, workflow `release.yml`, and environment `release`.
60
64
  9. The package sanity gate verifies the gem payload and executable metadata before publishing.
61
65
 
62
- ## Residual verification note
66
+ ## Tag-policy verification note
63
67
 
64
- GitHub accepted the custom deployment policy name `v*`, but the deployment-branch-policy API response reports the policy object as `type: branch`. The next real release should verify that a pushed `v*` tag can still deploy to the `release` environment after human approval.
68
+ The first `v2.1` workflow attempt proved that a custom deployment policy named `v*` with API `type: branch` does not permit tag deployments. GitHub rejected the run before any workflow steps executed with:
65
69
 
66
- If GitHub treats the custom policy as branch-only and blocks tag deployments, maintainers should either adjust the environment policy to the supported tag pattern mechanism or document the limitation and rely on the workflow's `v*` tag trigger plus tag/version check as the tag-control layer.
70
+ ```text
71
+ Tag "v2.1" is not allowed to deploy to release due to environment protection rules.
72
+ ```
73
+
74
+ After explicit human approval, the branch policy was replaced with a custom deployment policy named `v*` and `type: tag`. The rerun of workflow `26670139178` for tag `v2.1` then entered the required-review gate, was approved by `RusDavies`, completed the full release workflow successfully, and published `moose-inventory` version `2.1` to RubyGems.
67
75
 
68
76
  ## Change-control note
69
77
 
@@ -0,0 +1,58 @@
1
+ # Snapshot Import Availability Fuzz Audit - 2026-05-29
2
+
3
+ Repository: `RusDavies/moose-inventory`
4
+ Local path: `/home/skippy/.openclaw/workspace/projects/moose-inventory`
5
+ Commit audited: `48da0472f8fadb7685777987545d51b4b62eb806`
6
+ Version audited: `2.1`
7
+ Audit context: follow-up to `code-security-audit` run `3`
8
+
9
+ ## Scope
10
+
11
+ This pass targeted snapshot import availability and malformed-input behavior for the local CLI import path:
12
+
13
+ ```bash
14
+ ruby -Ilib bin/moose-inventory --config spec/config/config.yml import SNAPSHOT.yml --preview
15
+ ```
16
+
17
+ The pass used preview mode to keep the fuzz cases non-mutating while still exercising YAML loading, snapshot normalization, validation, and preview generation.
18
+
19
+ ## Cases exercised
20
+
21
+ | Case | Result | Notes |
22
+ | --- | --- | --- |
23
+ | Baseline valid snapshot | Pass | Preview completed in 0.127s. |
24
+ | Malformed YAML | Graceful rejection | Reported a sanitized `ERROR: Could not parse inventory snapshot ...`. |
25
+ | YAML alias | Ungraceful rejection | Rejected safely, but emitted a Ruby/Psych stack trace. |
26
+ | Disallowed symbol key / duplicate-normalized-key probe | Ungraceful rejection | Rejected safely, but emitted a Ruby/Psych stack trace before project-level validation. |
27
+ | Group cycle | Graceful rejection | Reported `Invalid inventory snapshot: group hierarchy contains a cycle ...`. |
28
+ | Unknown child group reference | Graceful rejection | Reported `Invalid inventory snapshot: group ... references unknown child group ...`. |
29
+ | 5,000 variable entries | Pass | Preview completed in 0.157s. |
30
+ | 250-deep group chain | Pass | Preview completed in 0.142s. |
31
+ | 1,000-deep group chain | Pass | Preview completed in 0.187s. |
32
+ | 3,000-deep group chain | Pass | Preview completed in 0.306s. |
33
+
34
+ Raw local artifact: `.openclaw-security-audit/fuzz_snapshot_import_availability_run.json`.
35
+
36
+ ## Finding
37
+
38
+ ### P3 / Low: snapshot import does not sanitize all Psych safe-load exceptions
39
+
40
+ `Application#import` currently rescues `Psych::SyntaxError` around `YAML.safe_load_file`, but safe-load can also raise other Psych exceptions such as `Psych::AliasesNotEnabled` and `Psych::DisallowedClass` before Moose Inventory's snapshot validator runs.
41
+
42
+ Observed examples:
43
+
44
+ - YAML alias payload is rejected because aliases are disabled, but the CLI prints a Ruby/Psych stack trace.
45
+ - Symbol-tag payload is rejected because permitted classes are restricted, but the CLI prints a Ruby/Psych stack trace.
46
+
47
+ Security interpretation:
48
+
49
+ - The unsafe payloads were rejected, so this is not unsafe deserialization or code execution.
50
+ - The issue is availability/UX hardening and minor information disclosure through local stack traces and filesystem/gem paths.
51
+ - Reachability is local CLI import of an attacker-supplied or untrusted snapshot file.
52
+ - Priority is P3/low because the blast radius is local command failure/noisy logs rather than inventory corruption or privilege escalation.
53
+
54
+ ## Recommended remediation
55
+
56
+ Add a focused backlog item to handle non-syntax `Psych::Exception` failures in the import path with the same sanitized `ERROR: Could not parse inventory snapshot ...` style, and add regression cases for aliases/disallowed classes.
57
+
58
+ A later hardening pass may also consider explicit snapshot size/entity/depth limits, but this fuzz pass did not find practical timeout behavior at the bounded sizes tested.
@@ -72,6 +72,8 @@ module Moose
72
72
  puts "Associations added: #{result.associations}"
73
73
  rescue Psych::SyntaxError => e
74
74
  abort("ERROR: Could not parse inventory snapshot '#{file}': #{e.message}")
75
+ rescue Psych::Exception => e
76
+ abort("ERROR: Could not load inventory snapshot '#{file}': #{e.message}")
75
77
  rescue db.exceptions[:moose] => e
76
78
  abort("ERROR: #{e.message}")
77
79
  end
@@ -3,6 +3,64 @@
3
3
  module Moose
4
4
  module Inventory
5
5
  module Operations
6
+ # Validates group hierarchy cycle safety without recursive traversal.
7
+ class GroupCycleValidator
8
+ def initialize(context:)
9
+ @context = context
10
+ end
11
+
12
+ def call(groups)
13
+ visiting = {}
14
+ visited = {}
15
+
16
+ groups.each_key do |root|
17
+ validate_from_root!(root, groups, visiting, visited)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :context
24
+
25
+ def validate_from_root!(root, groups, visiting, visited)
26
+ stack = [[root, false]]
27
+ until stack.empty?
28
+ name, expanded = stack.pop
29
+ next if visited[name]
30
+
31
+ if expanded
32
+ visiting.delete(name)
33
+ visited[name] = true
34
+ else
35
+ queue_group_children!(name, groups, visiting, stack)
36
+ end
37
+ end
38
+ end
39
+
40
+ def queue_group_children!(name, groups, visiting, stack)
41
+ raise_invalid("group hierarchy contains a cycle at '#{name}'") if visiting[name]
42
+
43
+ visiting[name] = true
44
+ stack << [name, true]
45
+ array_value(groups[name], 'children', label: "group '#{name}' children").reverse_each do |child_name|
46
+ raise_invalid("group hierarchy contains a cycle at '#{child_name}'") if visiting[child_name]
47
+
48
+ stack << [child_name, false]
49
+ end
50
+ end
51
+
52
+ def array_value(payload, key, label:)
53
+ value = payload.fetch(key, [])
54
+ raise_invalid("#{label} must be a list") unless value.is_a?(Array)
55
+
56
+ value.map(&:to_s)
57
+ end
58
+
59
+ def raise_invalid(message)
60
+ raise context.moose_exception_class, "Invalid inventory snapshot: #{message}."
61
+ end
62
+ end
63
+
6
64
  # Normalizes and validates portable inventory snapshot input before import.
7
65
  class InventorySnapshotValidator
8
66
  def initialize(context:)
@@ -73,25 +131,7 @@ module Moose
73
131
  end
74
132
 
75
133
  def validate_group_cycles!(groups)
76
- visiting = {}
77
- visited = {}
78
-
79
- groups.each_key do |name|
80
- visit_group!(name, groups, visiting, visited)
81
- end
82
- end
83
-
84
- def visit_group!(name, groups, visiting, visited)
85
- return if visited[name]
86
-
87
- raise_invalid("group hierarchy contains a cycle at '#{name}'") if visiting[name]
88
-
89
- visiting[name] = true
90
- array_value(groups[name], 'children', label: "group '#{name}' children").each do |child_name|
91
- visit_group!(child_name, groups, visiting, visited)
92
- end
93
- visiting.delete(name)
94
- visited[name] = true
134
+ GroupCycleValidator.new(context: context).call(groups)
95
135
  end
96
136
 
97
137
  def array_value(payload, key, label:)
@@ -4,6 +4,6 @@ module Moose
4
4
  ##
5
5
  # The Moose-Tools dynamic inventory management library
6
6
  module Inventory
7
- VERSION = '2.1'
7
+ VERSION = '2.1.1'
8
8
  end
9
9
  end
@@ -65,4 +65,7 @@ else
65
65
  exit 0
66
66
  fi
67
67
 
68
- "${OSV_SCANNER[@]}" scan source --lockfile Gemfile.lock .
68
+ # osv-scanner 2.x treats --lockfile as an explicit scan target; adding the
69
+ # repository path as a positional source makes it try to infer an extractor for
70
+ # Gemfile.lock through the directory scanner and fail with exit 127.
71
+ "${OSV_SCANNER[@]}" scan source --lockfile Gemfile.lock
@@ -96,5 +96,37 @@ RSpec.describe Moose::Inventory::Cli::Application do
96
96
  expect(@db.models[:host].count).to eq(0)
97
97
  end
98
98
  end
99
+
100
+ it 'sanitizes alias rejection errors while previewing snapshot import' do
101
+ Dir.mktmpdir do |dir|
102
+ path = File.join(dir, 'inventory.yml')
103
+ File.write(path, "---\nversion: 1\ngroups: &groups {}\nhosts: *groups\n")
104
+
105
+ actual = runner { @app.start(['import', path, '--preview']) }
106
+
107
+ expect(actual[:aborted]).to eq(true)
108
+ expect(actual[:STDERR]).to include("ERROR: Could not load inventory snapshot '#{path}':")
109
+ expect(actual[:STDERR]).to include('Alias parsing was not enabled')
110
+ expect(actual[:STDERR]).not_to include('/psych/')
111
+ expect(actual[:STDERR]).not_to include('from ')
112
+ expect(@db.models[:host].count).to eq(0)
113
+ end
114
+ end
115
+
116
+ it 'sanitizes disallowed class errors while previewing snapshot import' do
117
+ Dir.mktmpdir do |dir|
118
+ path = File.join(dir, 'inventory.yml')
119
+ File.write(path, "---\nversion: 1\ngroups:\n g: {}\n :g: {}\nhosts: {}\n")
120
+
121
+ actual = runner { @app.start(['import', path, '--preview']) }
122
+
123
+ expect(actual[:aborted]).to eq(true)
124
+ expect(actual[:STDERR]).to include("ERROR: Could not load inventory snapshot '#{path}':")
125
+ expect(actual[:STDERR]).to include('Tried to load unspecified class: Symbol')
126
+ expect(actual[:STDERR]).not_to include('/psych/')
127
+ expect(actual[:STDERR]).not_to include('from ')
128
+ expect(@db.models[:host].count).to eq(0)
129
+ end
130
+ end
99
131
  end
100
132
  # rubocop:enable Metrics/BlockLength
@@ -222,5 +222,18 @@ RSpec.describe Moose::Inventory::Operations::ImportInventorySnapshot, '#preview'
222
222
  expect(@db.models[:group].where(name: 'blue').count).to eq(0)
223
223
  expect(group.groupvars_dataset[name: 'role'][:value]).to eq('old')
224
224
  end
225
+
226
+ it 'previews deep acyclic group chains without recursive validator stack exhaustion' do
227
+ groups = (1..5_000).to_h do |index|
228
+ child = index < 5_000 ? ["group#{index + 1}"] : []
229
+ ["group#{index}", { children: child, vars: {} }]
230
+ end
231
+
232
+ preview = operation.preview(snapshot: { version: 1, hosts: {}, groups: groups })
233
+
234
+ expect(preview['changes_applied']).to eq(false)
235
+ expect(preview.dig('summary', 'groups_created')).to eq(5_000)
236
+ expect(@db.models[:group].count).to eq(0)
237
+ end
225
238
  end
226
239
  # rubocop:enable Metrics/BlockLength
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moose-inventory
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.1'
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Russell Davies
@@ -307,6 +307,7 @@ files:
307
307
  - docs/security-audit-2026-05-21.md
308
308
  - docs/security-audit-2026-05-26-rerun.md
309
309
  - docs/security-audit-2026-05-26.md
310
+ - docs/security-audit-2026-05-29-snapshot-import-fuzz.md
310
311
  - docs/security/accepted-risk-register.md
311
312
  - docs/security/security-privacy-process.md
312
313
  - docs/ux/cli-workflow-notes.md