gem_guard 0.1.7 → 1.1.2
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/README.md +316 -33
- data/SECURITY.md +46 -2
- data/lib/gem_guard/analyzer.rb +4 -1
- data/lib/gem_guard/auto_fixer.rb +162 -0
- data/lib/gem_guard/cli.rb +191 -7
- data/lib/gem_guard/parser.rb +3 -1
- data/lib/gem_guard/typosquat_checker.rb +157 -0
- data/lib/gem_guard/version.rb +1 -1
- data/lib/gem_guard/vulnerability_fetcher.rb +25 -1
- data/lib/gem_guard.rb +4 -2
- data/test_nokogiri.lock +13 -0
- data/test_nokogiri.lock.backup.20250810_002252 +13 -0
- metadata +5 -2
- data/gem_guard-0.1.0.gem +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2870725c07a4b4c39c53e01c031663a934db6f9e457ad75f509d2ee6e3e35684
|
4
|
+
data.tar.gz: 6f630c2ef6939a0c5de8c3b8982781a907065b8c1a3901483f1101910387e244
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b7c598cd6789dfdf5d3b90c82fc9f2ade69c730418f1d2a9f6ba6c22555fad01b7e4bbd438bbb03d6116f429176a407e9b0f13221c47fd2cd0e9e2175563e98
|
7
|
+
data.tar.gz: 822e03ed7fa56e17d170abf92db8677fb27f92ff04d0f52ecdc1793c73db9aab3052e37e6cbcb28b7a217f3a30a7fb062a3860169f908067b2c3672964d9f977
|
data/README.md
CHANGED
@@ -6,15 +6,41 @@
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
7
7
|
[](SECURITY.md)
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
-
|
17
|
-
-
|
9
|
+
**The comprehensive Ruby dependency security scanner and SBOM generator.**
|
10
|
+
|
11
|
+
GemGuard is your one-stop solution for Ruby supply chain security. Detect vulnerabilities, identify typosquats, generate SBOMs, and secure your dependencies with enterprise-grade tooling designed for modern DevOps workflows.
|
12
|
+
|
13
|
+
## ✨ Features
|
14
|
+
|
15
|
+
### 🔍 **Vulnerability Scanning**
|
16
|
+
- Detect known CVEs from OSV.dev and Ruby Advisory Database
|
17
|
+
- Smart deduplication handles platform-specific gems
|
18
|
+
- Severity-based filtering and thresholds
|
19
|
+
- Actionable fix recommendations with exact commands
|
20
|
+
|
21
|
+
### 🎯 **Typosquat Detection**
|
22
|
+
- Fuzzy matching against popular Ruby gems
|
23
|
+
- Configurable similarity thresholds
|
24
|
+
- Risk level classification (Critical/High/Medium/Low)
|
25
|
+
- Hardcoded fallback for reliable detection
|
26
|
+
|
27
|
+
### 📋 **SBOM Generation**
|
28
|
+
- Industry-standard SPDX 2.3 format
|
29
|
+
- CycloneDX 1.5 support
|
30
|
+
- Complete dependency metadata
|
31
|
+
- License and checksum information
|
32
|
+
|
33
|
+
### 🚀 **CI/CD Integration**
|
34
|
+
- Configurable exit codes for pipeline control
|
35
|
+
- JSON output for automated processing
|
36
|
+
- Config file support (`.gemguard.yml`)
|
37
|
+
- Multiple output formats and file export
|
38
|
+
|
39
|
+
### 🎨 **Developer Experience**
|
40
|
+
- Beautiful, colorful terminal output
|
41
|
+
- Progress indicators and clear error messages
|
42
|
+
- Comprehensive help and documentation
|
43
|
+
- Zero-config operation with sensible defaults
|
18
44
|
|
19
45
|
## Installation
|
20
46
|
|
@@ -32,30 +58,47 @@ Or install it yourself as:
|
|
32
58
|
|
33
59
|
$ gem install gem_guard
|
34
60
|
|
35
|
-
##
|
61
|
+
## 🚀 Quick Start
|
62
|
+
|
63
|
+
```bash
|
64
|
+
# Install GemGuard
|
65
|
+
gem install gem_guard
|
66
|
+
|
67
|
+
# Scan for vulnerabilities
|
68
|
+
gem_guard scan
|
69
|
+
|
70
|
+
# Check for typosquats
|
71
|
+
gem_guard typosquat
|
36
72
|
|
37
|
-
|
73
|
+
# Generate SBOM
|
74
|
+
gem_guard sbom
|
75
|
+
```
|
76
|
+
|
77
|
+
## 📖 Usage
|
38
78
|
|
39
|
-
|
79
|
+
### 🔍 Vulnerability Scanning
|
40
80
|
|
81
|
+
**Basic scan:**
|
41
82
|
```bash
|
42
83
|
gem_guard scan
|
43
84
|
```
|
44
85
|
|
45
|
-
|
46
|
-
|
86
|
+
**Custom lockfile:**
|
47
87
|
```bash
|
48
88
|
gem_guard scan --lockfile path/to/Gemfile.lock
|
49
89
|
```
|
50
90
|
|
51
|
-
|
52
|
-
|
91
|
+
**JSON output for automation:**
|
53
92
|
```bash
|
54
|
-
gem_guard scan --format json
|
93
|
+
gem_guard scan --format json --output vulnerabilities.json
|
55
94
|
```
|
56
95
|
|
57
|
-
|
96
|
+
**CI/CD integration with exit codes:**
|
97
|
+
```bash
|
98
|
+
gem_guard scan --fail-on-vulnerabilities --severity-threshold high
|
99
|
+
```
|
58
100
|
|
101
|
+
**Example output:**
|
59
102
|
```
|
60
103
|
🚨 Security Vulnerabilities Found
|
61
104
|
==================================================
|
@@ -66,17 +109,208 @@ Summary:
|
|
66
109
|
|
67
110
|
Details:
|
68
111
|
|
69
|
-
📦
|
70
|
-
🔍 Vulnerability:
|
71
|
-
⚠️ Severity:
|
72
|
-
📝 Summary:
|
73
|
-
🔧 Fix: bundle update
|
112
|
+
📦 nokogiri (1.18.8)
|
113
|
+
🔍 Vulnerability: GHSA-353f-x4gh-cqq8
|
114
|
+
⚠️ Severity: UNKNOWN
|
115
|
+
📝 Summary: Nokogiri patches vendored libxml2 to resolve multiple CVEs
|
116
|
+
🔧 Fix: bundle update nokogiri --to 1.18.9
|
117
|
+
|
118
|
+
📦 thor (1.3.2)
|
119
|
+
🔍 Vulnerability: GHSA-mqcp-p2hv-vw6x
|
120
|
+
⚠️ Severity: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
|
121
|
+
📝 Summary: Thor can construct an unsafe shell command from library input.
|
122
|
+
🔧 Fix: bundle update thor --to 1.4.0
|
123
|
+
```
|
124
|
+
|
125
|
+
### 🎯 Typosquat Detection
|
126
|
+
|
127
|
+
**Basic typosquat check:**
|
128
|
+
```bash
|
129
|
+
gem_guard typosquat
|
130
|
+
```
|
131
|
+
|
132
|
+
**Custom similarity threshold:**
|
133
|
+
```bash
|
134
|
+
gem_guard typosquat --threshold 0.9
|
135
|
+
```
|
136
|
+
|
137
|
+
**JSON output:**
|
138
|
+
```bash
|
139
|
+
gem_guard typosquat --format json --output typosquats.json
|
140
|
+
```
|
141
|
+
|
142
|
+
**Example output:**
|
143
|
+
```
|
144
|
+
🎯 Potential Typosquat Dependencies Found
|
145
|
+
==========================================
|
146
|
+
|
147
|
+
📦 railz (7.0.0)
|
148
|
+
🚨 Risk Level: CRITICAL
|
149
|
+
📊 Similarity: 80.0% to 'rails'
|
150
|
+
⚠️ This gem name is suspiciously similar to the popular gem 'rails'
|
151
|
+
🔧 Consider: Did you mean 'rails'? Review this dependency carefully.
|
152
|
+
```
|
153
|
+
|
154
|
+
### 📋 SBOM Generation
|
74
155
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
156
|
+
**Generate SPDX SBOM:**
|
157
|
+
```bash
|
158
|
+
gem_guard sbom
|
159
|
+
```
|
160
|
+
|
161
|
+
**Generate CycloneDX SBOM:**
|
162
|
+
```bash
|
163
|
+
gem_guard sbom --format cyclone-dx
|
164
|
+
```
|
165
|
+
|
166
|
+
**Custom project name and output:**
|
167
|
+
```bash
|
168
|
+
gem_guard sbom --project my-app --output sbom.json
|
169
|
+
```
|
170
|
+
|
171
|
+
**Example SPDX output:**
|
172
|
+
```json
|
173
|
+
{
|
174
|
+
"spdxVersion": "SPDX-2.3",
|
175
|
+
"dataLicense": "CC0-1.0",
|
176
|
+
"SPDXID": "SPDXRef-DOCUMENT",
|
177
|
+
"name": "my-app-sbom",
|
178
|
+
"documentNamespace": "https://gem-guard.dev/my-app/2025-01-09T23:55:00Z",
|
179
|
+
"creationInfo": {
|
180
|
+
"created": "2025-01-09T23:55:00Z",
|
181
|
+
"creators": ["Tool: gem_guard-1.0.0"]
|
182
|
+
},
|
183
|
+
"packages": [...],
|
184
|
+
"relationships": [...]
|
185
|
+
}
|
186
|
+
```
|
187
|
+
|
188
|
+
## ⚙️ Configuration
|
189
|
+
|
190
|
+
GemGuard supports project-level configuration via `.gemguard.yml`:
|
191
|
+
|
192
|
+
```yaml
|
193
|
+
# .gemguard.yml
|
194
|
+
lockfile_path: "Gemfile.lock"
|
195
|
+
output_format: "table" # table, json
|
196
|
+
fail_on_vulnerabilities: true
|
197
|
+
severity_threshold: "medium" # low, medium, high, critical
|
198
|
+
output_file: null
|
199
|
+
ignore_vulnerabilities:
|
200
|
+
- "CVE-2021-12345" # Ignore specific CVEs
|
201
|
+
- "GHSA-xxxx-xxxx-xxxx"
|
202
|
+
typosquat:
|
203
|
+
similarity_threshold: 0.8
|
204
|
+
enabled: true
|
205
|
+
sbom:
|
206
|
+
format: "spdx" # spdx, cyclone-dx
|
207
|
+
project_name: "my-project"
|
208
|
+
```
|
209
|
+
|
210
|
+
### Configuration Options
|
211
|
+
|
212
|
+
| Option | Description | Default |
|
213
|
+
|--------|-------------|---------|
|
214
|
+
| `lockfile_path` | Path to Gemfile.lock | `"Gemfile.lock"` |
|
215
|
+
| `output_format` | Output format (table/json) | `"table"` |
|
216
|
+
| `fail_on_vulnerabilities` | Exit with code 1 if vulnerabilities found | `true` |
|
217
|
+
| `severity_threshold` | Minimum severity to report | `"low"` |
|
218
|
+
| `output_file` | Write output to file | `null` |
|
219
|
+
| `ignore_vulnerabilities` | List of CVE/GHSA IDs to ignore | `[]` |
|
220
|
+
| `typosquat.similarity_threshold` | Typosquat detection sensitivity | `0.8` |
|
221
|
+
| `typosquat.enabled` | Enable typosquat detection | `true` |
|
222
|
+
| `sbom.format` | SBOM format (spdx/cyclone-dx) | `"spdx"` |
|
223
|
+
| `sbom.project_name` | Project name in SBOM | `"ruby-project"` |
|
224
|
+
|
225
|
+
## 🔄 CI/CD Integration
|
226
|
+
|
227
|
+
### Exit Codes
|
228
|
+
|
229
|
+
GemGuard uses standard exit codes for CI/CD integration:
|
230
|
+
|
231
|
+
- **0**: Success (no vulnerabilities or typosquats found)
|
232
|
+
- **1**: Vulnerabilities/typosquats found
|
233
|
+
- **2**: Error (invalid arguments, missing files, etc.)
|
234
|
+
|
235
|
+
### GitHub Actions
|
236
|
+
|
237
|
+
```yaml
|
238
|
+
name: Security Scan
|
239
|
+
on: [push, pull_request]
|
240
|
+
|
241
|
+
jobs:
|
242
|
+
security:
|
243
|
+
runs-on: ubuntu-latest
|
244
|
+
steps:
|
245
|
+
- uses: actions/checkout@v4
|
246
|
+
- uses: ruby/setup-ruby@v1
|
247
|
+
with:
|
248
|
+
ruby-version: '3.2'
|
249
|
+
bundler-cache: true
|
250
|
+
|
251
|
+
- name: Install GemGuard
|
252
|
+
run: gem install gem_guard
|
253
|
+
|
254
|
+
- name: Vulnerability Scan
|
255
|
+
run: gem_guard scan --format json --output vulnerabilities.json
|
256
|
+
|
257
|
+
- name: Typosquat Check
|
258
|
+
run: gem_guard typosquat --format json --output typosquats.json
|
259
|
+
|
260
|
+
- name: Generate SBOM
|
261
|
+
run: gem_guard sbom --output sbom.json
|
262
|
+
|
263
|
+
- name: Upload Security Reports
|
264
|
+
uses: actions/upload-artifact@v4
|
265
|
+
if: always()
|
266
|
+
with:
|
267
|
+
name: security-reports
|
268
|
+
path: |
|
269
|
+
vulnerabilities.json
|
270
|
+
typosquats.json
|
271
|
+
sbom.json
|
272
|
+
```
|
273
|
+
|
274
|
+
### GitLab CI
|
275
|
+
|
276
|
+
```yaml
|
277
|
+
security_scan:
|
278
|
+
stage: test
|
279
|
+
image: ruby:3.2
|
280
|
+
before_script:
|
281
|
+
- bundle install
|
282
|
+
- gem install gem_guard
|
283
|
+
script:
|
284
|
+
- gem_guard scan --format json --output vulnerabilities.json
|
285
|
+
- gem_guard typosquat --format json --output typosquats.json
|
286
|
+
- gem_guard sbom --output sbom.json
|
287
|
+
artifacts:
|
288
|
+
reports:
|
289
|
+
# GitLab can parse these for security dashboard
|
290
|
+
dependency_scanning: vulnerabilities.json
|
291
|
+
paths:
|
292
|
+
- "*.json"
|
293
|
+
when: always
|
294
|
+
allow_failure: false
|
295
|
+
```
|
296
|
+
|
297
|
+
### CircleCI
|
298
|
+
|
299
|
+
```yaml
|
300
|
+
version: 2.1
|
301
|
+
jobs:
|
302
|
+
security:
|
303
|
+
docker:
|
304
|
+
- image: cimg/ruby:3.2
|
305
|
+
steps:
|
306
|
+
- checkout
|
307
|
+
- run: bundle install
|
308
|
+
- run: gem install gem_guard
|
309
|
+
- run: gem_guard scan --fail-on-vulnerabilities
|
310
|
+
- run: gem_guard typosquat
|
311
|
+
- run: gem_guard sbom --output sbom.json
|
312
|
+
- store_artifacts:
|
313
|
+
path: sbom.json
|
80
314
|
```
|
81
315
|
|
82
316
|
## Development
|
@@ -106,14 +340,63 @@ Releases are automated via GitHub Actions. To create a new release:
|
|
106
340
|
|
107
341
|
The release workflow is triggered only when `lib/gem_guard/version.rb` changes.
|
108
342
|
|
109
|
-
## Contributing
|
343
|
+
## 🤝 Contributing
|
344
|
+
|
345
|
+
We welcome contributions! Here's how you can help:
|
346
|
+
|
347
|
+
1. **Fork the repository**
|
348
|
+
2. **Create a feature branch** (`git checkout -b feature/amazing-feature`)
|
349
|
+
3. **Write tests** for your changes (we use strict TDD)
|
350
|
+
4. **Run the test suite** (`bundle exec rspec`)
|
351
|
+
5. **Run the linter** (`bundle exec rake standard`)
|
352
|
+
6. **Commit your changes** (`git commit -am 'Add amazing feature'`)
|
353
|
+
7. **Push to the branch** (`git push origin feature/amazing-feature`)
|
354
|
+
8. **Open a Pull Request**
|
355
|
+
|
356
|
+
### Development Guidelines
|
357
|
+
|
358
|
+
- Follow DHH-style Ruby: pragmatic, intention-revealing, minimal abstractions
|
359
|
+
- Use strict outside-in TDD with RSpec
|
360
|
+
- Maintain 100% test coverage
|
361
|
+
- Follow StandardRB for code style
|
362
|
+
- Write clear, descriptive commit messages
|
110
363
|
|
111
|
-
|
364
|
+
## 📊 Roadmap
|
112
365
|
|
113
|
-
|
366
|
+
- [ ] **Enhanced Vulnerability Sources**: Additional security databases
|
367
|
+
- [ ] **Auto-Fix Suggestions**: Automated dependency updates
|
368
|
+
- [ ] **Web Dashboard**: Browser-based security monitoring
|
369
|
+
- [ ] **IDE Integrations**: VS Code, RubyMine plugins
|
370
|
+
- [ ] **Slack/Teams Notifications**: Real-time security alerts
|
371
|
+
- [ ] **Custom Rules Engine**: User-defined security policies
|
372
|
+
|
373
|
+
## 🏆 Why GemGuard?
|
374
|
+
|
375
|
+
| Feature | GemGuard | bundler-audit | Other Tools |
|
376
|
+
|---------|----------|---------------|-------------|
|
377
|
+
| **Vulnerability Scanning** | ✅ OSV.dev + Ruby Advisory | ✅ Ruby Advisory Only | ❌ Limited Sources |
|
378
|
+
| **Typosquat Detection** | ✅ Fuzzy Matching | ❌ | ❌ |
|
379
|
+
| **SBOM Generation** | ✅ SPDX + CycloneDX | ❌ | ❌ |
|
380
|
+
| **CI/CD Integration** | ✅ Full Support | ⚠️ Basic | ⚠️ Limited |
|
381
|
+
| **JSON Output** | ✅ | ✅ | ⚠️ Varies |
|
382
|
+
| **Configuration Files** | ✅ | ❌ | ⚠️ Limited |
|
383
|
+
| **Platform Deduplication** | ✅ | ❌ | ❌ |
|
384
|
+
| **Active Development** | ✅ | ⚠️ Maintenance | ⚠️ Varies |
|
385
|
+
|
386
|
+
## 📄 License
|
114
387
|
|
115
388
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
116
389
|
|
117
|
-
## Security
|
390
|
+
## 🔒 Security
|
391
|
+
|
392
|
+
If you discover a security vulnerability within GemGuard, please see our [Security Policy](SECURITY.md) for responsible disclosure guidelines.
|
393
|
+
|
394
|
+
## 🙏 Acknowledgments
|
395
|
+
|
396
|
+
- [OSV.dev](https://osv.dev/) for comprehensive vulnerability data
|
397
|
+
- [Ruby Advisory Database](https://github.com/rubysec/ruby-advisory-db) for Ruby-specific advisories
|
398
|
+
- The Ruby community for continuous feedback and contributions
|
399
|
+
|
400
|
+
---
|
118
401
|
|
119
|
-
|
402
|
+
**Made with ❤️ for the Ruby community**
|
data/SECURITY.md
CHANGED
@@ -46,8 +46,52 @@ GemGuard itself implements several security best practices:
|
|
46
46
|
|
47
47
|
When using GemGuard:
|
48
48
|
|
49
|
-
- Keep GemGuard updated to the latest version
|
50
|
-
- Review vulnerability reports carefully before applying fixes
|
49
|
+
- **Keep GemGuard updated** to the latest version for security patches
|
50
|
+
- **Review vulnerability reports** carefully before applying fixes
|
51
|
+
- **Validate SBOM outputs** before sharing with external parties
|
52
|
+
- **Use secure channels** when transmitting security reports
|
53
|
+
- **Configure ignore lists** carefully to avoid missing critical vulnerabilities
|
54
|
+
- **Monitor CI/CD pipelines** for security scan failures
|
55
|
+
|
56
|
+
## Threat Model
|
57
|
+
|
58
|
+
GemGuard protects against:
|
59
|
+
|
60
|
+
- **Known Vulnerabilities**: CVEs in your dependency chain
|
61
|
+
- **Typosquat Attacks**: Malicious gems with similar names to popular packages
|
62
|
+
- **Supply Chain Attacks**: Compromised or malicious dependencies
|
63
|
+
- **Outdated Dependencies**: Gems with known security issues
|
64
|
+
|
65
|
+
## Data Handling
|
66
|
+
|
67
|
+
GemGuard:
|
68
|
+
|
69
|
+
- **Does not collect** personal or sensitive data
|
70
|
+
- **Queries public APIs** (OSV.dev) for vulnerability information
|
71
|
+
- **Processes locally** your Gemfile.lock and dependency information
|
72
|
+
- **Does not transmit** your code or proprietary information
|
73
|
+
- **Caches vulnerability data** temporarily for performance
|
74
|
+
|
75
|
+
## Security Updates
|
76
|
+
|
77
|
+
We provide security updates through:
|
78
|
+
|
79
|
+
- **GitHub Security Advisories** for critical vulnerabilities
|
80
|
+
- **RubyGems.org releases** with security patches
|
81
|
+
- **Email notifications** to security@wilburhimself.com subscribers
|
82
|
+
- **GitHub releases** with detailed changelogs
|
83
|
+
|
84
|
+
## Contact
|
85
|
+
|
86
|
+
For security-related inquiries:
|
87
|
+
|
88
|
+
- **Email**: security@wilburhimself.com
|
89
|
+
- **PGP Key**: Available upon request
|
90
|
+
- **Response Time**: 48 hours for initial response
|
91
|
+
|
92
|
+
---
|
93
|
+
|
94
|
+
*Last updated: January 2025*
|
51
95
|
- Use GemGuard in your CI/CD pipeline to catch vulnerabilities early
|
52
96
|
- Consider the source and severity of reported vulnerabilities
|
53
97
|
|
data/lib/gem_guard/analyzer.rb
CHANGED
@@ -8,7 +8,10 @@ module GemGuard
|
|
8
8
|
|
9
9
|
next if matching_vulns.empty?
|
10
10
|
|
11
|
-
|
11
|
+
# Deduplicate vulnerabilities by ID to avoid duplicate entries for the same vulnerability
|
12
|
+
unique_vulns = matching_vulns.uniq { |vuln| vuln.id }
|
13
|
+
|
14
|
+
unique_vulns.each do |vulnerability|
|
12
15
|
if version_affected?(dependency.version, vulnerability)
|
13
16
|
vulnerable_dependencies << VulnerableDependency.new(
|
14
17
|
dependency: dependency,
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require "bundler"
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
module GemGuard
|
5
|
+
class AutoFixer
|
6
|
+
def initialize(lockfile_path = "Gemfile.lock", gemfile_path = "Gemfile")
|
7
|
+
@lockfile_path = lockfile_path
|
8
|
+
@gemfile_path = gemfile_path
|
9
|
+
@backup_created = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def fix_vulnerabilities(vulnerable_dependencies, options = {})
|
13
|
+
dry_run = options.fetch(:dry_run, false)
|
14
|
+
interactive = options.fetch(:interactive, false)
|
15
|
+
create_backup = options.fetch(:backup, true)
|
16
|
+
|
17
|
+
unless File.exist?(@gemfile_path)
|
18
|
+
raise "Gemfile not found at #{@gemfile_path}. Auto-fix requires a Gemfile."
|
19
|
+
end
|
20
|
+
|
21
|
+
unless File.exist?(@lockfile_path)
|
22
|
+
raise "Gemfile.lock not found at #{@lockfile_path}. Run 'bundle install' first."
|
23
|
+
end
|
24
|
+
|
25
|
+
fixes = plan_fixes(vulnerable_dependencies)
|
26
|
+
|
27
|
+
if fixes.empty?
|
28
|
+
return {status: :no_fixes_needed, message: "No automatic fixes available."}
|
29
|
+
end
|
30
|
+
|
31
|
+
if dry_run
|
32
|
+
return {status: :dry_run, fixes: fixes, message: "Dry run completed. #{fixes.length} fixes planned."}
|
33
|
+
end
|
34
|
+
|
35
|
+
if interactive && !confirm_fixes(fixes)
|
36
|
+
return {status: :cancelled, message: "Fix operation cancelled by user."}
|
37
|
+
end
|
38
|
+
|
39
|
+
create_lockfile_backup if create_backup
|
40
|
+
|
41
|
+
applied_fixes = apply_fixes(fixes)
|
42
|
+
|
43
|
+
{
|
44
|
+
status: :completed,
|
45
|
+
fixes: applied_fixes,
|
46
|
+
message: "Applied #{applied_fixes.length} fixes successfully."
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def plan_fixes(vulnerable_dependencies)
|
53
|
+
fixes = []
|
54
|
+
|
55
|
+
vulnerable_dependencies.each do |vuln_dep|
|
56
|
+
dependency = vuln_dep.dependency
|
57
|
+
vulnerability = vuln_dep.vulnerability
|
58
|
+
|
59
|
+
# Extract the recommended version from the fix suggestion
|
60
|
+
recommended_version = extract_version_from_fix(vuln_dep.recommended_fix)
|
61
|
+
|
62
|
+
next unless recommended_version
|
63
|
+
|
64
|
+
# Check if the recommended version is available and safe
|
65
|
+
if version_available_and_safe?(dependency.name, recommended_version)
|
66
|
+
fixes << {
|
67
|
+
gem_name: dependency.name,
|
68
|
+
current_version: dependency.version,
|
69
|
+
target_version: recommended_version,
|
70
|
+
vulnerability_id: vulnerability.id,
|
71
|
+
severity: vulnerability.severity
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
fixes
|
77
|
+
end
|
78
|
+
|
79
|
+
def extract_version_from_fix(fix_command)
|
80
|
+
# Extract version from commands like "bundle update nokogiri --to 1.18.9"
|
81
|
+
match = fix_command.match(/--to\s+([^\s]+)/)
|
82
|
+
match ? match[1] : nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def version_available_and_safe?(gem_name, version)
|
86
|
+
# Check if the version exists on RubyGems
|
87
|
+
# This is a simplified check - in production, you might want more robust validation
|
88
|
+
return false if version.nil? || version.empty?
|
89
|
+
|
90
|
+
# Basic semantic version validation
|
91
|
+
version.match?(/^\d+\.\d+(\.\d+)?/)
|
92
|
+
end
|
93
|
+
|
94
|
+
def confirm_fixes(fixes)
|
95
|
+
puts "\n🔧 Planned Fixes:"
|
96
|
+
puts "=" * 50
|
97
|
+
|
98
|
+
fixes.each do |fix|
|
99
|
+
severity_emoji = severity_emoji(fix[:severity])
|
100
|
+
puts "#{severity_emoji} #{fix[:gem_name]}: #{fix[:current_version]} → #{fix[:target_version]}"
|
101
|
+
puts " Fixes: #{fix[:vulnerability_id]}"
|
102
|
+
end
|
103
|
+
|
104
|
+
puts "\n⚠️ This will modify your Gemfile.lock and may require bundle install."
|
105
|
+
print "Do you want to proceed? (y/N): "
|
106
|
+
|
107
|
+
response = $stdin.gets.chomp.downcase
|
108
|
+
response == "y" || response == "yes"
|
109
|
+
end
|
110
|
+
|
111
|
+
def severity_emoji(severity)
|
112
|
+
case severity.to_s.downcase
|
113
|
+
when /critical/
|
114
|
+
"🔴"
|
115
|
+
when /high/
|
116
|
+
"🟠"
|
117
|
+
when /medium/
|
118
|
+
"🟡"
|
119
|
+
else
|
120
|
+
"🟢"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_lockfile_backup
|
125
|
+
return if @backup_created
|
126
|
+
|
127
|
+
backup_path = "#{@lockfile_path}.backup.#{Time.now.strftime("%Y%m%d_%H%M%S")}"
|
128
|
+
FileUtils.cp(@lockfile_path, backup_path)
|
129
|
+
@backup_created = true
|
130
|
+
puts "📦 Created backup: #{backup_path}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def apply_fixes(fixes)
|
134
|
+
applied_fixes = []
|
135
|
+
|
136
|
+
fixes.each do |fix|
|
137
|
+
if apply_single_fix(fix)
|
138
|
+
applied_fixes << fix
|
139
|
+
puts "✅ Updated #{fix[:gem_name]} to #{fix[:target_version]}"
|
140
|
+
else
|
141
|
+
puts "❌ Failed to update #{fix[:gem_name]}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Run bundle install to update the lockfile
|
146
|
+
if applied_fixes.any?
|
147
|
+
puts "\n🔄 Running bundle install to update lockfile..."
|
148
|
+
system("bundle install")
|
149
|
+
end
|
150
|
+
|
151
|
+
applied_fixes
|
152
|
+
end
|
153
|
+
|
154
|
+
def apply_single_fix(fix)
|
155
|
+
# Use bundler to update the specific gem
|
156
|
+
command = "bundle update #{fix[:gem_name]} --conservative"
|
157
|
+
|
158
|
+
# Execute the bundle update command
|
159
|
+
system(command, out: File::NULL, err: File::NULL)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/gem_guard/cli.rb
CHANGED
@@ -98,8 +98,50 @@ module GemGuard
|
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
101
|
+
desc "typosquat", "Check for potential typosquat dependencies"
|
102
|
+
option :lockfile, type: :string, default: "Gemfile.lock", desc: "Path to Gemfile.lock"
|
103
|
+
option :format, type: :string, default: "table", desc: "Output format (table, json)"
|
104
|
+
option :output, type: :string, desc: "Output file path"
|
105
|
+
option :config, type: :string, desc: "Config file path"
|
106
|
+
def typosquat
|
107
|
+
config = Config.new(options[:config] || ".gemguard.yml")
|
108
|
+
|
109
|
+
lockfile_path = options[:lockfile] || config.lockfile_path
|
110
|
+
format = options[:format] || config.output_format
|
111
|
+
output_file = options[:output] || config.output_file
|
112
|
+
|
113
|
+
unless File.exist?(lockfile_path)
|
114
|
+
puts "Error: #{lockfile_path} not found"
|
115
|
+
exit EXIT_ERROR
|
116
|
+
end
|
117
|
+
|
118
|
+
begin
|
119
|
+
dependencies = Parser.new.parse(lockfile_path)
|
120
|
+
checker = TyposquatChecker.new
|
121
|
+
suspicious_gems = checker.check_dependencies(dependencies)
|
122
|
+
|
123
|
+
if output_file
|
124
|
+
output_content = format_typosquat_output(suspicious_gems, format)
|
125
|
+
File.write(output_file, output_content)
|
126
|
+
puts "Typosquat report written to #{output_file}"
|
127
|
+
else
|
128
|
+
display_typosquat_results(suspicious_gems, format)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Exit with appropriate code
|
132
|
+
if suspicious_gems.any? { |sg| sg[:risk_level] == "critical" || sg[:risk_level] == "high" }
|
133
|
+
exit EXIT_VULNERABILITIES_FOUND
|
134
|
+
else
|
135
|
+
exit EXIT_SUCCESS
|
136
|
+
end
|
137
|
+
rescue => e
|
138
|
+
puts "Error: #{e.message}"
|
139
|
+
exit EXIT_ERROR
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
101
143
|
desc "config", "Manage configuration"
|
102
|
-
option :init, type: :boolean, desc: "Initialize
|
144
|
+
option :init, type: :boolean, desc: "Initialize default config file"
|
103
145
|
option :show, type: :boolean, desc: "Show current configuration"
|
104
146
|
option :path, type: :string, default: ".gemguard.yml", desc: "Config file path"
|
105
147
|
def config
|
@@ -148,6 +190,87 @@ module GemGuard
|
|
148
190
|
end
|
149
191
|
end
|
150
192
|
|
193
|
+
desc "fix", "Automatically fix vulnerable dependencies"
|
194
|
+
option :lockfile, type: :string, desc: "Path to Gemfile.lock"
|
195
|
+
option :gemfile, type: :string, desc: "Path to Gemfile"
|
196
|
+
option :dry_run, type: :boolean, desc: "Show planned fixes without applying them"
|
197
|
+
option :interactive, type: :boolean, desc: "Ask for confirmation before applying fixes"
|
198
|
+
option :no_backup, type: :boolean, desc: "Skip creating backup of Gemfile.lock"
|
199
|
+
option :config, type: :string, desc: "Config file path"
|
200
|
+
def fix
|
201
|
+
config = Config.new(options[:config] || ".gemguard.yml")
|
202
|
+
|
203
|
+
lockfile_path = options[:lockfile] || config.lockfile_path
|
204
|
+
gemfile_path = options[:gemfile] || "Gemfile"
|
205
|
+
dry_run = options[:dry_run] || false
|
206
|
+
interactive = options[:interactive] || false
|
207
|
+
create_backup = !options[:no_backup]
|
208
|
+
|
209
|
+
unless File.exist?(lockfile_path)
|
210
|
+
puts "Error: #{lockfile_path} not found"
|
211
|
+
exit EXIT_ERROR
|
212
|
+
end
|
213
|
+
|
214
|
+
unless File.exist?(gemfile_path)
|
215
|
+
puts "Error: #{gemfile_path} not found. Auto-fix requires a Gemfile."
|
216
|
+
exit EXIT_ERROR
|
217
|
+
end
|
218
|
+
|
219
|
+
begin
|
220
|
+
# First, scan for vulnerabilities
|
221
|
+
dependencies = Parser.new.parse(lockfile_path)
|
222
|
+
vulnerabilities = VulnerabilityFetcher.new.fetch_for(dependencies)
|
223
|
+
analysis = Analyzer.new.analyze(dependencies, vulnerabilities)
|
224
|
+
|
225
|
+
if analysis.vulnerable_dependencies.empty?
|
226
|
+
puts "✅ No vulnerabilities found. Nothing to fix!"
|
227
|
+
exit EXIT_SUCCESS
|
228
|
+
end
|
229
|
+
|
230
|
+
# Apply fixes
|
231
|
+
auto_fixer = AutoFixer.new(lockfile_path, gemfile_path)
|
232
|
+
result = auto_fixer.fix_vulnerabilities(
|
233
|
+
analysis.vulnerable_dependencies,
|
234
|
+
dry_run: dry_run,
|
235
|
+
interactive: interactive,
|
236
|
+
backup: create_backup
|
237
|
+
)
|
238
|
+
|
239
|
+
case result[:status]
|
240
|
+
when :no_fixes_needed
|
241
|
+
puts "ℹ️ #{result[:message]}"
|
242
|
+
exit EXIT_SUCCESS
|
243
|
+
when :dry_run
|
244
|
+
puts "🔍 Dry Run Results:"
|
245
|
+
puts "=" * 40
|
246
|
+
result[:fixes].each do |fix|
|
247
|
+
puts "#{fix[:gem_name]}: #{fix[:current_version]} → #{fix[:target_version]}"
|
248
|
+
puts " Fixes: #{fix[:vulnerability_id]} (#{fix[:severity]})"
|
249
|
+
end
|
250
|
+
puts "\n#{result[:message]}"
|
251
|
+
puts "Run without --dry-run to apply these fixes."
|
252
|
+
exit EXIT_SUCCESS
|
253
|
+
when :cancelled
|
254
|
+
puts "❌ #{result[:message]}"
|
255
|
+
exit EXIT_SUCCESS
|
256
|
+
when :completed
|
257
|
+
puts "🎉 #{result[:message]}"
|
258
|
+
puts "\n📋 Applied Fixes:"
|
259
|
+
result[:fixes].each do |fix|
|
260
|
+
puts "✅ #{fix[:gem_name]}: #{fix[:current_version]} → #{fix[:target_version]}"
|
261
|
+
end
|
262
|
+
puts "\n💡 Run 'gem_guard scan' to verify fixes."
|
263
|
+
exit EXIT_SUCCESS
|
264
|
+
else
|
265
|
+
puts "❌ Unexpected error during fix operation"
|
266
|
+
exit EXIT_ERROR
|
267
|
+
end
|
268
|
+
rescue => e
|
269
|
+
puts "Error: #{e.message}"
|
270
|
+
exit EXIT_ERROR
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
151
274
|
desc "version", "Show gem_guard version"
|
152
275
|
def version
|
153
276
|
puts GemGuard::VERSION
|
@@ -165,9 +288,7 @@ module GemGuard
|
|
165
288
|
return analysis unless severity_threshold
|
166
289
|
|
167
290
|
filtered_vulnerable_deps = analysis.vulnerable_dependencies.select do |vuln_dep|
|
168
|
-
vuln_dep.
|
169
|
-
config.meets_severity_threshold?(extract_severity_level(vuln.severity))
|
170
|
-
end
|
291
|
+
config.meets_severity_threshold?(extract_severity_level(vuln_dep.vulnerability.severity))
|
171
292
|
end
|
172
293
|
|
173
294
|
# Create new analysis with filtered vulnerabilities
|
@@ -193,12 +314,75 @@ module GemGuard
|
|
193
314
|
end
|
194
315
|
|
195
316
|
def capture_report_output(analysis, format)
|
317
|
+
output = StringIO.new
|
196
318
|
old_stdout = $stdout
|
197
|
-
$stdout =
|
319
|
+
$stdout = output
|
320
|
+
|
198
321
|
Reporter.new.report(analysis, format: format)
|
199
|
-
|
200
|
-
ensure
|
322
|
+
|
201
323
|
$stdout = old_stdout
|
324
|
+
output.string
|
325
|
+
end
|
326
|
+
|
327
|
+
def display_typosquat_results(suspicious_gems, format)
|
328
|
+
if suspicious_gems.empty?
|
329
|
+
puts "No potential typosquat dependencies found."
|
330
|
+
return
|
331
|
+
end
|
332
|
+
|
333
|
+
case format.downcase
|
334
|
+
when "json"
|
335
|
+
puts JSON.pretty_generate(suspicious_gems)
|
336
|
+
else
|
337
|
+
display_typosquat_table(suspicious_gems)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def display_typosquat_table(suspicious_gems)
|
342
|
+
puts "\n🚨 Potential Typosquat Dependencies Found:"
|
343
|
+
puts "=" * 80
|
344
|
+
|
345
|
+
suspicious_gems.each do |gem_info|
|
346
|
+
risk_emoji = case gem_info[:risk_level]
|
347
|
+
when "critical" then "🔴"
|
348
|
+
when "high" then "🟠"
|
349
|
+
when "medium" then "🟡"
|
350
|
+
else "🟢"
|
351
|
+
end
|
352
|
+
|
353
|
+
puts "\n#{risk_emoji} #{gem_info[:gem_name]} (#{gem_info[:version]})"
|
354
|
+
puts " Suspected target: #{gem_info[:suspected_target]}"
|
355
|
+
puts " Similarity: #{(gem_info[:similarity_score] * 100).round(1)}%"
|
356
|
+
puts " Risk level: #{gem_info[:risk_level].upcase}"
|
357
|
+
puts " Target downloads: #{number_with_commas(gem_info[:target_downloads])}"
|
358
|
+
end
|
359
|
+
|
360
|
+
puts "\n" + "=" * 80
|
361
|
+
puts "💡 Review these dependencies carefully. Consider:"
|
362
|
+
puts " • Verifying the gem's legitimacy on rubygems.org"
|
363
|
+
puts " • Checking the gem's source code repository"
|
364
|
+
puts " • Looking for official documentation or endorsements"
|
365
|
+
puts " • Comparing with the suspected target gem"
|
366
|
+
end
|
367
|
+
|
368
|
+
def format_typosquat_output(suspicious_gems, format)
|
369
|
+
case format.downcase
|
370
|
+
when "json"
|
371
|
+
JSON.pretty_generate(suspicious_gems)
|
372
|
+
else
|
373
|
+
output = StringIO.new
|
374
|
+
old_stdout = $stdout
|
375
|
+
$stdout = output
|
376
|
+
|
377
|
+
display_typosquat_table(suspicious_gems)
|
378
|
+
|
379
|
+
$stdout = old_stdout
|
380
|
+
output.string
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def number_with_commas(number)
|
385
|
+
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
202
386
|
end
|
203
387
|
end
|
204
388
|
end
|
data/lib/gem_guard/parser.rb
CHANGED
@@ -0,0 +1,157 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module GemGuard
|
6
|
+
class TyposquatChecker
|
7
|
+
POPULAR_GEMS_CACHE_TTL = 3600 # 1 hour
|
8
|
+
SIMILARITY_THRESHOLD = 0.8
|
9
|
+
MIN_POPULAR_GEM_DOWNLOADS = 1_000_000
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@popular_gems_cache = nil
|
13
|
+
@cache_timestamp = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_dependencies(dependencies)
|
17
|
+
suspicious_gems = []
|
18
|
+
popular_gems = fetch_popular_gems
|
19
|
+
|
20
|
+
dependencies.each do |dependency|
|
21
|
+
suspicious_match = find_suspicious_match(dependency.name, popular_gems)
|
22
|
+
if suspicious_match
|
23
|
+
suspicious_gems << {
|
24
|
+
gem_name: dependency.name,
|
25
|
+
version: dependency.version,
|
26
|
+
suspected_target: suspicious_match[:name],
|
27
|
+
similarity_score: suspicious_match[:similarity],
|
28
|
+
target_downloads: suspicious_match[:downloads],
|
29
|
+
risk_level: calculate_risk_level(suspicious_match[:similarity])
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
suspicious_gems
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def fetch_popular_gems
|
40
|
+
return @popular_gems_cache if cache_valid?
|
41
|
+
|
42
|
+
begin
|
43
|
+
# Use fallback popular gems list for now since RubyGems API structure is complex
|
44
|
+
# In a production environment, you might want to use a different data source
|
45
|
+
# or scrape the RubyGems.org popular page
|
46
|
+
@popular_gems_cache = fallback_popular_gems
|
47
|
+
@cache_timestamp = Time.now
|
48
|
+
@popular_gems_cache
|
49
|
+
rescue
|
50
|
+
# Silently fall back to hardcoded list - no need to warn user
|
51
|
+
fallback_popular_gems
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def cache_valid?
|
56
|
+
@popular_gems_cache && @cache_timestamp &&
|
57
|
+
(Time.now - @cache_timestamp) < POPULAR_GEMS_CACHE_TTL
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_suspicious_match(gem_name, popular_gems)
|
61
|
+
return nil if popular_gems.any? { |pg| pg[:name] == gem_name }
|
62
|
+
|
63
|
+
best_match = nil
|
64
|
+
highest_similarity = 0
|
65
|
+
|
66
|
+
popular_gems.each do |popular_gem|
|
67
|
+
similarity = calculate_similarity(gem_name, popular_gem[:name])
|
68
|
+
|
69
|
+
if similarity >= SIMILARITY_THRESHOLD && similarity > highest_similarity
|
70
|
+
highest_similarity = similarity
|
71
|
+
best_match = {
|
72
|
+
name: popular_gem[:name],
|
73
|
+
similarity: similarity,
|
74
|
+
downloads: popular_gem[:downloads]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
best_match
|
80
|
+
end
|
81
|
+
|
82
|
+
def calculate_similarity(str1, str2)
|
83
|
+
return 0.0 if str1.nil? || str2.nil? || str1.empty? || str2.empty?
|
84
|
+
return 1.0 if str1 == str2
|
85
|
+
|
86
|
+
# Use Levenshtein distance for similarity calculation
|
87
|
+
levenshtein_similarity(str1.downcase, str2.downcase)
|
88
|
+
end
|
89
|
+
|
90
|
+
def levenshtein_similarity(str1, str2)
|
91
|
+
distance = levenshtein_distance(str1, str2)
|
92
|
+
max_length = [str1.length, str2.length].max
|
93
|
+
return 1.0 if max_length == 0
|
94
|
+
|
95
|
+
1.0 - (distance.to_f / max_length)
|
96
|
+
end
|
97
|
+
|
98
|
+
def levenshtein_distance(str1, str2)
|
99
|
+
matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
|
100
|
+
|
101
|
+
(0..str1.length).each { |i| matrix[i][0] = i }
|
102
|
+
(0..str2.length).each { |j| matrix[0][j] = j }
|
103
|
+
|
104
|
+
(1..str1.length).each do |i|
|
105
|
+
(1..str2.length).each do |j|
|
106
|
+
cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1
|
107
|
+
matrix[i][j] = [
|
108
|
+
matrix[i - 1][j] + 1, # deletion
|
109
|
+
matrix[i][j - 1] + 1, # insertion
|
110
|
+
matrix[i - 1][j - 1] + cost # substitution
|
111
|
+
].min
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
matrix[str1.length][str2.length]
|
116
|
+
end
|
117
|
+
|
118
|
+
def calculate_risk_level(similarity)
|
119
|
+
case similarity
|
120
|
+
when 0.95..1.0
|
121
|
+
"critical"
|
122
|
+
when 0.9..0.95
|
123
|
+
"high"
|
124
|
+
when 0.85..0.9
|
125
|
+
"medium"
|
126
|
+
else
|
127
|
+
"low"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def fallback_popular_gems
|
132
|
+
# Hardcoded list of very popular Ruby gems as fallback
|
133
|
+
[
|
134
|
+
{name: "rails", downloads: 100_000_000},
|
135
|
+
{name: "bundler", downloads: 90_000_000},
|
136
|
+
{name: "rake", downloads: 80_000_000},
|
137
|
+
{name: "json", downloads: 70_000_000},
|
138
|
+
{name: "minitest", downloads: 60_000_000},
|
139
|
+
{name: "thread_safe", downloads: 50_000_000},
|
140
|
+
{name: "tzinfo", downloads: 45_000_000},
|
141
|
+
{name: "concurrent-ruby", downloads: 40_000_000},
|
142
|
+
{name: "i18n", downloads: 35_000_000},
|
143
|
+
{name: "activesupport", downloads: 30_000_000},
|
144
|
+
{name: "activerecord", downloads: 25_000_000},
|
145
|
+
{name: "actionpack", downloads: 20_000_000},
|
146
|
+
{name: "actionview", downloads: 18_000_000},
|
147
|
+
{name: "activemodel", downloads: 15_000_000},
|
148
|
+
{name: "rspec", downloads: 12_000_000},
|
149
|
+
{name: "puma", downloads: 10_000_000},
|
150
|
+
{name: "nokogiri", downloads: 8_000_000},
|
151
|
+
{name: "thor", downloads: 7_000_000},
|
152
|
+
{name: "sass", downloads: 6_000_000},
|
153
|
+
{name: "devise", downloads: 5_000_000}
|
154
|
+
]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/gem_guard/version.rb
CHANGED
@@ -15,7 +15,31 @@ module GemGuard
|
|
15
15
|
vulnerabilities.concat(fetch_ruby_advisory_vulnerabilities(dependency))
|
16
16
|
end
|
17
17
|
|
18
|
-
vulnerabilities
|
18
|
+
# Deduplicate vulnerabilities by ID and gem name, merging affected/fixed versions
|
19
|
+
deduplicated = {}
|
20
|
+
vulnerabilities.each do |vuln|
|
21
|
+
key = [vuln.id, vuln.gem_name]
|
22
|
+
if deduplicated[key]
|
23
|
+
# Merge affected and fixed versions from duplicate entries
|
24
|
+
existing = deduplicated[key]
|
25
|
+
merged_affected = (existing.affected_versions + vuln.affected_versions).uniq
|
26
|
+
merged_fixed = (existing.fixed_versions + vuln.fixed_versions).uniq
|
27
|
+
|
28
|
+
deduplicated[key] = Vulnerability.new(
|
29
|
+
id: existing.id,
|
30
|
+
gem_name: existing.gem_name,
|
31
|
+
affected_versions: merged_affected,
|
32
|
+
fixed_versions: merged_fixed,
|
33
|
+
severity: existing.severity,
|
34
|
+
summary: existing.summary,
|
35
|
+
details: existing.details
|
36
|
+
)
|
37
|
+
else
|
38
|
+
deduplicated[key] = vuln
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
deduplicated.values
|
19
43
|
end
|
20
44
|
|
21
45
|
private
|
data/lib/gem_guard.rb
CHANGED
@@ -3,9 +3,11 @@ require_relative "gem_guard/parser"
|
|
3
3
|
require_relative "gem_guard/vulnerability_fetcher"
|
4
4
|
require_relative "gem_guard/analyzer"
|
5
5
|
require_relative "gem_guard/reporter"
|
6
|
-
require_relative "gem_guard/sbom_generator"
|
7
|
-
require_relative "gem_guard/config"
|
8
6
|
require_relative "gem_guard/cli"
|
7
|
+
require_relative "gem_guard/config"
|
8
|
+
require_relative "gem_guard/sbom_generator"
|
9
|
+
require_relative "gem_guard/typosquat_checker"
|
10
|
+
require_relative "gem_guard/auto_fixer"
|
9
11
|
|
10
12
|
module GemGuard
|
11
13
|
class Error < StandardError; end
|
data/test_nokogiri.lock
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gem_guard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilbur Suero
|
@@ -96,21 +96,24 @@ files:
|
|
96
96
|
- Rakefile
|
97
97
|
- SECURITY.md
|
98
98
|
- exe/gem_guard
|
99
|
-
- gem_guard-0.1.0.gem
|
100
99
|
- gem_guard.gemspec
|
101
100
|
- lib/gem_guard.rb
|
102
101
|
- lib/gem_guard/analyzer.rb
|
102
|
+
- lib/gem_guard/auto_fixer.rb
|
103
103
|
- lib/gem_guard/cli.rb
|
104
104
|
- lib/gem_guard/config.rb
|
105
105
|
- lib/gem_guard/parser.rb
|
106
106
|
- lib/gem_guard/reporter.rb
|
107
107
|
- lib/gem_guard/sbom_generator.rb
|
108
|
+
- lib/gem_guard/typosquat_checker.rb
|
108
109
|
- lib/gem_guard/version.rb
|
109
110
|
- lib/gem_guard/vulnerability_fetcher.rb
|
110
111
|
- plan.md
|
111
112
|
- templates/circleci-config.yml
|
112
113
|
- templates/github-actions.yml
|
113
114
|
- templates/gitlab-ci.yml
|
115
|
+
- test_nokogiri.lock
|
116
|
+
- test_nokogiri.lock.backup.20250810_002252
|
114
117
|
homepage: https://github.com/wilburhimself/gem_guard
|
115
118
|
licenses:
|
116
119
|
- MIT
|
data/gem_guard-0.1.0.gem
DELETED
Binary file
|