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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/git-secrets/config.yml +63 -0
  3. data/.ace-defaults/git-secrets/gitleaks.toml +14 -0
  4. data/.ace-defaults/nav/protocols/guide-sources/ace-git-secrets.yml +10 -0
  5. data/.ace-defaults/nav/protocols/wfi-sources/ace-git-secrets.yml +19 -0
  6. data/CHANGELOG.md +298 -0
  7. data/LICENSE +21 -0
  8. data/README.md +40 -0
  9. data/Rakefile +16 -0
  10. data/docs/demo/ace-git-secrets-getting-started.gif +0 -0
  11. data/docs/demo/ace-git-secrets-getting-started.tape.yml +38 -0
  12. data/docs/demo/fixtures/README.md +3 -0
  13. data/docs/demo/fixtures/sample.txt +1 -0
  14. data/docs/getting-started.md +109 -0
  15. data/docs/handbook.md +43 -0
  16. data/docs/usage.md +301 -0
  17. data/exe/ace-git-secrets +19 -0
  18. data/handbook/agents/security-audit.ag.md +237 -0
  19. data/handbook/guides/security/ruby.md +27 -0
  20. data/handbook/guides/security/rust.md +51 -0
  21. data/handbook/guides/security/typescript.md +33 -0
  22. data/handbook/guides/security.g.md +155 -0
  23. data/handbook/skills/as-git-security-audit/SKILL.md +29 -0
  24. data/handbook/skills/as-git-token-remediation/SKILL.md +21 -0
  25. data/handbook/workflow-instructions/git/security-audit.wf.md +247 -0
  26. data/handbook/workflow-instructions/git/token-remediation.wf.md +294 -0
  27. data/lib/ace/git/secrets/atoms/gitleaks_runner.rb +244 -0
  28. data/lib/ace/git/secrets/atoms/service_api_client.rb +188 -0
  29. data/lib/ace/git/secrets/cli/commands/check_release.rb +41 -0
  30. data/lib/ace/git/secrets/cli/commands/revoke.rb +44 -0
  31. data/lib/ace/git/secrets/cli/commands/rewrite.rb +46 -0
  32. data/lib/ace/git/secrets/cli/commands/scan.rb +51 -0
  33. data/lib/ace/git/secrets/cli.rb +75 -0
  34. data/lib/ace/git/secrets/commands/check_release_command.rb +48 -0
  35. data/lib/ace/git/secrets/commands/revoke_command.rb +199 -0
  36. data/lib/ace/git/secrets/commands/rewrite_command.rb +147 -0
  37. data/lib/ace/git/secrets/commands/scan_command.rb +113 -0
  38. data/lib/ace/git/secrets/models/detected_token.rb +129 -0
  39. data/lib/ace/git/secrets/models/revocation_result.rb +119 -0
  40. data/lib/ace/git/secrets/models/scan_report.rb +402 -0
  41. data/lib/ace/git/secrets/molecules/git_rewriter.rb +199 -0
  42. data/lib/ace/git/secrets/molecules/history_scanner.rb +155 -0
  43. data/lib/ace/git/secrets/molecules/token_revoker.rb +100 -0
  44. data/lib/ace/git/secrets/organisms/history_cleaner.rb +201 -0
  45. data/lib/ace/git/secrets/organisms/release_gate.rb +133 -0
  46. data/lib/ace/git/secrets/organisms/security_auditor.rb +220 -0
  47. data/lib/ace/git/secrets/version.rb +9 -0
  48. data/lib/ace/git/secrets.rb +168 -0
  49. 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