ace-git-secrets 0.13.0
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 +7 -0
- data/.ace-defaults/git-secrets/config.yml +63 -0
- data/.ace-defaults/git-secrets/gitleaks.toml +14 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-git-secrets.yml +10 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-secrets.yml +19 -0
- data/CHANGELOG.md +298 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +16 -0
- data/docs/demo/ace-git-secrets-getting-started.gif +0 -0
- data/docs/demo/ace-git-secrets-getting-started.tape.yml +38 -0
- data/docs/demo/fixtures/README.md +3 -0
- data/docs/demo/fixtures/sample.txt +1 -0
- data/docs/getting-started.md +109 -0
- data/docs/handbook.md +43 -0
- data/docs/usage.md +301 -0
- data/exe/ace-git-secrets +19 -0
- data/handbook/agents/security-audit.ag.md +237 -0
- data/handbook/guides/security/ruby.md +27 -0
- data/handbook/guides/security/rust.md +51 -0
- data/handbook/guides/security/typescript.md +33 -0
- data/handbook/guides/security.g.md +155 -0
- data/handbook/skills/as-git-security-audit/SKILL.md +29 -0
- data/handbook/skills/as-git-token-remediation/SKILL.md +21 -0
- data/handbook/workflow-instructions/git/security-audit.wf.md +247 -0
- data/handbook/workflow-instructions/git/token-remediation.wf.md +294 -0
- data/lib/ace/git/secrets/atoms/gitleaks_runner.rb +244 -0
- data/lib/ace/git/secrets/atoms/service_api_client.rb +188 -0
- data/lib/ace/git/secrets/cli/commands/check_release.rb +41 -0
- data/lib/ace/git/secrets/cli/commands/revoke.rb +44 -0
- data/lib/ace/git/secrets/cli/commands/rewrite.rb +46 -0
- data/lib/ace/git/secrets/cli/commands/scan.rb +51 -0
- data/lib/ace/git/secrets/cli.rb +75 -0
- data/lib/ace/git/secrets/commands/check_release_command.rb +48 -0
- data/lib/ace/git/secrets/commands/revoke_command.rb +199 -0
- data/lib/ace/git/secrets/commands/rewrite_command.rb +147 -0
- data/lib/ace/git/secrets/commands/scan_command.rb +113 -0
- data/lib/ace/git/secrets/models/detected_token.rb +129 -0
- data/lib/ace/git/secrets/models/revocation_result.rb +119 -0
- data/lib/ace/git/secrets/models/scan_report.rb +402 -0
- data/lib/ace/git/secrets/molecules/git_rewriter.rb +199 -0
- data/lib/ace/git/secrets/molecules/history_scanner.rb +155 -0
- data/lib/ace/git/secrets/molecules/token_revoker.rb +100 -0
- data/lib/ace/git/secrets/organisms/history_cleaner.rb +201 -0
- data/lib/ace/git/secrets/organisms/release_gate.rb +133 -0
- data/lib/ace/git/secrets/organisms/security_auditor.rb +220 -0
- data/lib/ace/git/secrets/version.rb +9 -0
- data/lib/ace/git/secrets.rb +168 -0
- metadata +227 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
title: Token Remediation Workflow
|
|
4
|
+
purpose: security remediation workflow instruction
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-03-04
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Token Remediation Workflow
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Execute a complete security remediation workflow to detect, revoke, and remove leaked authentication tokens from Git history using ace-git-secrets.
|
|
15
|
+
|
|
16
|
+
## Context
|
|
17
|
+
|
|
18
|
+
ace-git-secrets provides:
|
|
19
|
+
- Token detection via gitleaks (required)
|
|
20
|
+
- Token revocation via GitHub Credential Revocation API
|
|
21
|
+
- History rewriting via git-filter-repo
|
|
22
|
+
- Pre-release security gates for CI/CD
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- Git repository with push access
|
|
27
|
+
- `gitleaks` installed (`brew install gitleaks`) - required for token detection
|
|
28
|
+
- For history rewriting: `git-filter-repo` installed (`brew install git-filter-repo`)
|
|
29
|
+
- Clean working directory (no uncommitted changes)
|
|
30
|
+
|
|
31
|
+
## Variables
|
|
32
|
+
|
|
33
|
+
- `$mode`: Workflow mode - `scan-only` (detection only) or `full` (complete remediation)
|
|
34
|
+
|
|
35
|
+
## Instructions
|
|
36
|
+
|
|
37
|
+
### Phase 1: Detection
|
|
38
|
+
|
|
39
|
+
1. **Scan Git history for tokens:**
|
|
40
|
+
```bash
|
|
41
|
+
# Full history scan (saves report to .ace-local/git-secrets/)
|
|
42
|
+
ace-git-secrets scan
|
|
43
|
+
|
|
44
|
+
# Scan with verbose table output
|
|
45
|
+
ace-git-secrets scan --verbose
|
|
46
|
+
|
|
47
|
+
# Scan since specific date (faster for large repos)
|
|
48
|
+
ace-git-secrets scan --since "2024-01-01"
|
|
49
|
+
|
|
50
|
+
# Scan high confidence tokens only
|
|
51
|
+
ace-git-secrets scan --confidence high
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The scan automatically saves a JSON report with raw token values to
|
|
55
|
+
`.ace-local/git-secrets/<timestamp>-report.json`. This report can be
|
|
56
|
+
reused for revocation and history rewriting without rescanning.
|
|
57
|
+
|
|
58
|
+
2. **Review scan results:**
|
|
59
|
+
- Check token types detected (github_pat, anthropic_api_key, aws_access_key, etc.)
|
|
60
|
+
- Verify confidence levels (high, medium, low)
|
|
61
|
+
- Note commit hashes and file paths for context
|
|
62
|
+
- Identify false positives
|
|
63
|
+
|
|
64
|
+
### Phase 1.5: Token Analysis & Categorization
|
|
65
|
+
|
|
66
|
+
3. **Read the providers report for unique token summary:**
|
|
67
|
+
|
|
68
|
+
The scan generates two report files:
|
|
69
|
+
- `<timestamp>-report.json` - Full details of all occurrences
|
|
70
|
+
- `<timestamp>-providers.md` - Unique tokens grouped by provider
|
|
71
|
+
|
|
72
|
+
Read the providers report to understand unique tokens and their locations.
|
|
73
|
+
|
|
74
|
+
4. **Categorize each token by file path pattern:**
|
|
75
|
+
|
|
76
|
+
| Category | Path Patterns | Recommendation |
|
|
77
|
+
|----------|---------------|----------------|
|
|
78
|
+
| **A: Test Fixtures** | `**/test/**`, `**/spec/**`, `**/__tests__/**`, `**/fixtures/**` | Whitelist (fake tokens) |
|
|
79
|
+
| **B: Recorded Keys** | `**/cassettes/**`, `**/vcr/**`, `**/recordings/**` | Verify revoked, then whitelist |
|
|
80
|
+
| **C: Real Credentials** | `.env`, `config/**`, `secrets/**`, `src/**`, `lib/**` | IMMEDIATE REVOCATION |
|
|
81
|
+
| **D: Legacy/Backup** | `*.backup*/**`, `*.bak`, `_legacy/**`, `archive/**` | Review, cleanup, whitelist |
|
|
82
|
+
| **E: Documentation** | `**/docs/**`, `**/*.md` | Whitelist (examples) |
|
|
83
|
+
|
|
84
|
+
5. **For Category A (Test Fixtures):**
|
|
85
|
+
- These are fake tokens used in tests
|
|
86
|
+
- Add to whitelist, no revocation needed:
|
|
87
|
+
```yaml
|
|
88
|
+
# .ace/git-secrets/config.yml
|
|
89
|
+
whitelist:
|
|
90
|
+
- file: "**/test/**"
|
|
91
|
+
reason: "Unit test fixtures"
|
|
92
|
+
- file: "**/spec/**"
|
|
93
|
+
reason: "RSpec test files"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
6. **For Category B (Recorded API Keys in VCR cassettes):**
|
|
97
|
+
- These may contain real API keys recorded during tests
|
|
98
|
+
- List unique keys and verify each is revoked at provider:
|
|
99
|
+
- GCP: https://console.cloud.google.com/apis/credentials
|
|
100
|
+
- Anthropic: https://console.anthropic.com/account/keys
|
|
101
|
+
- OpenAI: https://platform.openai.com/api-keys
|
|
102
|
+
- GitHub: https://github.com/settings/tokens
|
|
103
|
+
- Only add to whitelist AFTER confirming revocation
|
|
104
|
+
|
|
105
|
+
7. **For Category C (Real Credentials):**
|
|
106
|
+
- These require IMMEDIATE action
|
|
107
|
+
- Proceed to Phase 2 Revocation without delay
|
|
108
|
+
- Do NOT whitelist these
|
|
109
|
+
|
|
110
|
+
8. **For Category D (Legacy/Backup):**
|
|
111
|
+
- Consider removing legacy directories from git entirely
|
|
112
|
+
- If keeping, verify any real keys are revoked first
|
|
113
|
+
- Add to whitelist after cleanup
|
|
114
|
+
|
|
115
|
+
9. **Generate recommendations summary:**
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
## Token Analysis Summary
|
|
119
|
+
|
|
120
|
+
Category A (Test Fixtures): [N] tokens → whitelist
|
|
121
|
+
Category B (Recorded Keys): [N] tokens → verify revoked, whitelist
|
|
122
|
+
Category C (Real Creds): [N] tokens → REVOKE IMMEDIATELY
|
|
123
|
+
Category D (Legacy): [N] tokens → review + cleanup
|
|
124
|
+
Category E (Docs): [N] tokens → whitelist
|
|
125
|
+
|
|
126
|
+
Recommended whitelist patterns:
|
|
127
|
+
- [pattern]: [reason]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
10. **If `$mode` is `scan-only`, stop here and report analysis with recommendations.**
|
|
131
|
+
|
|
132
|
+
### Phase 2: Revocation (CRITICAL - Do First)
|
|
133
|
+
|
|
134
|
+
11. **Revoke tokens immediately** (before they can be exploited):
|
|
135
|
+
```bash
|
|
136
|
+
# Revoke all detected tokens (runs fresh scan)
|
|
137
|
+
ace-git-secrets revoke
|
|
138
|
+
|
|
139
|
+
# Revoke from saved scan file (faster, no rescan needed)
|
|
140
|
+
ace-git-secrets revoke --scan-file .ace-local/git-secrets/<timestamp>-report.json
|
|
141
|
+
|
|
142
|
+
# Revoke specific service only
|
|
143
|
+
ace-git-secrets revoke --service github
|
|
144
|
+
|
|
145
|
+
# Revoke specific token directly
|
|
146
|
+
ace-git-secrets revoke --token "ghp_..."
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Tip:** Using `--scan-file` from a previous scan is faster and ensures
|
|
150
|
+
you're revoking the exact tokens that were detected.
|
|
151
|
+
|
|
152
|
+
12. **Verify revocation:**
|
|
153
|
+
- Check revocation results for each token
|
|
154
|
+
- Note any tokens that couldn't be revoked (unsupported services)
|
|
155
|
+
- For unsupported services, manually revoke via provider dashboards:
|
|
156
|
+
- GitHub: https://github.com/settings/tokens
|
|
157
|
+
- Anthropic: https://console.anthropic.com/account/keys
|
|
158
|
+
- OpenAI: https://platform.openai.com/api-keys
|
|
159
|
+
- AWS: https://console.aws.amazon.com/iam
|
|
160
|
+
|
|
161
|
+
### Phase 3: History Rewriting
|
|
162
|
+
|
|
163
|
+
13. **Create backup before rewriting:**
|
|
164
|
+
```bash
|
|
165
|
+
# Create backup clone (CRITICAL)
|
|
166
|
+
git clone --mirror . ../repo-backup-$(date +%Y%m%d)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
14. **Preview history rewrite (dry run):**
|
|
170
|
+
```bash
|
|
171
|
+
ace-git-secrets rewrite-history --dry-run
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
15. **Execute history rewrite:**
|
|
175
|
+
```bash
|
|
176
|
+
# Interactive mode (requires confirmation, runs fresh scan)
|
|
177
|
+
ace-git-secrets rewrite-history
|
|
178
|
+
|
|
179
|
+
# From saved scan file (faster, consistent with revocation)
|
|
180
|
+
ace-git-secrets rewrite-history --scan-file .ace-local/git-secrets/<timestamp>-report.json
|
|
181
|
+
|
|
182
|
+
# Force mode (skip confirmation - use with caution)
|
|
183
|
+
ace-git-secrets rewrite-history --force
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Tip:** Use the same `--scan-file` used for revocation to ensure
|
|
187
|
+
the exact same tokens are removed from history.
|
|
188
|
+
|
|
189
|
+
16. **Verify history is clean:**
|
|
190
|
+
```bash
|
|
191
|
+
# Rescan to confirm tokens removed
|
|
192
|
+
ace-git-secrets scan
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Phase 4: Push and Notify
|
|
196
|
+
|
|
197
|
+
17. **Force push cleaned history:**
|
|
198
|
+
```bash
|
|
199
|
+
# Force push with lease (safer)
|
|
200
|
+
git push --force-with-lease origin main
|
|
201
|
+
|
|
202
|
+
# Push all branches if needed
|
|
203
|
+
git push --force-with-lease --all origin
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
18. **Notify collaborators:**
|
|
207
|
+
- All collaborators must re-clone or reset their local copies
|
|
208
|
+
- Anyone who fetched the compromised history should check for token exposure
|
|
209
|
+
- Consider rotating any tokens that may have been cached locally
|
|
210
|
+
|
|
211
|
+
19. **Update documentation:**
|
|
212
|
+
- Document incident and remediation in security log
|
|
213
|
+
- Update .gitignore to prevent future leaks
|
|
214
|
+
- Consider adding pre-commit hooks
|
|
215
|
+
|
|
216
|
+
## Post-Remediation Actions
|
|
217
|
+
|
|
218
|
+
### Prevent Future Leaks
|
|
219
|
+
|
|
220
|
+
1. **Add pre-commit integration:**
|
|
221
|
+
```bash
|
|
222
|
+
# Add to .pre-commit-config.yaml
|
|
223
|
+
ace-git-secrets check-release --strict
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
2. **Configure CI/CD gate:**
|
|
227
|
+
```yaml
|
|
228
|
+
# In CI workflow
|
|
229
|
+
- name: Security Check
|
|
230
|
+
run: ace-git-secrets check-release --strict
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
3. **Add patterns to .gitignore:**
|
|
234
|
+
```
|
|
235
|
+
.env
|
|
236
|
+
.env.local
|
|
237
|
+
*.pem
|
|
238
|
+
*_rsa
|
|
239
|
+
credentials.json
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Whitelist False Positives
|
|
243
|
+
|
|
244
|
+
If the scan detected false positives, add them to configuration:
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
# .ace/git-secrets/config.yml
|
|
248
|
+
whitelist:
|
|
249
|
+
- pattern: 'ghp_example_for_docs'
|
|
250
|
+
reason: Documentation example
|
|
251
|
+
- file: 'test/fixtures/mock_tokens.json'
|
|
252
|
+
reason: Test fixtures
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Options Reference
|
|
256
|
+
|
|
257
|
+
### Scan Command
|
|
258
|
+
- `--since DATE`: Scan commits after date
|
|
259
|
+
- `--confidence LEVEL`: Minimum confidence (low, medium, high)
|
|
260
|
+
- `--format FORMAT`: Stdout format when --verbose is used (table, json, yaml)
|
|
261
|
+
- `--report-format FORMAT`: Format for saved report file (json, markdown)
|
|
262
|
+
- `--verbose`: Enable verbose output with full report to stdout
|
|
263
|
+
- `--quiet`: Suppress non-essential output (for CI)
|
|
264
|
+
|
|
265
|
+
### Revoke Command
|
|
266
|
+
- `--scan-file FILE`: Revoke tokens from scan results file
|
|
267
|
+
- `--service SERVICE`: Revoke only specific service (github, anthropic, openai)
|
|
268
|
+
- `--token TOKEN`: Revoke specific token
|
|
269
|
+
|
|
270
|
+
### Rewrite Command
|
|
271
|
+
- `--dry-run`: Preview changes without modifying history
|
|
272
|
+
- `--force`: Skip confirmation prompt
|
|
273
|
+
- `--no-backup`: Skip backup (not recommended)
|
|
274
|
+
- `--scan-file FILE`: Use tokens from scan results file
|
|
275
|
+
|
|
276
|
+
### Check-Release Command
|
|
277
|
+
- `--strict`: Fail on medium confidence tokens too
|
|
278
|
+
- `--format FORMAT`: Output format (table, json)
|
|
279
|
+
|
|
280
|
+
## Success Criteria
|
|
281
|
+
|
|
282
|
+
- All detected tokens are revoked
|
|
283
|
+
- Git history no longer contains any tokens
|
|
284
|
+
- Force push completed successfully
|
|
285
|
+
- Collaborators notified and re-cloned
|
|
286
|
+
- Pre-commit/CI integration added to prevent future leaks
|
|
287
|
+
|
|
288
|
+
## Response Template
|
|
289
|
+
|
|
290
|
+
**Scan Results:** [Number of tokens found, by type and confidence]
|
|
291
|
+
**Revocation Status:** [Tokens revoked / total, any failures]
|
|
292
|
+
**History Rewrite:** [Commits modified, files cleaned]
|
|
293
|
+
**Push Status:** [Branch(es) pushed, any conflicts]
|
|
294
|
+
**Status:** Complete | Partial (manual action needed for [reason])
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Git
|
|
9
|
+
module Secrets
|
|
10
|
+
module Atoms
|
|
11
|
+
# Runner for gitleaks external tool
|
|
12
|
+
# Handles gitleaks availability detection and execution
|
|
13
|
+
#
|
|
14
|
+
# Gitleaks is REQUIRED for ace-git-secrets. The gem focuses on
|
|
15
|
+
# remediation (revocation, history rewriting) while delegating
|
|
16
|
+
# detection to gitleaks which has 100+ actively maintained patterns.
|
|
17
|
+
class GitleaksRunner
|
|
18
|
+
# Error raised when gitleaks is not installed
|
|
19
|
+
class GitleaksNotFoundError < StandardError; end
|
|
20
|
+
|
|
21
|
+
attr_reader :config_path
|
|
22
|
+
|
|
23
|
+
# @param config_path [String, nil] Path to gitleaks config file
|
|
24
|
+
def initialize(config_path: nil)
|
|
25
|
+
@config_path = config_path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if gitleaks is available in PATH
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def self.available?
|
|
31
|
+
system("which gitleaks > /dev/null 2>&1")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Ensure gitleaks is available, raising error if not
|
|
35
|
+
# @raise [GitleaksNotFoundError] If gitleaks is not installed
|
|
36
|
+
def self.ensure_available!
|
|
37
|
+
return if available?
|
|
38
|
+
|
|
39
|
+
raise GitleaksNotFoundError,
|
|
40
|
+
"gitleaks is required but not installed. Install with: brew install gitleaks"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Instance method for backward compatibility
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def available?
|
|
46
|
+
self.class.available?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get gitleaks version
|
|
50
|
+
# @return [String, nil] Version string or nil if not available
|
|
51
|
+
def version
|
|
52
|
+
return nil unless available?
|
|
53
|
+
|
|
54
|
+
stdout, _status = Open3.capture2("gitleaks version")
|
|
55
|
+
stdout.strip
|
|
56
|
+
rescue
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if gitleaks version is compatible (8.0+)
|
|
61
|
+
# ace-git-secrets requires gitleaks 8.x for the `git` subcommand and JSON report format
|
|
62
|
+
# @return [Boolean] true if version is compatible
|
|
63
|
+
def compatible_version?
|
|
64
|
+
ver = version
|
|
65
|
+
return false unless ver
|
|
66
|
+
|
|
67
|
+
# Extract major version from strings like "v8.18.4" or "8.18.4"
|
|
68
|
+
match = ver.match(/v?(\d+)\./)
|
|
69
|
+
return false unless match
|
|
70
|
+
|
|
71
|
+
major = match[1].to_i
|
|
72
|
+
major >= 8
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Ensure gitleaks version is compatible
|
|
76
|
+
# @raise [GitleaksNotFoundError] If gitleaks is incompatible
|
|
77
|
+
def self.ensure_compatible!
|
|
78
|
+
runner = new
|
|
79
|
+
return if runner.compatible_version?
|
|
80
|
+
|
|
81
|
+
ver = runner.version || "unknown"
|
|
82
|
+
raise GitleaksNotFoundError,
|
|
83
|
+
"gitleaks version #{ver} is not compatible. Version 8.0+ is required. " \
|
|
84
|
+
"Upgrade with: brew upgrade gitleaks"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Run gitleaks scan on current files (no git history)
|
|
88
|
+
# @param path [String] Path to scan
|
|
89
|
+
# @param verbose [Boolean] Enable verbose output
|
|
90
|
+
# @return [Hash] Scan results with :success, :findings, :output keys
|
|
91
|
+
def scan_files(path: ".", verbose: false)
|
|
92
|
+
run_gitleaks(path: path, no_git: true, verbose: verbose)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Run gitleaks scan on git history
|
|
96
|
+
# @param path [String] Path to repository
|
|
97
|
+
# @param since [String, nil] Start commit for scanning
|
|
98
|
+
# @param verbose [Boolean] Enable verbose output
|
|
99
|
+
# @return [Hash] Scan results with :success, :findings, :output keys
|
|
100
|
+
def scan_history(path: ".", since: nil, verbose: false)
|
|
101
|
+
run_gitleaks(path: path, no_git: false, since: since, verbose: verbose)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Execute gitleaks command
|
|
107
|
+
# @param path [String] Path to scan
|
|
108
|
+
# @param no_git [Boolean] Whether to skip git history (scan files only)
|
|
109
|
+
# @param since [String, nil] Start commit for git history scan
|
|
110
|
+
# @param verbose [Boolean] Enable verbose output
|
|
111
|
+
# @return [Hash] Results hash
|
|
112
|
+
def run_gitleaks(path:, no_git: false, since: nil, verbose: false)
|
|
113
|
+
unless available?
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
skipped: true,
|
|
117
|
+
message: "Gitleaks not installed - skipping. Install with: brew install gitleaks",
|
|
118
|
+
findings: []
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Use temp file for JSON report (gitleaks 8.x doesn't output JSON to stdout)
|
|
123
|
+
Tempfile.create(["gitleaks-report", ".json"]) do |report_file|
|
|
124
|
+
cmd = build_command(path: path, no_git: no_git, since: since, verbose: verbose, report_path: report_file.path)
|
|
125
|
+
|
|
126
|
+
# Use array form to avoid shell injection - stderr captured separately
|
|
127
|
+
_, stderr, status = Open3.capture3(*cmd)
|
|
128
|
+
|
|
129
|
+
# Read JSON from temp file
|
|
130
|
+
json_output = begin
|
|
131
|
+
File.read(report_file.path)
|
|
132
|
+
rescue
|
|
133
|
+
""
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
parse_results(json_output, stderr, status)
|
|
137
|
+
end
|
|
138
|
+
rescue => e
|
|
139
|
+
{
|
|
140
|
+
success: false,
|
|
141
|
+
skipped: false,
|
|
142
|
+
message: "Gitleaks execution failed: #{e.message}",
|
|
143
|
+
findings: []
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Build gitleaks command as array (safe from shell injection)
|
|
148
|
+
# @return [Array<String>]
|
|
149
|
+
def build_command(path:, no_git:, since:, verbose:, report_path:)
|
|
150
|
+
# Use gitleaks git for history scanning, detect for file-only scanning
|
|
151
|
+
cmd = if no_git
|
|
152
|
+
["gitleaks", "detect", "--no-git"]
|
|
153
|
+
else
|
|
154
|
+
["gitleaks", "git"]
|
|
155
|
+
end
|
|
156
|
+
cmd << path
|
|
157
|
+
cmd << "--report-format=json"
|
|
158
|
+
cmd << "--report-path=#{report_path}"
|
|
159
|
+
cmd << "--log-opts=--since=#{since}" if since && !no_git && valid_since_format?(since)
|
|
160
|
+
if config_path
|
|
161
|
+
cmd << "--config"
|
|
162
|
+
cmd << config_path
|
|
163
|
+
end
|
|
164
|
+
cmd << "--verbose" if verbose
|
|
165
|
+
cmd
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Validate since parameter format to prevent injection
|
|
169
|
+
# Accepts: dates (YYYY-MM-DD, "30 days ago", etc.), commit SHAs (hex)
|
|
170
|
+
# Rejects: anything with shell metacharacters or git option flags
|
|
171
|
+
# @param since [String] The since parameter to validate
|
|
172
|
+
# @return [Boolean] true if format is safe
|
|
173
|
+
def valid_since_format?(since)
|
|
174
|
+
return false if since.nil? || since.empty?
|
|
175
|
+
|
|
176
|
+
# Reject if contains shell metacharacters or starts with dash (could be option)
|
|
177
|
+
return false if since.match?(/[;|&$`\\<>]/)
|
|
178
|
+
return false if since.start_with?("-")
|
|
179
|
+
|
|
180
|
+
# Accept common formats:
|
|
181
|
+
# - ISO dates: 2024-01-01
|
|
182
|
+
# - Relative dates: "30 days ago", "2 weeks ago"
|
|
183
|
+
# - Commit SHAs: abc123, abc123def456
|
|
184
|
+
# - Git date strings: yesterday, last monday
|
|
185
|
+
since.match?(/\A[\w\s\-:.,]+\z/)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Parse gitleaks output
|
|
189
|
+
# @param stdout [String] Standard output
|
|
190
|
+
# @param stderr [String] Standard error
|
|
191
|
+
# @param status [Process::Status] Exit status
|
|
192
|
+
# @return [Hash]
|
|
193
|
+
def parse_results(stdout, stderr, status)
|
|
194
|
+
# Exit code 0 = no leaks found
|
|
195
|
+
# Exit code 1 = leaks found
|
|
196
|
+
# Exit code > 1 = error
|
|
197
|
+
|
|
198
|
+
findings = []
|
|
199
|
+
|
|
200
|
+
if stdout && !stdout.empty? && stdout.start_with?("[")
|
|
201
|
+
begin
|
|
202
|
+
findings = JSON.parse(stdout)
|
|
203
|
+
rescue JSON::ParserError => e
|
|
204
|
+
# Not valid JSON - gitleaks may have crashed or output unexpected text
|
|
205
|
+
# Log debug info when DEBUG env is set
|
|
206
|
+
if ENV["DEBUG"]
|
|
207
|
+
warn "[DEBUG] gitleaks JSON parse error: #{e.message}"
|
|
208
|
+
warn "[DEBUG] stdout (first 500 chars): #{stdout[0, 500].inspect}"
|
|
209
|
+
warn "[DEBUG] stderr: #{stderr.inspect}" unless stderr.to_s.empty?
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
{
|
|
215
|
+
success: status.exitstatus <= 1,
|
|
216
|
+
clean: status.exitstatus == 0,
|
|
217
|
+
skipped: false,
|
|
218
|
+
message: (status.exitstatus == 0) ? "No secrets detected" : "#{findings.size} secret(s) detected",
|
|
219
|
+
findings: findings.map { |f| normalize_finding(f) },
|
|
220
|
+
raw_output: stdout,
|
|
221
|
+
stderr: stderr
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Normalize gitleaks finding to our format
|
|
226
|
+
# @param finding [Hash] Raw gitleaks finding
|
|
227
|
+
# @return [Hash]
|
|
228
|
+
def normalize_finding(finding)
|
|
229
|
+
{
|
|
230
|
+
pattern_name: finding["RuleID"] || finding["ruleID"] || "unknown",
|
|
231
|
+
token_type: finding["RuleID"] || finding["ruleID"] || "unknown",
|
|
232
|
+
confidence: "high", # Gitleaks findings are high confidence
|
|
233
|
+
matched_value: finding["Secret"] || finding["secret"] || "",
|
|
234
|
+
file_path: finding["File"] || finding["file"] || "",
|
|
235
|
+
line_number: finding["StartLine"] || finding["startLine"],
|
|
236
|
+
commit_hash: finding["Commit"] || finding["commit"] || "",
|
|
237
|
+
description: finding["Description"] || finding["description"] || ""
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|