gem_guard 0.1.7 → 0.1.10
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/cli.rb +110 -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 +1 -0
- data/test_nokogiri.lock +13 -0
- metadata +3 -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: 0370d291f988c0082519f85fdfaffc6fd2acfad9150f91e1c0c66e4d913fd2c0
|
4
|
+
data.tar.gz: 81378c1f38167cccd7ff0ed7f6fb8399556afe40bb29421d2d50601e5aafed91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f07731cbc1d4b3dffe6494a4749dfac41cd31849df39e5e712b3735322135131a59f8dd06aaa790c39104d632637d050e3a25e58a9b92c9441accde55a90126e
|
7
|
+
data.tar.gz: 5906908cb03aac3227f78b16cc49b5e8a7b78756361b809840c1b8de9a5dfb3bc5d138954a3703f051e2ee62e8eaea11cb047e1073882c5b2bf3f2715a36468e
|
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,
|
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
|
@@ -165,9 +207,7 @@ module GemGuard
|
|
165
207
|
return analysis unless severity_threshold
|
166
208
|
|
167
209
|
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
|
210
|
+
config.meets_severity_threshold?(extract_severity_level(vuln_dep.vulnerability.severity))
|
171
211
|
end
|
172
212
|
|
173
213
|
# Create new analysis with filtered vulnerabilities
|
@@ -193,12 +233,75 @@ module GemGuard
|
|
193
233
|
end
|
194
234
|
|
195
235
|
def capture_report_output(analysis, format)
|
236
|
+
output = StringIO.new
|
196
237
|
old_stdout = $stdout
|
197
|
-
$stdout =
|
238
|
+
$stdout = output
|
239
|
+
|
198
240
|
Reporter.new.report(analysis, format: format)
|
199
|
-
|
200
|
-
ensure
|
241
|
+
|
201
242
|
$stdout = old_stdout
|
243
|
+
output.string
|
244
|
+
end
|
245
|
+
|
246
|
+
def display_typosquat_results(suspicious_gems, format)
|
247
|
+
if suspicious_gems.empty?
|
248
|
+
puts "No potential typosquat dependencies found."
|
249
|
+
return
|
250
|
+
end
|
251
|
+
|
252
|
+
case format.downcase
|
253
|
+
when "json"
|
254
|
+
puts JSON.pretty_generate(suspicious_gems)
|
255
|
+
else
|
256
|
+
display_typosquat_table(suspicious_gems)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def display_typosquat_table(suspicious_gems)
|
261
|
+
puts "\n🚨 Potential Typosquat Dependencies Found:"
|
262
|
+
puts "=" * 80
|
263
|
+
|
264
|
+
suspicious_gems.each do |gem_info|
|
265
|
+
risk_emoji = case gem_info[:risk_level]
|
266
|
+
when "critical" then "🔴"
|
267
|
+
when "high" then "🟠"
|
268
|
+
when "medium" then "🟡"
|
269
|
+
else "🟢"
|
270
|
+
end
|
271
|
+
|
272
|
+
puts "\n#{risk_emoji} #{gem_info[:gem_name]} (#{gem_info[:version]})"
|
273
|
+
puts " Suspected target: #{gem_info[:suspected_target]}"
|
274
|
+
puts " Similarity: #{(gem_info[:similarity_score] * 100).round(1)}%"
|
275
|
+
puts " Risk level: #{gem_info[:risk_level].upcase}"
|
276
|
+
puts " Target downloads: #{number_with_commas(gem_info[:target_downloads])}"
|
277
|
+
end
|
278
|
+
|
279
|
+
puts "\n" + "=" * 80
|
280
|
+
puts "💡 Review these dependencies carefully. Consider:"
|
281
|
+
puts " • Verifying the gem's legitimacy on rubygems.org"
|
282
|
+
puts " • Checking the gem's source code repository"
|
283
|
+
puts " • Looking for official documentation or endorsements"
|
284
|
+
puts " • Comparing with the suspected target gem"
|
285
|
+
end
|
286
|
+
|
287
|
+
def format_typosquat_output(suspicious_gems, format)
|
288
|
+
case format.downcase
|
289
|
+
when "json"
|
290
|
+
JSON.pretty_generate(suspicious_gems)
|
291
|
+
else
|
292
|
+
output = StringIO.new
|
293
|
+
old_stdout = $stdout
|
294
|
+
$stdout = output
|
295
|
+
|
296
|
+
display_typosquat_table(suspicious_gems)
|
297
|
+
|
298
|
+
$stdout = old_stdout
|
299
|
+
output.string
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def number_with_commas(number)
|
304
|
+
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
202
305
|
end
|
203
306
|
end
|
204
307
|
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
@@ -4,6 +4,7 @@ require_relative "gem_guard/vulnerability_fetcher"
|
|
4
4
|
require_relative "gem_guard/analyzer"
|
5
5
|
require_relative "gem_guard/reporter"
|
6
6
|
require_relative "gem_guard/sbom_generator"
|
7
|
+
require_relative "gem_guard/typosquat_checker"
|
7
8
|
require_relative "gem_guard/config"
|
8
9
|
require_relative "gem_guard/cli"
|
9
10
|
|
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: 0.1.
|
4
|
+
version: 0.1.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilbur Suero
|
@@ -96,7 +96,6 @@ 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
|
@@ -105,12 +104,14 @@ files:
|
|
105
104
|
- lib/gem_guard/parser.rb
|
106
105
|
- lib/gem_guard/reporter.rb
|
107
106
|
- lib/gem_guard/sbom_generator.rb
|
107
|
+
- lib/gem_guard/typosquat_checker.rb
|
108
108
|
- lib/gem_guard/version.rb
|
109
109
|
- lib/gem_guard/vulnerability_fetcher.rb
|
110
110
|
- plan.md
|
111
111
|
- templates/circleci-config.yml
|
112
112
|
- templates/github-actions.yml
|
113
113
|
- templates/gitlab-ci.yml
|
114
|
+
- test_nokogiri.lock
|
114
115
|
homepage: https://github.com/wilburhimself/gem_guard
|
115
116
|
licenses:
|
116
117
|
- MIT
|
data/gem_guard-0.1.0.gem
DELETED
Binary file
|