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,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Secrets
|
|
6
|
+
module Organisms
|
|
7
|
+
# Pre-release security gate
|
|
8
|
+
# Blocks releases if tokens are detected in history
|
|
9
|
+
#
|
|
10
|
+
# Requires gitleaks to be installed: brew install gitleaks
|
|
11
|
+
class ReleaseGate
|
|
12
|
+
attr_reader :scanner, :strict_mode
|
|
13
|
+
|
|
14
|
+
# @param repository_path [String] Path to git repository
|
|
15
|
+
# @param gitleaks_config [String, nil] Path to gitleaks config file
|
|
16
|
+
# @param strict [Boolean] Fail on medium confidence matches too
|
|
17
|
+
# @param exclusions [Array<String>, nil] Glob patterns for files to exclude
|
|
18
|
+
def initialize(repository_path: ".", gitleaks_config: nil, strict: false, exclusions: nil)
|
|
19
|
+
@scanner = Molecules::HistoryScanner.new(
|
|
20
|
+
repository_path: repository_path,
|
|
21
|
+
gitleaks_config: gitleaks_config,
|
|
22
|
+
exclusions: exclusions
|
|
23
|
+
)
|
|
24
|
+
@strict_mode = strict
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Run pre-release security check
|
|
28
|
+
# @return [Hash] Result with :passed, :message, :report keys
|
|
29
|
+
def check
|
|
30
|
+
min_confidence = strict_mode ? "medium" : "high"
|
|
31
|
+
|
|
32
|
+
report = scanner.scan(min_confidence: min_confidence)
|
|
33
|
+
|
|
34
|
+
if report.clean?
|
|
35
|
+
{
|
|
36
|
+
passed: true,
|
|
37
|
+
exit_code: 0,
|
|
38
|
+
message: "Pre-release security check: PASSED",
|
|
39
|
+
summary: "No authentication tokens detected in Git history.",
|
|
40
|
+
report: report
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
{
|
|
44
|
+
passed: false,
|
|
45
|
+
exit_code: 1,
|
|
46
|
+
message: "Pre-release security check: FAILED",
|
|
47
|
+
summary: failure_summary(report),
|
|
48
|
+
report: report,
|
|
49
|
+
remediation: remediation_steps(report)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Format result for CI output
|
|
55
|
+
# @param result [Hash] Check result
|
|
56
|
+
# @param format [String] Output format (table, json)
|
|
57
|
+
# @return [String]
|
|
58
|
+
def format_result(result, format: "table")
|
|
59
|
+
case format
|
|
60
|
+
when "json"
|
|
61
|
+
require "json"
|
|
62
|
+
JSON.pretty_generate({
|
|
63
|
+
passed: result[:passed],
|
|
64
|
+
message: result[:message],
|
|
65
|
+
token_count: result[:report].token_count,
|
|
66
|
+
tokens: result[:report].tokens.map { |t| t.to_h }
|
|
67
|
+
})
|
|
68
|
+
else
|
|
69
|
+
format_table_result(result)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def failure_summary(report)
|
|
76
|
+
lines = []
|
|
77
|
+
lines << "#{report.token_count} authentication token(s) detected in Git history."
|
|
78
|
+
lines << ""
|
|
79
|
+
lines << "Summary by type:"
|
|
80
|
+
|
|
81
|
+
report.token_types.each do |type|
|
|
82
|
+
count = report.tokens.count { |t| t.token_type == type }
|
|
83
|
+
lines << " - #{type}: #{count}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
lines << ""
|
|
87
|
+
lines << "Release blocked until tokens are removed."
|
|
88
|
+
|
|
89
|
+
lines.join("\n")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def remediation_steps(report)
|
|
93
|
+
<<~STEPS
|
|
94
|
+
To fix this issue:
|
|
95
|
+
|
|
96
|
+
1. Review detected tokens:
|
|
97
|
+
ace-git-secrets scan
|
|
98
|
+
|
|
99
|
+
2. Revoke compromised tokens immediately:
|
|
100
|
+
ace-git-secrets revoke
|
|
101
|
+
|
|
102
|
+
3. Remove tokens from Git history:
|
|
103
|
+
ace-git-secrets rewrite-history
|
|
104
|
+
|
|
105
|
+
4. Force push cleaned history:
|
|
106
|
+
git push --force-with-lease
|
|
107
|
+
|
|
108
|
+
5. Re-run this check:
|
|
109
|
+
ace-git-secrets check-release
|
|
110
|
+
STEPS
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def format_table_result(result)
|
|
114
|
+
lines = []
|
|
115
|
+
lines << "=" * 60
|
|
116
|
+
lines << result[:message]
|
|
117
|
+
lines << "=" * 60
|
|
118
|
+
lines << ""
|
|
119
|
+
lines << result[:summary]
|
|
120
|
+
|
|
121
|
+
unless result[:passed]
|
|
122
|
+
lines << ""
|
|
123
|
+
lines << "-" * 60
|
|
124
|
+
lines << result[:remediation]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
lines.join("\n")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Secrets
|
|
6
|
+
module Organisms
|
|
7
|
+
# Orchestrates security scanning and reporting
|
|
8
|
+
# High-level workflow for detecting tokens in repositories
|
|
9
|
+
#
|
|
10
|
+
# Uses gitleaks for token detection with whitelist filtering,
|
|
11
|
+
# formatted reporting, and actionable next steps.
|
|
12
|
+
#
|
|
13
|
+
# Requires gitleaks to be installed: brew install gitleaks
|
|
14
|
+
class SecurityAuditor
|
|
15
|
+
attr_reader :scanner, :output_format, :whitelist, :whitelisted_count, :whitelist_audit_log
|
|
16
|
+
|
|
17
|
+
# @param repository_path [String] Path to git repository
|
|
18
|
+
# @param gitleaks_config [String, nil] Path to gitleaks config file
|
|
19
|
+
# @param output_format [String] Output format (table, json, yaml)
|
|
20
|
+
# @param whitelist [Array<Hash>] Patterns/files to whitelist
|
|
21
|
+
# @param exclusions [Array<String>, nil] Glob patterns for files to exclude
|
|
22
|
+
def initialize(repository_path: ".", gitleaks_config: nil, output_format: "table",
|
|
23
|
+
whitelist: [], exclusions: nil)
|
|
24
|
+
@scanner = Molecules::HistoryScanner.new(
|
|
25
|
+
repository_path: repository_path,
|
|
26
|
+
gitleaks_config: gitleaks_config,
|
|
27
|
+
exclusions: exclusions
|
|
28
|
+
)
|
|
29
|
+
@output_format = output_format
|
|
30
|
+
@whitelist = whitelist || []
|
|
31
|
+
@whitelisted_count = 0
|
|
32
|
+
@whitelist_audit_log = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Run full security audit on repository
|
|
36
|
+
# @param since [String, nil] Start commit or date
|
|
37
|
+
# @param min_confidence [String] Minimum confidence level
|
|
38
|
+
# @param output_path [String, nil] Path to save report
|
|
39
|
+
# @param verbose [Boolean] Enable verbose output
|
|
40
|
+
# @return [Models::ScanReport]
|
|
41
|
+
def audit(since: nil, min_confidence: "low", output_path: nil, verbose: false)
|
|
42
|
+
puts "Scanning Git history for authentication tokens..." if verbose
|
|
43
|
+
|
|
44
|
+
start_time = Time.now
|
|
45
|
+
|
|
46
|
+
report = scanner.scan(
|
|
47
|
+
since: since,
|
|
48
|
+
min_confidence: min_confidence
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
scan_duration = Time.now - start_time
|
|
52
|
+
|
|
53
|
+
# Apply whitelist filtering
|
|
54
|
+
report = apply_whitelist(report) if whitelist.any?
|
|
55
|
+
|
|
56
|
+
# Add timing metadata to report
|
|
57
|
+
report = add_timing_metadata(report, scan_duration)
|
|
58
|
+
|
|
59
|
+
# Output results
|
|
60
|
+
output_report(report, output_path)
|
|
61
|
+
|
|
62
|
+
report
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Audit only current files (no history)
|
|
66
|
+
# @param min_confidence [String] Minimum confidence level
|
|
67
|
+
# @param output_path [String, nil] Path to save report
|
|
68
|
+
# @return [Models::ScanReport]
|
|
69
|
+
def audit_files(min_confidence: "low", output_path: nil)
|
|
70
|
+
report = scanner.scan_files(min_confidence: min_confidence)
|
|
71
|
+
output_report(report, output_path)
|
|
72
|
+
report
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get formatted output
|
|
76
|
+
# @param report [Models::ScanReport]
|
|
77
|
+
# @return [String]
|
|
78
|
+
def format_report(report)
|
|
79
|
+
case output_format
|
|
80
|
+
when "json"
|
|
81
|
+
report.to_json
|
|
82
|
+
when "yaml"
|
|
83
|
+
report.to_yaml
|
|
84
|
+
else
|
|
85
|
+
report.to_table
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Print actionable next steps
|
|
90
|
+
# @param report [Models::ScanReport]
|
|
91
|
+
# @return [String]
|
|
92
|
+
def next_steps(report)
|
|
93
|
+
return "No tokens detected. Repository is clean." if report.clean?
|
|
94
|
+
|
|
95
|
+
steps = []
|
|
96
|
+
steps << "SECURITY ALERT: #{report.token_count} token(s) detected in repository"
|
|
97
|
+
steps << ""
|
|
98
|
+
steps << "Recommended next steps:"
|
|
99
|
+
steps << "1. Review detected tokens to confirm they are real (not false positives)"
|
|
100
|
+
steps << "2. Revoke tokens immediately: ace-git-secrets revoke"
|
|
101
|
+
steps << "3. Remove tokens from history: ace-git-secrets rewrite-history"
|
|
102
|
+
steps << "4. Force push the cleaned history: git push --force-with-lease"
|
|
103
|
+
steps << "5. Notify affected team members to re-clone"
|
|
104
|
+
steps << ""
|
|
105
|
+
|
|
106
|
+
if report.revocable_tokens.any?
|
|
107
|
+
steps << "Tokens that can be revoked via API: #{report.revocable_tokens.size}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
manual = report.tokens.reject(&:revocable?)
|
|
111
|
+
if manual.any?
|
|
112
|
+
steps << "Tokens requiring manual revocation: #{manual.size}"
|
|
113
|
+
steps << " Visit provider dashboards to revoke these manually."
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
steps.join("\n")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Output report to file or stdout
|
|
122
|
+
def output_report(report, output_path)
|
|
123
|
+
formatted = format_report(report)
|
|
124
|
+
|
|
125
|
+
if output_path
|
|
126
|
+
File.write(output_path, formatted)
|
|
127
|
+
puts "Report saved to: #{output_path}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
formatted
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Apply whitelist filtering to scan report
|
|
134
|
+
# @param report [Models::ScanReport] Original report
|
|
135
|
+
# @return [Models::ScanReport] Filtered report
|
|
136
|
+
def apply_whitelist(report)
|
|
137
|
+
whitelisted_tokens = []
|
|
138
|
+
filtered_tokens = report.tokens.reject do |token|
|
|
139
|
+
is_whitelisted = whitelisted?(token)
|
|
140
|
+
whitelisted_tokens << token if is_whitelisted
|
|
141
|
+
is_whitelisted
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Track whitelisted count for display
|
|
145
|
+
@whitelisted_count = whitelisted_tokens.size
|
|
146
|
+
|
|
147
|
+
# Return new report with filtered tokens
|
|
148
|
+
Models::ScanReport.new(
|
|
149
|
+
tokens: filtered_tokens,
|
|
150
|
+
repository_path: report.repository_path,
|
|
151
|
+
scanned_at: report.scanned_at,
|
|
152
|
+
scan_options: report.scan_options,
|
|
153
|
+
commits_scanned: report.commits_scanned,
|
|
154
|
+
detection_method: report.detection_method
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check if a token matches any whitelist entry
|
|
159
|
+
# @param token [Models::DetectedToken] Token to check
|
|
160
|
+
# @return [Boolean] true if whitelisted
|
|
161
|
+
def whitelisted?(token)
|
|
162
|
+
whitelist.any? do |entry|
|
|
163
|
+
if matches_whitelist_entry?(token, entry)
|
|
164
|
+
# Audit log for security review: track what was whitelisted and why
|
|
165
|
+
@whitelist_audit_log << {
|
|
166
|
+
token_masked: token.masked_value,
|
|
167
|
+
token_type: token.token_type,
|
|
168
|
+
file_path: token.file_path,
|
|
169
|
+
matched_entry: entry,
|
|
170
|
+
reason: entry["reason"]
|
|
171
|
+
}
|
|
172
|
+
true
|
|
173
|
+
else
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Check if token matches a specific whitelist entry
|
|
180
|
+
# @param token [Models::DetectedToken]
|
|
181
|
+
# @param entry [Hash] Whitelist entry with :pattern or :file key
|
|
182
|
+
# @return [Boolean]
|
|
183
|
+
def matches_whitelist_entry?(token, entry)
|
|
184
|
+
# Match by pattern (exact token value match)
|
|
185
|
+
if entry["pattern"]
|
|
186
|
+
return true if token.raw_value == entry["pattern"]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Match by file path (glob pattern)
|
|
190
|
+
# FNM_EXTGLOB enables ** to match across directories
|
|
191
|
+
# FNM_DOTMATCH enables matching dotfiles like .env, .claude*
|
|
192
|
+
if entry["file"]
|
|
193
|
+
pattern = entry["file"]
|
|
194
|
+
flags = File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH
|
|
195
|
+
return true if File.fnmatch?(pattern, token.file_path, flags)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Add timing metadata to report
|
|
202
|
+
# @param report [Models::ScanReport] Original report
|
|
203
|
+
# @param scan_duration [Float] Scan duration in seconds
|
|
204
|
+
# @return [Models::ScanReport] Report with timing metadata
|
|
205
|
+
def add_timing_metadata(report, scan_duration)
|
|
206
|
+
Models::ScanReport.new(
|
|
207
|
+
tokens: report.tokens,
|
|
208
|
+
repository_path: report.repository_path,
|
|
209
|
+
scanned_at: report.scanned_at,
|
|
210
|
+
scan_options: report.scan_options,
|
|
211
|
+
commits_scanned: report.commits_scanned,
|
|
212
|
+
detection_method: report.detection_method,
|
|
213
|
+
scan_duration: scan_duration
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "secrets/version"
|
|
4
|
+
|
|
5
|
+
# Load ace-config for configuration cascade management
|
|
6
|
+
require "ace/support/config"
|
|
7
|
+
|
|
8
|
+
# Models
|
|
9
|
+
require_relative "secrets/models/detected_token"
|
|
10
|
+
require_relative "secrets/models/revocation_result"
|
|
11
|
+
require_relative "secrets/models/scan_report"
|
|
12
|
+
|
|
13
|
+
# Atoms
|
|
14
|
+
require_relative "secrets/atoms/gitleaks_runner"
|
|
15
|
+
require_relative "secrets/atoms/service_api_client"
|
|
16
|
+
|
|
17
|
+
# Molecules
|
|
18
|
+
require_relative "secrets/molecules/history_scanner"
|
|
19
|
+
require_relative "secrets/molecules/git_rewriter"
|
|
20
|
+
require_relative "secrets/molecules/token_revoker"
|
|
21
|
+
|
|
22
|
+
# Organisms
|
|
23
|
+
require_relative "secrets/organisms/security_auditor"
|
|
24
|
+
require_relative "secrets/organisms/history_cleaner"
|
|
25
|
+
require_relative "secrets/organisms/release_gate"
|
|
26
|
+
|
|
27
|
+
# Commands
|
|
28
|
+
require_relative "secrets/commands/scan_command"
|
|
29
|
+
require_relative "secrets/commands/rewrite_command"
|
|
30
|
+
require_relative "secrets/commands/revoke_command"
|
|
31
|
+
require_relative "secrets/commands/check_release_command"
|
|
32
|
+
|
|
33
|
+
# CLI
|
|
34
|
+
require_relative "secrets/cli"
|
|
35
|
+
|
|
36
|
+
module Ace
|
|
37
|
+
module Git
|
|
38
|
+
module Secrets
|
|
39
|
+
class Error < StandardError; end
|
|
40
|
+
class GitRewriteError < Error; end
|
|
41
|
+
class RevocationError < Error; end
|
|
42
|
+
|
|
43
|
+
# Mutex for thread-safe config loading
|
|
44
|
+
@config_mutex = Mutex.new
|
|
45
|
+
|
|
46
|
+
# Load ace-git-secrets configuration using ace-config cascade
|
|
47
|
+
# Follows ADR-022: Load defaults from .ace-defaults/, merge user overrides from .ace/
|
|
48
|
+
# Uses Ace::Support::Config.create() for configuration cascade resolution
|
|
49
|
+
#
|
|
50
|
+
# @note Thread Safety: This method is thread-safe via Mutex synchronization.
|
|
51
|
+
# The config is loaded once and cached for subsequent calls.
|
|
52
|
+
# IMPORTANT: Config MUST be preloaded via CLI.start before parallel operations
|
|
53
|
+
# begin. When using ace-git-secrets as a library (not via CLI), call
|
|
54
|
+
# Ace::Git::Secrets.config explicitly before spawning any threads that
|
|
55
|
+
# perform scanning or revocation. Failure to preload may result in race
|
|
56
|
+
# conditions during config initialization under concurrent load.
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] Configuration hash
|
|
59
|
+
def self.config
|
|
60
|
+
@config_mutex.synchronize do
|
|
61
|
+
@config ||= begin
|
|
62
|
+
gem_root = Gem.loaded_specs["ace-git-secrets"]&.gem_dir ||
|
|
63
|
+
File.expand_path("../../..", __dir__)
|
|
64
|
+
|
|
65
|
+
resolver = Ace::Support::Config.create(
|
|
66
|
+
config_dir: ".ace",
|
|
67
|
+
defaults_dir: ".ace-defaults",
|
|
68
|
+
gem_path: gem_root
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Resolve config for git-secrets namespace
|
|
72
|
+
config = resolver.resolve_namespace("git-secrets")
|
|
73
|
+
|
|
74
|
+
# Extract git-secrets section if present
|
|
75
|
+
config.data["git-secrets"] || config.data
|
|
76
|
+
rescue => e
|
|
77
|
+
warn "Warning: Could not load ace-git-secrets config: #{e.message}"
|
|
78
|
+
fallback_defaults
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Fallback defaults when config loading fails
|
|
84
|
+
# Note: Should rarely be used - .ace-defaults/ should always be present
|
|
85
|
+
# @return [Hash] Minimal fallback configuration
|
|
86
|
+
def self.fallback_defaults
|
|
87
|
+
{
|
|
88
|
+
"exclusions" => [],
|
|
89
|
+
"whitelist" => [],
|
|
90
|
+
"output" => {
|
|
91
|
+
"format" => "table",
|
|
92
|
+
"mask_tokens" => true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get file exclusions from config
|
|
98
|
+
# ADR-022: Exclusions come from .ace-defaults/, merged with user config
|
|
99
|
+
# @return [Array<String>] Glob patterns for files to exclude
|
|
100
|
+
def self.exclusions
|
|
101
|
+
config["exclusions"] || []
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Resolve gitleaks config path with cascade
|
|
105
|
+
# Checks: .ace/git-secrets/gitleaks.toml -> .ace-defaults/git-secrets/gitleaks.toml
|
|
106
|
+
#
|
|
107
|
+
# @note Thread Safety: This method uses the same mutex as config to ensure
|
|
108
|
+
# thread-safe initialization. Like config, it should be preloaded before
|
|
109
|
+
# spawning threads (the CLI does this automatically via CLI.start).
|
|
110
|
+
# @note Environment Variable: Set ACE_GITLEAKS_CONFIG_PATH to override
|
|
111
|
+
# automatic config discovery (useful for testing).
|
|
112
|
+
# @return [String, nil] Path to gitleaks config, or nil if not found
|
|
113
|
+
def self.gitleaks_config_path
|
|
114
|
+
@config_mutex.synchronize do
|
|
115
|
+
@gitleaks_config_path ||= begin
|
|
116
|
+
# Check environment variable override first (useful for testing)
|
|
117
|
+
env_path = ENV["ACE_GITLEAKS_CONFIG_PATH"]
|
|
118
|
+
if env_path && File.exist?(env_path)
|
|
119
|
+
env_path
|
|
120
|
+
else
|
|
121
|
+
# Check user config first (project .ace/)
|
|
122
|
+
user_path = find_user_gitleaks_config
|
|
123
|
+
if user_path && File.exist?(user_path)
|
|
124
|
+
user_path
|
|
125
|
+
else
|
|
126
|
+
# Fall back to gem defaults
|
|
127
|
+
gem_root = File.expand_path("../../..", __dir__)
|
|
128
|
+
example_path = File.join(gem_root, ".ace-defaults", "git-secrets", "gitleaks.toml")
|
|
129
|
+
File.exist?(example_path) ? example_path : nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Find user gitleaks config in project .ace/ directory
|
|
137
|
+
# @return [String, nil] Path to user gitleaks config
|
|
138
|
+
def self.find_user_gitleaks_config
|
|
139
|
+
# Search from current dir upward for .ace/git-secrets/gitleaks.toml
|
|
140
|
+
dir = Dir.pwd
|
|
141
|
+
while dir != "/"
|
|
142
|
+
config_path = File.join(dir, ".ace", "git-secrets", "gitleaks.toml")
|
|
143
|
+
return config_path if File.exist?(config_path)
|
|
144
|
+
dir = File.dirname(dir)
|
|
145
|
+
end
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Check if gitleaks is available in PATH
|
|
150
|
+
# @return [Boolean] true if gitleaks is available
|
|
151
|
+
def self.gitleaks_available?
|
|
152
|
+
@gitleaks_available ||= system("which gitleaks > /dev/null 2>&1")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Reset config cache
|
|
156
|
+
# Useful for testing to ensure clean state between tests.
|
|
157
|
+
# Thread-safe - uses mutex to reset all cached values atomically.
|
|
158
|
+
# @return [void]
|
|
159
|
+
def self.reset_config!
|
|
160
|
+
@config_mutex.synchronize do
|
|
161
|
+
@config = nil
|
|
162
|
+
@gitleaks_config_path = nil
|
|
163
|
+
end
|
|
164
|
+
@gitleaks_available = nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|