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,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