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,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Git
|
|
9
|
+
module Secrets
|
|
10
|
+
module Atoms
|
|
11
|
+
# HTTP API client for token revocation services
|
|
12
|
+
# Builds requests for GitHub, Anthropic, OpenAI credential revocation APIs
|
|
13
|
+
class ServiceApiClient
|
|
14
|
+
# Default GitHub Credential Revocation API (unauthenticated, rate limited)
|
|
15
|
+
DEFAULT_GITHUB_REVOKE_URL = "https://api.github.com/credentials/revoke"
|
|
16
|
+
|
|
17
|
+
# Default user agent for API requests
|
|
18
|
+
DEFAULT_USER_AGENT = "ace-git-secrets/#{Ace::Git::Secrets::VERSION}"
|
|
19
|
+
|
|
20
|
+
attr_reader :timeout, :retry_count, :github_revoke_url, :github_api_base_url, :user_agent
|
|
21
|
+
|
|
22
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
23
|
+
# @param retry_count [Integer] Number of retries
|
|
24
|
+
# @param github_api_url [String, nil] Custom GitHub API URL (for GitHub Enterprise)
|
|
25
|
+
# @param user_agent [String, nil] Custom User-Agent header
|
|
26
|
+
def initialize(timeout: 30, retry_count: 3, github_api_url: nil, user_agent: nil)
|
|
27
|
+
@timeout = timeout
|
|
28
|
+
@retry_count = retry_count
|
|
29
|
+
@github_api_base_url = github_api_url&.chomp("/") || "https://api.github.com"
|
|
30
|
+
@github_revoke_url = "#{@github_api_base_url}/credentials/revoke"
|
|
31
|
+
@user_agent = user_agent || DEFAULT_USER_AGENT
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Revoke a GitHub token
|
|
35
|
+
# Uses GitHub's Credential Revocation API (unauthenticated)
|
|
36
|
+
# Connection is reused for bulk revocation efficiency
|
|
37
|
+
# @param token [String] The token to revoke
|
|
38
|
+
# @param check_rate_limit [Boolean] Whether to check rate limit before request
|
|
39
|
+
# @return [Hash] Result with :success, :message, :response keys
|
|
40
|
+
def revoke_github_token(token, check_rate_limit: false)
|
|
41
|
+
# Optionally check rate limit before attempting revocation
|
|
42
|
+
if check_rate_limit
|
|
43
|
+
rate_info = github_rate_limit
|
|
44
|
+
if rate_info[:remaining].is_a?(Integer) && rate_info[:remaining] == 0
|
|
45
|
+
reset_msg = rate_info[:reset_at] ? " Reset at #{rate_info[:reset_at]}" : ""
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
message: "GitHub API rate limit exceeded.#{reset_msg}",
|
|
49
|
+
response: nil,
|
|
50
|
+
rate_limited: true
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
response = github_revoke_connection.post do |req|
|
|
56
|
+
req.headers["Content-Type"] = "application/json"
|
|
57
|
+
req.headers["Accept"] = "application/json"
|
|
58
|
+
req.body = JSON.generate({credential: token})
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
parse_github_response(response)
|
|
62
|
+
rescue Faraday::Error => e
|
|
63
|
+
{success: false, message: "GitHub API error: #{e.message}", response: nil}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get cached GitHub revoke connection for bulk operations
|
|
67
|
+
# @return [Faraday::Connection]
|
|
68
|
+
def github_revoke_connection
|
|
69
|
+
@github_revoke_connection ||= build_connection(github_revoke_url)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build revocation request for a service
|
|
73
|
+
# @param service [String] Service name (github, anthropic, openai)
|
|
74
|
+
# @param token [String] Token to revoke
|
|
75
|
+
# @return [Hash] Request details for manual revocation if API not available
|
|
76
|
+
def build_revocation_request(service, token)
|
|
77
|
+
case service
|
|
78
|
+
when "github"
|
|
79
|
+
{
|
|
80
|
+
method: :post,
|
|
81
|
+
url: github_revoke_url,
|
|
82
|
+
headers: {"Content-Type" => "application/json"},
|
|
83
|
+
body: {credential: token},
|
|
84
|
+
notes: "GitHub tokens can be revoked via API without authentication"
|
|
85
|
+
}
|
|
86
|
+
when "anthropic"
|
|
87
|
+
{
|
|
88
|
+
method: :manual,
|
|
89
|
+
url: "https://console.anthropic.com/settings/keys",
|
|
90
|
+
notes: "Anthropic API keys must be revoked manually via the console"
|
|
91
|
+
}
|
|
92
|
+
when "openai"
|
|
93
|
+
{
|
|
94
|
+
method: :manual,
|
|
95
|
+
url: "https://platform.openai.com/api-keys",
|
|
96
|
+
notes: "OpenAI API keys must be revoked manually via the platform"
|
|
97
|
+
}
|
|
98
|
+
when "aws"
|
|
99
|
+
{
|
|
100
|
+
method: :manual,
|
|
101
|
+
url: "https://console.aws.amazon.com/iam/home#/security_credentials",
|
|
102
|
+
notes: "AWS credentials must be rotated/deleted via IAM console"
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
{
|
|
106
|
+
method: :unsupported,
|
|
107
|
+
url: nil,
|
|
108
|
+
notes: "Automatic revocation not supported for #{service}"
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check API rate limit status for GitHub
|
|
114
|
+
# Uses configured GitHub API base URL (supports GitHub Enterprise)
|
|
115
|
+
# @return [Hash] Rate limit info
|
|
116
|
+
def github_rate_limit
|
|
117
|
+
conn = Faraday.new(url: github_api_base_url) do |f|
|
|
118
|
+
f.options.timeout = timeout
|
|
119
|
+
if retry_count > 0
|
|
120
|
+
f.request :retry, max: retry_count
|
|
121
|
+
end
|
|
122
|
+
f.adapter Faraday.default_adapter
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
response = conn.get("/rate_limit")
|
|
126
|
+
|
|
127
|
+
if response.success?
|
|
128
|
+
data = JSON.parse(response.body)
|
|
129
|
+
resources = data["resources"]["core"]
|
|
130
|
+
{
|
|
131
|
+
limit: resources["limit"],
|
|
132
|
+
remaining: resources["remaining"],
|
|
133
|
+
reset_at: Time.at(resources["reset"])
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
{limit: 60, remaining: "unknown", reset_at: nil}
|
|
137
|
+
end
|
|
138
|
+
rescue
|
|
139
|
+
{limit: 60, remaining: "unknown", reset_at: nil}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# Build Faraday connection
|
|
145
|
+
# @param url [String] Base URL
|
|
146
|
+
# @return [Faraday::Connection]
|
|
147
|
+
def build_connection(url)
|
|
148
|
+
Faraday.new(url: url) do |f|
|
|
149
|
+
f.options.timeout = timeout
|
|
150
|
+
f.headers["User-Agent"] = user_agent
|
|
151
|
+
# Only add retry middleware if retry_count > 0
|
|
152
|
+
# (Faraday 2.x requires faraday-retry gem for this)
|
|
153
|
+
if retry_count > 0
|
|
154
|
+
f.request :retry, max: retry_count, interval: 0.5,
|
|
155
|
+
exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
|
|
156
|
+
end
|
|
157
|
+
f.adapter Faraday.default_adapter
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Parse GitHub API response
|
|
162
|
+
# @param response [Faraday::Response]
|
|
163
|
+
# @return [Hash]
|
|
164
|
+
def parse_github_response(response)
|
|
165
|
+
case response.status
|
|
166
|
+
when 200, 204
|
|
167
|
+
{success: true, message: "Token revoked successfully", response: response}
|
|
168
|
+
when 404
|
|
169
|
+
{success: false, message: "Token not found or already revoked", response: response}
|
|
170
|
+
when 422
|
|
171
|
+
{success: false, message: "Invalid token format", response: response}
|
|
172
|
+
when 429
|
|
173
|
+
{success: false, message: "Rate limit exceeded. Try again later.", response: response}
|
|
174
|
+
else
|
|
175
|
+
body = response.body.to_s
|
|
176
|
+
message = begin
|
|
177
|
+
JSON.parse(body)["message"]
|
|
178
|
+
rescue
|
|
179
|
+
body
|
|
180
|
+
end
|
|
181
|
+
{success: false, message: "GitHub API error: #{message}", response: response}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Secrets
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
# ace-support-cli command for pre-release security check
|
|
9
|
+
#
|
|
10
|
+
# Exit codes:
|
|
11
|
+
# - 0: Passed (no tokens detected)
|
|
12
|
+
# - 1: Failed (tokens detected)
|
|
13
|
+
# - 2: Error occurred
|
|
14
|
+
class CheckRelease < Ace::Support::Cli::Command
|
|
15
|
+
include Ace::Support::Cli::Base
|
|
16
|
+
|
|
17
|
+
desc "Pre-release security validation check"
|
|
18
|
+
|
|
19
|
+
option :strict, type: :boolean, default: false,
|
|
20
|
+
desc: "Fail on medium confidence matches"
|
|
21
|
+
option :format, type: :string, aliases: ["f"], default: "table",
|
|
22
|
+
desc: "Output format (table, json)"
|
|
23
|
+
option :debug, type: :boolean, default: false,
|
|
24
|
+
desc: "Show debug output"
|
|
25
|
+
|
|
26
|
+
def call(**options)
|
|
27
|
+
debug_log("Starting check-release with options: #{format_pairs(options)}", options)
|
|
28
|
+
|
|
29
|
+
# Delegate to existing CheckReleaseCommand logic
|
|
30
|
+
exit_code = Ace::Git::Secrets::Commands::CheckReleaseCommand.execute(options)
|
|
31
|
+
raise Ace::Support::Cli::Error.new("Pre-release check failed", exit_code: exit_code) if exit_code != 0
|
|
32
|
+
rescue => e
|
|
33
|
+
debug_log(e.full_message, options) if debug?(options)
|
|
34
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Secrets
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
# ace-support-cli command for revoking tokens via provider APIs
|
|
9
|
+
#
|
|
10
|
+
# Exit codes:
|
|
11
|
+
# - 0: Success (all tokens revoked)
|
|
12
|
+
# - 1: Partial success or failure
|
|
13
|
+
# - 2: Error occurred
|
|
14
|
+
class Revoke < Ace::Support::Cli::Command
|
|
15
|
+
include Ace::Support::Cli::Base
|
|
16
|
+
|
|
17
|
+
desc "Revoke detected tokens via provider APIs"
|
|
18
|
+
|
|
19
|
+
option :service, type: :string, aliases: ["s"],
|
|
20
|
+
desc: "Revoke for specific service"
|
|
21
|
+
option :token, type: :string, aliases: ["t"],
|
|
22
|
+
desc: "Revoke specific token"
|
|
23
|
+
option :scan_file, type: :string,
|
|
24
|
+
desc: "Use previous scan results file"
|
|
25
|
+
option :debug, type: :boolean, default: false,
|
|
26
|
+
desc: "Show debug output"
|
|
27
|
+
|
|
28
|
+
def call(**options)
|
|
29
|
+
debug_log("Starting revoke with options: #{format_pairs(options)}", options)
|
|
30
|
+
|
|
31
|
+
exit_code = Ace::Git::Secrets::Commands::RevokeCommand.execute(options)
|
|
32
|
+
raise Ace::Support::Cli::Error.new("Revocation failed", exit_code: exit_code) if exit_code != 0
|
|
33
|
+
rescue Ace::Support::Cli::Error
|
|
34
|
+
raise
|
|
35
|
+
rescue => e
|
|
36
|
+
debug_log(e.full_message, options) if debug?(options)
|
|
37
|
+
raise Ace::Support::Cli::Error.new(e.message, exit_code: 2)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Secrets
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
# ace-support-cli command for rewriting Git history to remove tokens
|
|
9
|
+
#
|
|
10
|
+
# Exit codes:
|
|
11
|
+
# - 0: Success
|
|
12
|
+
# - 1: Failure
|
|
13
|
+
# - 2: Error occurred
|
|
14
|
+
class Rewrite < Ace::Support::Cli::Command
|
|
15
|
+
include Ace::Support::Cli::Base
|
|
16
|
+
|
|
17
|
+
desc "Remove detected tokens from Git history"
|
|
18
|
+
|
|
19
|
+
option :dry_run, type: :boolean, aliases: ["n"], default: false,
|
|
20
|
+
desc: "Show what would be rewritten"
|
|
21
|
+
option :backup, type: :boolean, default: true,
|
|
22
|
+
desc: "Create backup before rewrite"
|
|
23
|
+
option :force, type: :boolean, default: false,
|
|
24
|
+
desc: "Skip confirmation prompt"
|
|
25
|
+
option :scan_file, type: :string,
|
|
26
|
+
desc: "Use previous scan results file"
|
|
27
|
+
option :debug, type: :boolean, default: false,
|
|
28
|
+
desc: "Show debug output"
|
|
29
|
+
|
|
30
|
+
def call(**options)
|
|
31
|
+
debug_log("Starting rewrite-history with options: #{format_pairs(options)}", options)
|
|
32
|
+
|
|
33
|
+
exit_code = Ace::Git::Secrets::Commands::RewriteCommand.execute(options)
|
|
34
|
+
raise Ace::Support::Cli::Error.new("Rewrite failed", exit_code: exit_code) if exit_code != 0
|
|
35
|
+
rescue Ace::Support::Cli::Error
|
|
36
|
+
raise
|
|
37
|
+
rescue => e
|
|
38
|
+
debug_log(e.full_message, options) if debug?(options)
|
|
39
|
+
raise Ace::Support::Cli::Error.new(e.message, exit_code: 2)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Secrets
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
# ace-support-cli command for scanning repository for tokens
|
|
9
|
+
#
|
|
10
|
+
# Requires gitleaks to be installed (brew install gitleaks)
|
|
11
|
+
#
|
|
12
|
+
# Exit codes:
|
|
13
|
+
# - 0: Clean (no tokens found)
|
|
14
|
+
# - 1: Tokens detected
|
|
15
|
+
# - 2: Error occurred
|
|
16
|
+
class Scan < Ace::Support::Cli::Command
|
|
17
|
+
include Ace::Support::Cli::Base
|
|
18
|
+
|
|
19
|
+
desc "Scan Git history for authentication tokens"
|
|
20
|
+
|
|
21
|
+
option :since, type: :string, desc: "Start scanning from commit or date"
|
|
22
|
+
option :format, type: :string, aliases: ["f"], default: "table",
|
|
23
|
+
desc: "Stdout format when --verbose is used (table, json, yaml)"
|
|
24
|
+
option :report_format, type: :string, aliases: ["r"], default: "json",
|
|
25
|
+
desc: "Format for saved report file (json, markdown)"
|
|
26
|
+
option :confidence, type: :string, aliases: ["c"], default: "low",
|
|
27
|
+
desc: "Minimum confidence (high, medium, low)"
|
|
28
|
+
option :verbose, type: :boolean, default: false,
|
|
29
|
+
desc: "Enable verbose output with full report to stdout"
|
|
30
|
+
option :quiet, type: :boolean, aliases: ["q"], default: false,
|
|
31
|
+
desc: "Suppress non-essential output"
|
|
32
|
+
option :debug, type: :boolean, default: false,
|
|
33
|
+
desc: "Show debug output"
|
|
34
|
+
|
|
35
|
+
def call(**options)
|
|
36
|
+
debug_log("Starting scan with options: #{format_pairs(options)}", options)
|
|
37
|
+
|
|
38
|
+
exit_code = Ace::Git::Secrets::Commands::ScanCommand.execute(options)
|
|
39
|
+
raise Ace::Support::Cli::Error.new("Tokens detected", exit_code: exit_code) if exit_code != 0
|
|
40
|
+
rescue Ace::Support::Cli::Error
|
|
41
|
+
raise
|
|
42
|
+
rescue => e
|
|
43
|
+
debug_log(e.full_message, options) if debug?(options)
|
|
44
|
+
raise Ace::Support::Cli::Error.new(e.message, exit_code: 2)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "../secrets"
|
|
6
|
+
# Business logic command objects
|
|
7
|
+
require_relative "commands/scan_command"
|
|
8
|
+
require_relative "commands/rewrite_command"
|
|
9
|
+
require_relative "commands/revoke_command"
|
|
10
|
+
require_relative "commands/check_release_command"
|
|
11
|
+
# ace-support-cli command wrappers (Hanami pattern: CLI::Commands::)
|
|
12
|
+
require_relative "cli/commands/scan"
|
|
13
|
+
require_relative "cli/commands/rewrite"
|
|
14
|
+
require_relative "cli/commands/revoke"
|
|
15
|
+
require_relative "cli/commands/check_release"
|
|
16
|
+
require_relative "version"
|
|
17
|
+
|
|
18
|
+
module Ace
|
|
19
|
+
module Git
|
|
20
|
+
module Secrets
|
|
21
|
+
# ace-support-cli based CLI registry for ace-git-secrets
|
|
22
|
+
#
|
|
23
|
+
# This replaces the Thor-based CLI with ace-support-cli while maintaining
|
|
24
|
+
# complete command parity and user-facing behavior.
|
|
25
|
+
module CLI
|
|
26
|
+
extend Ace::Support::Cli::RegistryDsl
|
|
27
|
+
|
|
28
|
+
PROGRAM_NAME = "ace-git-secrets"
|
|
29
|
+
|
|
30
|
+
REGISTERED_COMMANDS = [
|
|
31
|
+
["scan", "Scan Git history for authentication tokens"],
|
|
32
|
+
["rewrite-history", "Rewrite Git history to remove leaked tokens"],
|
|
33
|
+
["revoke", "Revoke leaked tokens via provider APIs"],
|
|
34
|
+
["check-release", "Check repository readiness for release"]
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
HELP_EXAMPLES = [
|
|
38
|
+
"ace-git-secrets scan --staged # Pre-commit check",
|
|
39
|
+
"ace-git-secrets check-release # Verify before publish",
|
|
40
|
+
"ace-git-secrets revoke --token TOKEN # Revoke leaked credential"
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Register the scan command (default) - Hanami pattern: CLI::Commands::
|
|
44
|
+
register "scan", CLI::Commands::Scan.new
|
|
45
|
+
|
|
46
|
+
# Register the rewrite-history command
|
|
47
|
+
register "rewrite-history", CLI::Commands::Rewrite.new
|
|
48
|
+
|
|
49
|
+
# Register the revoke command
|
|
50
|
+
register "revoke", CLI::Commands::Revoke.new
|
|
51
|
+
|
|
52
|
+
# Register the check-release command
|
|
53
|
+
register "check-release", CLI::Commands::CheckRelease.new
|
|
54
|
+
|
|
55
|
+
# Register version command
|
|
56
|
+
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
57
|
+
gem_name: "ace-git-secrets",
|
|
58
|
+
version: Ace::Git::Secrets::VERSION
|
|
59
|
+
)
|
|
60
|
+
register "version", version_cmd
|
|
61
|
+
register "--version", version_cmd
|
|
62
|
+
|
|
63
|
+
help_cmd = Ace::Support::Cli::HelpCommand.build(
|
|
64
|
+
program_name: PROGRAM_NAME,
|
|
65
|
+
version: Ace::Git::Secrets::VERSION,
|
|
66
|
+
commands: REGISTERED_COMMANDS,
|
|
67
|
+
examples: HELP_EXAMPLES
|
|
68
|
+
)
|
|
69
|
+
register "help", help_cmd
|
|
70
|
+
register "--help", help_cmd
|
|
71
|
+
register "-h", help_cmd
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Secrets
|
|
6
|
+
module Commands
|
|
7
|
+
# CLI command for pre-release security check
|
|
8
|
+
class CheckReleaseCommand
|
|
9
|
+
# Execute check-release command
|
|
10
|
+
# @param options [Hash] Command options
|
|
11
|
+
# @return [Integer] Exit code (0=passed, 1=failed, 2=error)
|
|
12
|
+
def self.execute(options)
|
|
13
|
+
new(options).execute
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(options)
|
|
17
|
+
@options = options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute
|
|
21
|
+
# Ensure gitleaks is available
|
|
22
|
+
Atoms::GitleaksRunner.ensure_available!
|
|
23
|
+
|
|
24
|
+
puts "Performing pre-release security check..."
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
gate = Organisms::ReleaseGate.new(
|
|
28
|
+
repository_path: ".",
|
|
29
|
+
strict: @options[:strict],
|
|
30
|
+
gitleaks_config: Ace::Git::Secrets.gitleaks_config_path
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
result = gate.check
|
|
34
|
+
|
|
35
|
+
# Output formatted result
|
|
36
|
+
puts gate.format_result(result, format: @options[:format] || "table")
|
|
37
|
+
|
|
38
|
+
result[:exit_code]
|
|
39
|
+
rescue => e
|
|
40
|
+
puts "Error: #{e.message}"
|
|
41
|
+
puts e.backtrace.first(5).join("\n") if ENV["DEBUG"]
|
|
42
|
+
2
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|