subspace 3.0.20 → 3.0.21
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 +3 -0
- data/ansible/roles/gem-patch-report/README.md +137 -0
- data/ansible/roles/gem-patch-report/defaults/main.yml +2 -0
- data/ansible/roles/gem-patch-report/tasks/main.yml +89 -0
- data/ansible/roles/gem-patch-report/templates/gem-patch-report.sh.j2 +114 -0
- data/lib/subspace/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df4ca7c7afe682b2ea4aa12fd7ce11ea9fd7d87a380cd9b7adc9b4b3aa554228
|
|
4
|
+
data.tar.gz: 5f14219cb20dcfdb6adf6dde79b49dbdd33f1db12223dee02c8ddc76e5f72138
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cccd145a57a66a18dc2224496e250b022801d1f57643bbddd14bc4e7a844d684d9656d919c19fd339455ac209e1ef204702e0ea68a179faf6990181514479d0
|
|
7
|
+
data.tar.gz: b0eeed81c79e8637f78db6be40f32e8d6fff8ad552bf44e1305dad4fe57d0ede687bf3aa32de842ebaaee5cd4bbf1f38330a6ef74224b7412fa7f9e1ff02eb3d
|
data/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,9 @@ This project attempts to follow [semantic versioning](https://semver.org/).
|
|
|
12
12
|
|
|
13
13
|
## Unreleased
|
|
14
14
|
|
|
15
|
+
## 3.0.21
|
|
16
|
+
* Add gem-patch-report role. Sends stats for each vulnerable gem fixed since the start of the month.
|
|
17
|
+
|
|
15
18
|
## 3.0.20
|
|
16
19
|
* Update postgresql-client role to get the actual psql database version instead of the local client version
|
|
17
20
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Gem Patch Report Role
|
|
2
|
+
|
|
3
|
+
This Ansible role generates reports on Ruby gems that had security vulnerabilities at the beginning of the current month and have since been patched. It compares the Gemfile.lock from the start of the month (retrieved from the Capistrano git repo) against the current Gemfile.lock using bundle-audit, and reports only the gems that have been fixed.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby application deployed with Capistrano (requires the `repo/` directory)
|
|
8
|
+
- bundler-audit gem (automatically installed if missing)
|
|
9
|
+
- Stats server configuration (same as other subspace roles)
|
|
10
|
+
|
|
11
|
+
## Role Variables
|
|
12
|
+
|
|
13
|
+
Available variables with their default values:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
# Path to the Rails/Ruby application (Capistrano current symlink)
|
|
17
|
+
gem_patch_report_app_path: "/u/apps/{{project_name}}/current"
|
|
18
|
+
|
|
19
|
+
# Required for stats reporting (inherited from other roles)
|
|
20
|
+
send_stats: false
|
|
21
|
+
stats_url: ""
|
|
22
|
+
stats_api_key: ""
|
|
23
|
+
hostname: ""
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
1. Enable the role in your playbook:
|
|
29
|
+
```yaml
|
|
30
|
+
roles:
|
|
31
|
+
- gem-patch-report
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. Configure the required variables:
|
|
35
|
+
```yaml
|
|
36
|
+
send_stats: true
|
|
37
|
+
stats_url: "https://your-stats-server.com/api/stats"
|
|
38
|
+
stats_api_key: "your-api-key"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How It Works
|
|
42
|
+
|
|
43
|
+
1. Retrieves the Gemfile.lock from the last commit before the 1st of the current month using the Capistrano bare git repo at `<deploy_to>/repo/`
|
|
44
|
+
2. Runs `bundle-audit` against both the old and current Gemfile.lock
|
|
45
|
+
3. Compares the results to identify vulnerabilities that existed at the start of the month but are no longer present (i.e. patched)
|
|
46
|
+
4. Outputs a report containing only the patched gems
|
|
47
|
+
|
|
48
|
+
## Bundle-Audit Input Format
|
|
49
|
+
|
|
50
|
+
The role processes JSON output from `bundle-audit check --format json`. Here's the simplified structure:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"version": "0.9.2",
|
|
55
|
+
"created_at": "2026-03-06 12:16:58 -0600",
|
|
56
|
+
"results": [
|
|
57
|
+
{
|
|
58
|
+
"type": "unpatched_gem",
|
|
59
|
+
"gem": {
|
|
60
|
+
"name": "nokogiri",
|
|
61
|
+
"version": "1.18.9"
|
|
62
|
+
},
|
|
63
|
+
"advisory": {
|
|
64
|
+
"id": "GHSA-wx95-c6cv-8532",
|
|
65
|
+
"title": "Nokogiri does not check the return value from xmlC14NExecute",
|
|
66
|
+
"criticality": "medium",
|
|
67
|
+
"cve": null,
|
|
68
|
+
"patched_versions": [">= 1.19.1"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Key Fields Used
|
|
76
|
+
|
|
77
|
+
The role extracts data from these fields:
|
|
78
|
+
- `results[].type` - Filters for `"unpatched_gem"`
|
|
79
|
+
- `results[].gem.name` - Gem name
|
|
80
|
+
- `results[].gem.version` - Vulnerable version
|
|
81
|
+
- `results[].advisory.id` - Advisory ID (CVE, GHSA, etc.)
|
|
82
|
+
- `results[].advisory.criticality` - Severity level
|
|
83
|
+
|
|
84
|
+
## Report Format
|
|
85
|
+
|
|
86
|
+
The role generates a JSON array of gems that were patched during the current month. If a gem had multiple advisories that were all fixed, each advisory appears as its own entry.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
"name": "activestorage",
|
|
92
|
+
"version": "8.0.2.1",
|
|
93
|
+
"current_version": "8.0.4.1",
|
|
94
|
+
"advisory_id": "CVE-2026-33658",
|
|
95
|
+
"criticality": null
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "bcrypt",
|
|
99
|
+
"version": "3.1.20",
|
|
100
|
+
"current_version": "3.1.22",
|
|
101
|
+
"advisory_id": "CVE-2026-33306",
|
|
102
|
+
"criticality": null
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "devise",
|
|
106
|
+
"version": "4.9.4",
|
|
107
|
+
"current_version": "5.0.3",
|
|
108
|
+
"advisory_id": "CVE-2026-32700",
|
|
109
|
+
"criticality": null
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Gem Objects
|
|
115
|
+
|
|
116
|
+
Each gem object contains:
|
|
117
|
+
- `name`: The gem name
|
|
118
|
+
- `version`: The vulnerable version from the start of the month
|
|
119
|
+
- `current_version`: The current patched version (parsed from the current Gemfile.lock)
|
|
120
|
+
- `advisory_id`: The security advisory ID (CVE, GHSA, etc.)
|
|
121
|
+
- `criticality`: The severity level (low, medium, high, critical, or null if not specified)
|
|
122
|
+
|
|
123
|
+
## Tags
|
|
124
|
+
|
|
125
|
+
This role supports the following tags:
|
|
126
|
+
- `maintenance`
|
|
127
|
+
- `stats`
|
|
128
|
+
- `gem-patch-report`
|
|
129
|
+
|
|
130
|
+
## Error Handling
|
|
131
|
+
|
|
132
|
+
The role includes error handling for common scenarios:
|
|
133
|
+
- Missing Gemfile.lock
|
|
134
|
+
- No git history before the current month
|
|
135
|
+
- bundle-audit execution failures
|
|
136
|
+
|
|
137
|
+
Errors are logged but don't fail the entire playbook execution. If no patched gems are found, an empty array `[]` is reported.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
- name: Check if gem patch reporting is enabled and required variables are set
|
|
3
|
+
debug:
|
|
4
|
+
msg: "Gem patch reporting is enabled"
|
|
5
|
+
when: send_stats == true and stats_url is defined and stats_api_key is defined
|
|
6
|
+
tags:
|
|
7
|
+
- maintenance
|
|
8
|
+
- stats
|
|
9
|
+
- gem-patch-report
|
|
10
|
+
|
|
11
|
+
- name: Install bundler-audit gem globally
|
|
12
|
+
gem:
|
|
13
|
+
name: bundler-audit
|
|
14
|
+
state: present
|
|
15
|
+
user_install: false
|
|
16
|
+
become: true
|
|
17
|
+
when: send_stats == true and stats_url is defined and stats_api_key is defined
|
|
18
|
+
tags:
|
|
19
|
+
- maintenance
|
|
20
|
+
- stats
|
|
21
|
+
- gem-patch-report
|
|
22
|
+
|
|
23
|
+
- name: Generate gem patch report script
|
|
24
|
+
template:
|
|
25
|
+
src: gem-patch-report.sh.j2
|
|
26
|
+
dest: /tmp/gem-patch-report.sh
|
|
27
|
+
mode: '0755'
|
|
28
|
+
when: send_stats == true and stats_url is defined and stats_api_key is defined
|
|
29
|
+
tags:
|
|
30
|
+
- maintenance
|
|
31
|
+
- stats
|
|
32
|
+
- gem-patch-report
|
|
33
|
+
|
|
34
|
+
- name: Run gem patch report script
|
|
35
|
+
shell: /tmp/gem-patch-report.sh
|
|
36
|
+
register: gem_patch_report_result
|
|
37
|
+
become: true
|
|
38
|
+
when: send_stats == true and stats_url is defined and stats_api_key is defined
|
|
39
|
+
tags:
|
|
40
|
+
- maintenance
|
|
41
|
+
- stats
|
|
42
|
+
- gem-patch-report
|
|
43
|
+
ignore_errors: yes
|
|
44
|
+
|
|
45
|
+
- name: Read gem patch report file
|
|
46
|
+
slurp:
|
|
47
|
+
src: /tmp/gem-patch-report.json
|
|
48
|
+
register: gem_patch_report_file
|
|
49
|
+
when: send_stats == true and stats_url is defined and stats_api_key is defined
|
|
50
|
+
tags:
|
|
51
|
+
- maintenance
|
|
52
|
+
- stats
|
|
53
|
+
- gem-patch-report
|
|
54
|
+
ignore_errors: yes
|
|
55
|
+
|
|
56
|
+
- name: Send gem patch report to stats server
|
|
57
|
+
uri:
|
|
58
|
+
url: "{{ stats_url }}"
|
|
59
|
+
method: POST
|
|
60
|
+
headers:
|
|
61
|
+
X-API-Version: 1
|
|
62
|
+
X-Client-Api-key: "{{ stats_api_key }}"
|
|
63
|
+
body_format: json
|
|
64
|
+
body:
|
|
65
|
+
client_stat:
|
|
66
|
+
stat_type: gem_patch_report
|
|
67
|
+
value: "{{ (gem_patch_report_file.content | b64decode) | from_json }}"
|
|
68
|
+
hostname: "{{ hostname }}"
|
|
69
|
+
when: send_stats == true and stats_url is defined and stats_api_key is defined and gem_patch_report_file is defined and gem_patch_report_file is not failed
|
|
70
|
+
tags:
|
|
71
|
+
- maintenance
|
|
72
|
+
- stats
|
|
73
|
+
- gem-patch-report
|
|
74
|
+
ignore_errors: yes
|
|
75
|
+
|
|
76
|
+
- name: Clean up temporary gem patch report files
|
|
77
|
+
file:
|
|
78
|
+
path: "{{ item }}"
|
|
79
|
+
state: absent
|
|
80
|
+
become: true
|
|
81
|
+
loop:
|
|
82
|
+
- /tmp/gem-patch-report.sh
|
|
83
|
+
- /tmp/gem-patch-report.json
|
|
84
|
+
when: send_stats == true and stats_url is defined and stats_api_key is defined
|
|
85
|
+
tags:
|
|
86
|
+
- maintenance
|
|
87
|
+
- stats
|
|
88
|
+
- gem-patch-report
|
|
89
|
+
ignore_errors: yes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
APP_PATH="{{ gem_patch_report_app_path }}"
|
|
4
|
+
DEPLOY_DIR=$(cd "$APP_PATH" && cd .. && pwd)
|
|
5
|
+
REPO_DIR="$DEPLOY_DIR/repo"
|
|
6
|
+
TEMP_DIR="/tmp/gem-patch-report-$$"
|
|
7
|
+
REPORT_FILE="$TEMP_DIR/report.json"
|
|
8
|
+
OLD_AUDIT_FILE="$TEMP_DIR/audit_old.json"
|
|
9
|
+
CURRENT_AUDIT_FILE="$TEMP_DIR/audit_current.json"
|
|
10
|
+
OLD_GEMFILE_LOCK="$APP_PATH/Gemfile.lock.old"
|
|
11
|
+
|
|
12
|
+
# Create temp directory
|
|
13
|
+
mkdir -p "$TEMP_DIR"
|
|
14
|
+
|
|
15
|
+
# Check if current Gemfile.lock exists
|
|
16
|
+
if [ ! -f "$APP_PATH/Gemfile.lock" ]; then
|
|
17
|
+
echo '[]' > /tmp/gem-patch-report.json
|
|
18
|
+
rm -rf "$TEMP_DIR"
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Update bundle-audit advisory database
|
|
23
|
+
cd "$APP_PATH"
|
|
24
|
+
bundle exec bundle-audit update 2>/dev/null || true
|
|
25
|
+
|
|
26
|
+
# Get old Gemfile.lock from the beginning of the month
|
|
27
|
+
FIRST_OF_MONTH=$(date +"%Y-%m-01")
|
|
28
|
+
OLD_COMMIT=$(git -c safe.directory="$REPO_DIR" --git-dir="$REPO_DIR" log --all --before="$FIRST_OF_MONTH" -1 --format="%H" -- Gemfile.lock 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
if [ -z "$OLD_COMMIT" ]; then
|
|
31
|
+
# No commit before this month, nothing to compare
|
|
32
|
+
echo '[]' > /tmp/gem-patch-report.json
|
|
33
|
+
rm -rf "$TEMP_DIR"
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
git -c safe.directory="$REPO_DIR" --git-dir="$REPO_DIR" show "${OLD_COMMIT}:Gemfile.lock" > "$OLD_GEMFILE_LOCK"
|
|
38
|
+
|
|
39
|
+
# Run bundle-audit on the old Gemfile.lock
|
|
40
|
+
# bundle-audit exits non-zero when vulnerabilities are found, so ignore the exit code
|
|
41
|
+
bundle exec bundle-audit check --gemfile-lock Gemfile.lock.old --format json > "$OLD_AUDIT_FILE" 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
# Run bundle-audit on current Gemfile.lock
|
|
44
|
+
bundle exec bundle-audit check --format json > "$CURRENT_AUDIT_FILE" 2>/dev/null || true
|
|
45
|
+
|
|
46
|
+
# Set environment variables for Ruby script
|
|
47
|
+
export OLD_AUDIT_FILE="$OLD_AUDIT_FILE"
|
|
48
|
+
export CURRENT_AUDIT_FILE="$CURRENT_AUDIT_FILE"
|
|
49
|
+
export CURRENT_GEMFILE_LOCK="$APP_PATH/Gemfile.lock"
|
|
50
|
+
|
|
51
|
+
# Generate patch report using Ruby
|
|
52
|
+
ruby << 'RUBY' > "$REPORT_FILE"
|
|
53
|
+
require 'json'
|
|
54
|
+
require 'set'
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
old_audit = JSON.parse(File.read(ENV['OLD_AUDIT_FILE']))
|
|
58
|
+
current_audit = JSON.parse(File.read(ENV['CURRENT_AUDIT_FILE']))
|
|
59
|
+
|
|
60
|
+
old_results = old_audit['results'] || []
|
|
61
|
+
current_results = current_audit['results'] || []
|
|
62
|
+
|
|
63
|
+
# Build a set of current vulnerability keys (gem name + advisory id)
|
|
64
|
+
current_vuln_keys = current_results
|
|
65
|
+
.select { |r| r['type'] == 'unpatched_gem' && r['gem'] && r['advisory'] }
|
|
66
|
+
.map { |r| "#{r['gem']['name']}:#{r['advisory']['id']}" }
|
|
67
|
+
.to_set
|
|
68
|
+
|
|
69
|
+
# Parse current Gemfile.lock to get current gem versions
|
|
70
|
+
current_versions = {}
|
|
71
|
+
gemfile_lock = File.read(ENV['CURRENT_GEMFILE_LOCK']) rescue ''
|
|
72
|
+
in_specs = false
|
|
73
|
+
gemfile_lock.each_line do |line|
|
|
74
|
+
if line.strip == 'specs:'
|
|
75
|
+
in_specs = true
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
if in_specs
|
|
79
|
+
if line =~ /^\s{4}(\S+)\s+\(([^)]+)\)/
|
|
80
|
+
current_versions[$1] = $2
|
|
81
|
+
elsif line !~ /^\s/
|
|
82
|
+
in_specs = false
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Only include gems that were vulnerable at start of month but are now patched
|
|
88
|
+
patched_gems = old_results
|
|
89
|
+
.select { |r| r['type'] == 'unpatched_gem' && r['gem'] && r['advisory'] }
|
|
90
|
+
.reject { |r| current_vuln_keys.include?("#{r['gem']['name']}:#{r['advisory']['id']}") }
|
|
91
|
+
.map { |r|
|
|
92
|
+
{
|
|
93
|
+
'name' => r['gem']['name'],
|
|
94
|
+
'version' => r['gem']['version'],
|
|
95
|
+
'current_version' => current_versions[r['gem']['name']] || 'unknown',
|
|
96
|
+
'advisory_id' => r['advisory']['id'],
|
|
97
|
+
'criticality' => r['advisory']['criticality']
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
.sort_by { |g| [g['name'], g['advisory_id']] }
|
|
101
|
+
|
|
102
|
+
puts JSON.pretty_generate(patched_gems)
|
|
103
|
+
|
|
104
|
+
rescue JSON::ParserError => e
|
|
105
|
+
puts '[]'
|
|
106
|
+
rescue => e
|
|
107
|
+
puts '[]'
|
|
108
|
+
end
|
|
109
|
+
RUBY
|
|
110
|
+
|
|
111
|
+
# Copy report to known location and clean up
|
|
112
|
+
cp "$REPORT_FILE" /tmp/gem-patch-report.json
|
|
113
|
+
rm -rf "$TEMP_DIR"
|
|
114
|
+
rm -f "$OLD_GEMFILE_LOCK"
|
data/lib/subspace/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: subspace
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Samson
|
|
@@ -153,6 +153,10 @@ files:
|
|
|
153
153
|
- ansible/roles/delayed_job/tasks/main.yml
|
|
154
154
|
- ansible/roles/delayed_job/templates/delayed-job-monit-rc
|
|
155
155
|
- ansible/roles/delayed_job/templates/delayed-job-systemd.service
|
|
156
|
+
- ansible/roles/gem-patch-report/README.md
|
|
157
|
+
- ansible/roles/gem-patch-report/defaults/main.yml
|
|
158
|
+
- ansible/roles/gem-patch-report/tasks/main.yml
|
|
159
|
+
- ansible/roles/gem-patch-report/templates/gem-patch-report.sh.j2
|
|
156
160
|
- ansible/roles/letsencrypt/defaults/main.yml
|
|
157
161
|
- ansible/roles/letsencrypt/tasks/legacy.yml
|
|
158
162
|
- ansible/roles/letsencrypt/tasks/main.yml
|