Class: Ace::Git::Secrets::Models::ScanReport
- Inherits:
-
Object
- Object
- Ace::Git::Secrets::Models::ScanReport
- Defined in:
- lib/ace/git/secrets/models/scan_report.rb
Overview
Represents a complete scan report with detected tokens and metadata
Instance Attribute Summary collapse
-
#commits_scanned ⇒ Object
readonly
Returns the value of attribute commits_scanned.
-
#detection_method ⇒ Object
readonly
Returns the value of attribute detection_method.
-
#repository_path ⇒ Object
readonly
Returns the value of attribute repository_path.
-
#scan_duration ⇒ Object
readonly
Returns the value of attribute scan_duration.
-
#scan_options ⇒ Object
readonly
Returns the value of attribute scan_options.
-
#scanned_at ⇒ Object
readonly
Returns the value of attribute scanned_at.
-
#thread_count ⇒ Object
readonly
Returns the value of attribute thread_count.
-
#tokens ⇒ Object
readonly
Returns the value of attribute tokens.
Instance Method Summary collapse
-
#affected_commits ⇒ Array<String>
Get unique commits with tokens.
-
#affected_files ⇒ Array<String>
Get unique files with tokens.
-
#clean? ⇒ Boolean
Check if any tokens were detected.
-
#deduplicated_tokens ⇒ Hash<String, Hash>
Get unique tokens grouped by raw_value with all their locations.
-
#high_confidence_count ⇒ Integer
Count of high confidence tokens.
-
#initialize(tokens: [], repository_path: nil, scanned_at: nil, scan_options: {}, commits_scanned: 0, detection_method: "ruby_patterns", scan_duration: nil, thread_count: nil) ⇒ ScanReport
constructor
A new instance of ScanReport.
-
#low_confidence_count ⇒ Integer
Count of low confidence tokens.
-
#medium_confidence_count ⇒ Integer
Count of medium confidence tokens.
-
#revocable_tokens ⇒ Array<DetectedToken>
Get tokens that can be revoked.
-
#save_providers_report(sessions_dir, session_id) ⇒ String?
Save providers-grouped markdown report for revocation workflow.
-
#save_to_file(format: :json, directory: nil, include_raw: true, quiet: false) ⇒ String
Save report to file in cache directory.
-
#summary ⇒ Hash
Summary statistics.
-
#to_h(include_raw: false) ⇒ Hash
Convert to hash for serialization.
-
#to_json(include_raw: false) ⇒ String
Serialize to JSON.
-
#to_markdown ⇒ String
Format as markdown for file output.
-
#to_providers_markdown ⇒ String
Format as providers-grouped markdown for revocation workflow.
-
#to_summary(report_path: nil) ⇒ String
Generate concise summary for stdout.
-
#to_table ⇒ String
Format as table for CLI output.
-
#to_yaml(include_raw: false) ⇒ String
Serialize to YAML.
-
#token_count ⇒ Integer
Total number of detected tokens.
-
#token_types ⇒ Array<String>
Get unique token types found.
-
#tokens_by_confidence(level) ⇒ Array<DetectedToken>
Get tokens by confidence level.
-
#tokens_found? ⇒ Boolean
Check if tokens were detected.
Constructor Details
#initialize(tokens: [], repository_path: nil, scanned_at: nil, scan_options: {}, commits_scanned: 0, detection_method: "ruby_patterns", scan_duration: nil, thread_count: nil) ⇒ ScanReport
Returns a new instance of ScanReport.
25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 25 def initialize(tokens: [], repository_path: nil, scanned_at: nil, scan_options: {}, commits_scanned: 0, detection_method: "ruby_patterns", scan_duration: nil, thread_count: nil) @tokens = tokens.freeze @repository_path = repository_path @scanned_at = scanned_at || Time.now @scan_options = .freeze @commits_scanned = commits_scanned @detection_method = detection_method @scan_duration = scan_duration @thread_count = thread_count end |
Instance Attribute Details
#commits_scanned ⇒ Object (readonly)
Returns the value of attribute commits_scanned.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def commits_scanned @commits_scanned end |
#detection_method ⇒ Object (readonly)
Returns the value of attribute detection_method.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def detection_method @detection_method end |
#repository_path ⇒ Object (readonly)
Returns the value of attribute repository_path.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def repository_path @repository_path end |
#scan_duration ⇒ Object (readonly)
Returns the value of attribute scan_duration.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def scan_duration @scan_duration end |
#scan_options ⇒ Object (readonly)
Returns the value of attribute scan_options.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def @scan_options end |
#scanned_at ⇒ Object (readonly)
Returns the value of attribute scanned_at.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def scanned_at @scanned_at end |
#thread_count ⇒ Object (readonly)
Returns the value of attribute thread_count.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def thread_count @thread_count end |
#tokens ⇒ Object (readonly)
Returns the value of attribute tokens.
14 15 16 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 14 def tokens @tokens end |
Instance Method Details
#affected_commits ⇒ Array<String>
Get unique commits with tokens
95 96 97 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 95 def affected_commits tokens.map(&:commit_hash).uniq end |
#affected_files ⇒ Array<String>
Get unique files with tokens
89 90 91 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 89 def affected_files tokens.map(&:file_path).uniq.sort end |
#clean? ⇒ Boolean
Check if any tokens were detected
40 41 42 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 40 def clean? tokens.empty? end |
#deduplicated_tokens ⇒ Hash<String, Hash>
Get unique tokens grouped by raw_value with all their locations
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 264 def deduplicated_tokens result = {} tokens.each do |token| if result.key?(token.raw_value) result[token.raw_value][:locations] << { commit: token.short_commit, file: token.file_path, line: token.line_number } else result[token.raw_value] = { token: token, locations: [{ commit: token.short_commit, file: token.file_path, line: token.line_number }] } end end result end |
#high_confidence_count ⇒ Integer
Count of high confidence tokens
65 66 67 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 65 def high_confidence_count tokens_by_confidence("high").size end |
#low_confidence_count ⇒ Integer
Count of low confidence tokens
77 78 79 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 77 def low_confidence_count tokens_by_confidence("low").size end |
#medium_confidence_count ⇒ Integer
Count of medium confidence tokens
71 72 73 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 71 def medium_confidence_count tokens_by_confidence("medium").size end |
#revocable_tokens ⇒ Array<DetectedToken>
Get tokens that can be revoked
101 102 103 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 101 def revocable_tokens tokens.select(&:revocable?) end |
#save_providers_report(sessions_dir, session_id) ⇒ String?
Save providers-grouped markdown report for revocation workflow
225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 225 def save_providers_report(sessions_dir, session_id) providers_content = to_providers_markdown return nil unless providers_content providers_path = File.join(sessions_dir, "#{session_id}-providers.md") File.write(providers_path, providers_content) providers_path rescue => e # Log error but don't fail main report save warn "Warning: Could not save providers report: #{e.}" nil end |
#save_to_file(format: :json, directory: nil, include_raw: true, quiet: false) ⇒ String
Save report to file in cache directory
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 196 def save_to_file(format: :json, directory: nil, include_raw: true, quiet: false) cache_dir = directory || File.join(repository_path || ".", ".ace-local", "git-secrets") sessions_dir = File.join(cache_dir, "sessions") FileUtils.mkdir_p(sessions_dir) session_id = Ace::B36ts.encode(scanned_at) ext = (format == :markdown) ? "md" : "json" path = File.join(sessions_dir, "#{session_id}-report.#{ext}") # JSON format includes raw values by default for revoke/rewrite-history workflows # Markdown format never includes raw values (human-readable) content = (format == :markdown) ? to_markdown : to_json(include_raw: include_raw) File.write(path, content) # Security warning: remind user that raw secrets are written to disk if include_raw && tokens_found? && !quiet warn "SECURITY: Report contains raw token values. Delete after remediation: #{path}" end # Generate providers report for revocation workflow (only when tokens found) save_providers_report(sessions_dir, session_id) if tokens_found? path end |
#summary ⇒ Hash
Summary statistics
107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 107 def summary { total_tokens: token_count, high_confidence: high_confidence_count, medium_confidence: medium_confidence_count, low_confidence: low_confidence_count, token_types: token_types, affected_files: affected_files.size, affected_commits: affected_commits.size, revocable: revocable_tokens.size, detection_method: detection_method } end |
#to_h(include_raw: false) ⇒ Hash
Convert to hash for serialization
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 124 def to_h(include_raw: false) result = { scan_metadata: { repository: repository_path, scanned_at: scanned_at.iso8601, commits_scanned: commits_scanned, scan_duration_seconds: scan_duration&.round(2), thread_count: thread_count, detection_method: detection_method }, scan_options: , summary: summary, tokens: tokens.map { |t| t.to_h(include_raw: include_raw) } } # Remove nil values from scan_metadata result[:scan_metadata].compact! result end |
#to_json(include_raw: false) ⇒ String
Serialize to JSON
146 147 148 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 146 def to_json(include_raw: false) JSON.pretty_generate(to_h(include_raw: include_raw)) end |
#to_markdown ⇒ String
Format as markdown for file output
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 337 def to_markdown lines = [] lines << "# Security Scan Report" lines << "" lines << "## Scan Metadata" lines << "" lines << "| Field | Value |" lines << "|-------|-------|" lines << "| Repository | `#{repository_path}` |" lines << "| Scanned at | #{scanned_at.iso8601} |" lines << "| Commits scanned | #{commits_scanned} |" lines << "| Scan duration | #{scan_duration ? format_duration(scan_duration) : "N/A"} |" lines << "| Thread count | #{thread_count || "N/A"} |" lines << "| Detection method | #{detection_method} |" lines << "" lines << "## Summary" lines << "" lines << "| Metric | Count |" lines << "|--------|-------|" lines << "| Total tokens | #{token_count} |" lines << "| High confidence | #{high_confidence_count} |" lines << "| Medium confidence | #{medium_confidence_count} |" lines << "| Low confidence | #{low_confidence_count} |" lines << "" if tokens.any? lines << "## Detected Tokens" lines << "" tokens.each_with_index do |token, idx| lines << "### #{idx + 1}. #{token.token_type}" lines << "" lines << "- **Confidence**: #{token.confidence}" lines << "- **Value**: `#{token.masked_value}`" lines << "- **Commit**: #{token.short_commit}" lines << "- **File**: `#{token.file_path}#{":#{token.line_number}" if token.line_number}`" lines << "- **Detected by**: #{token.detected_by}" lines << "" end else lines << "No tokens detected. Repository is clean." lines << "" end lines.join("\n") end |
#to_providers_markdown ⇒ String
Format as providers-grouped markdown for revocation workflow
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 289 def to_providers_markdown return nil if clean? deduped = deduplicated_tokens # Group by provider, with revocable providers first by_provider = deduped.values.group_by { |entry| entry[:token].provider_name } # Sort providers: revocable first, manual last provider_order = %w[GitHub AWS Anthropic OpenAI] sorted_providers = by_provider.keys.sort_by do |name| idx = provider_order.index(name) idx.nil? ? provider_order.size : idx end lines = [] lines << "# Tokens to Revoke" lines << "" lines << "**Scan**: #{scanned_at.strftime("%Y-%m-%d %H:%M:%S")} | " \ "**Unique tokens**: #{deduped.size} | " \ "**Providers**: #{by_provider.size}" lines << "" sorted_providers.each do |provider_name| provider_tokens = by_provider[provider_name] lines << "## #{provider_name} (#{provider_tokens.size} token#{"s" if provider_tokens.size != 1})" lines << "" provider_tokens.each_with_index do |entry, idx| token = entry[:token] locations = entry[:locations] lines << "### #{idx + 1}. `#{token.masked_value}` (#{token.token_type})" lines << "" lines << "**Locations:**" locations.each do |loc| line_suffix = loc[:line] ? ":#{loc[:line]}" : "" lines << "- `#{loc[:commit]}` #{loc[:file]}#{line_suffix}" end lines << "" end end lines.join("\n") end |
#to_summary(report_path: nil) ⇒ String
Generate concise summary for stdout
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 241 def to_summary(report_path: nil) lines = [] # Timing and thread info timing = scan_duration ? "in #{format_duration(scan_duration)}" : "" threads = thread_count ? " (#{thread_count} threads)" : "" lines << "Scan completed#{" " + timing unless timing.empty?}#{threads}" # Token counts lines << if clean? "No tokens detected. Repository is clean." else "Tokens found: #{token_count} (high: #{high_confidence_count}, medium: #{medium_confidence_count})" end # Report path lines << "Report saved: #{report_path}" if report_path lines.join("\n") end |
#to_table ⇒ String
Format as table for CLI output
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 159 def to_table return "No tokens detected. Repository is clean." if clean? lines = [] lines << "Scan Report: #{repository_path}" lines << "=" * 60 lines << "Scanned at: #{scanned_at}" lines << "Commits scanned: #{commits_scanned}" lines << "Detection method: #{detection_method}" lines << "" lines << "Summary:" lines << " Total tokens: #{token_count}" lines << " High confidence: #{high_confidence_count}" lines << " Medium confidence: #{medium_confidence_count}" lines << " Low confidence: #{low_confidence_count}" lines << "" lines << "Detected Tokens:" lines << "-" * 60 tokens.each_with_index do |token, idx| lines << "#{idx + 1}. #{token.token_type} (#{token.confidence})" lines << " Value: #{token.masked_value}" lines << " Commit: #{token.short_commit}" lines << " File: #{token.file_path}#{":#{token.line_number}" if token.line_number}" lines << " Detected by: #{token.detected_by}" lines << "" end lines.join("\n") end |
#to_yaml(include_raw: false) ⇒ String
Serialize to YAML
153 154 155 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 153 def to_yaml(include_raw: false) to_h(include_raw: include_raw).to_yaml end |
#token_count ⇒ Integer
Total number of detected tokens
52 53 54 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 52 def token_count tokens.size end |
#token_types ⇒ Array<String>
Get unique token types found
83 84 85 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 83 def token_types tokens.map(&:token_type).uniq.sort end |
#tokens_by_confidence(level) ⇒ Array<DetectedToken>
Get tokens by confidence level
59 60 61 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 59 def tokens_by_confidence(level) tokens.select { |t| t.confidence == level } end |
#tokens_found? ⇒ Boolean
Check if tokens were detected
46 47 48 |
# File 'lib/ace/git/secrets/models/scan_report.rb', line 46 def tokens_found? !clean? end |