fiber_audit 0.1.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.fiber-audit.example.yml +33 -0
  3. data/CHANGELOG.md +32 -0
  4. data/README.md +150 -0
  5. data/bin/fiber-audit +5 -0
  6. data/lib/fiber_audit/audit.rb +288 -0
  7. data/lib/fiber_audit/cli.rb +245 -0
  8. data/lib/fiber_audit/configuration.rb +236 -0
  9. data/lib/fiber_audit/correlation/fingerprint.rb +26 -0
  10. data/lib/fiber_audit/errors.rb +8 -0
  11. data/lib/fiber_audit/execution_context.rb +47 -0
  12. data/lib/fiber_audit/findings/collection.rb +58 -0
  13. data/lib/fiber_audit/findings/confidence.rb +19 -0
  14. data/lib/fiber_audit/findings/evidence.rb +13 -0
  15. data/lib/fiber_audit/findings/finding.rb +76 -0
  16. data/lib/fiber_audit/findings/location.rb +9 -0
  17. data/lib/fiber_audit/findings/severity.rb +19 -0
  18. data/lib/fiber_audit/project.rb +96 -0
  19. data/lib/fiber_audit/reporters/base.rb +12 -0
  20. data/lib/fiber_audit/reporters/json.rb +34 -0
  21. data/lib/fiber_audit/reporters/schema.rb +574 -0
  22. data/lib/fiber_audit/reporters/text.rb +179 -0
  23. data/lib/fiber_audit/static/call_site.rb +71 -0
  24. data/lib/fiber_audit/static/call_site_extractor.rb +524 -0
  25. data/lib/fiber_audit/static/execution_context_resolver.rb +266 -0
  26. data/lib/fiber_audit/static/rules/base.rb +185 -0
  27. data/lib/fiber_audit/static/rules/blocking_subprocess.rb +94 -0
  28. data/lib/fiber_audit/static/rules/built_ins.rb +36 -0
  29. data/lib/fiber_audit/static/rules/direct_socket.rb +112 -0
  30. data/lib/fiber_audit/static/rules/io_select.rb +104 -0
  31. data/lib/fiber_audit/static/rules/net_http_in_request.rb +116 -0
  32. data/lib/fiber_audit/static/rules/registry.rb +123 -0
  33. data/lib/fiber_audit/static/rules/synchronization.rb +124 -0
  34. data/lib/fiber_audit/static/rules/thread_current_state.rb +113 -0
  35. data/lib/fiber_audit/static/rules/thread_join.rb +96 -0
  36. data/lib/fiber_audit/static/semantic_index.rb +300 -0
  37. data/lib/fiber_audit/suppressions/parser.rb +146 -0
  38. data/lib/fiber_audit/suppressions/store.rb +63 -0
  39. data/lib/fiber_audit/version.rb +5 -0
  40. data/lib/fiber_audit.rb +40 -0
  41. metadata +108 -0
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'schema'
5
+ require_relative '../version'
6
+ require_relative '../findings/severity'
7
+
8
+ module FiberAudit
9
+ module Reporters
10
+ # Text reporter that outputs human-readable audit results.
11
+ # Calls Schema.build then Schema.validate! to enforce the contract.
12
+ #
13
+ # Exact contract:
14
+ # - Header: `FiberAudit 0.1.0 — static analysis`
15
+ # - Summary counts, suppressed, status
16
+ # - Disclaimer under status
17
+ # - Findings: RULE SEVERITY path:line
18
+ # - Optional: symbol — operation [context]
19
+ # - message
20
+ # - (unknown location) fallback
21
+ # - Footer: Run `fiber-audit explain <RULE_ID>` for rule details.
22
+ # - ANSI escape constants only in Text::ANSI
23
+ # - Color ONLY severity labels, not footer/header
24
+ class Text < Base
25
+ # ANSI owns all escape sequences. Color is applied only to severity labels.
26
+ module ANSI
27
+ RESET = "\e[0m"
28
+ BOLD = "\e[1m"
29
+ DIM = "\e[2m"
30
+
31
+ RED = "\e[31m"
32
+ YELLOW = "\e[33m"
33
+ MAGENTA = "\e[35m"
34
+ CYAN = "\e[36m"
35
+ WHITE = "\e[37m"
36
+
37
+ SEVERITY_COLORS = {
38
+ critical: RED,
39
+ high: YELLOW,
40
+ medium: MAGENTA,
41
+ low: CYAN,
42
+ info: CYAN
43
+ }.freeze
44
+
45
+ module_function
46
+
47
+ def colorize(text, color_code)
48
+ "#{color_code}#{text}#{RESET}"
49
+ end
50
+
51
+ def severity_label(severity, color:)
52
+ label = severity.to_s.upcase
53
+ return label unless color
54
+
55
+ color_code = SEVERITY_COLORS[severity.to_sym] || WHITE
56
+ colorize(label, color_code)
57
+ end
58
+ end
59
+
60
+ def initialize(color: false)
61
+ super()
62
+ @color = color
63
+ end
64
+
65
+ def render(result)
66
+ # Build normalized primitive report hash from result protocol
67
+ report_hash = Schema.build(result)
68
+
69
+ # Validate the hash (also freezes it)
70
+ Schema.validate!(report_hash)
71
+
72
+ lines = []
73
+
74
+ # Header: exact contract
75
+ lines << header
76
+ lines << ''
77
+
78
+ # Summary counts and suppressed/status
79
+ lines << summary_section(report_hash[:summary])
80
+ lines << ''
81
+
82
+ # Status
83
+ lines << status_section(report_hash[:status])
84
+
85
+ # Mandatory disclaimer under status
86
+ lines << disclaimer_section
87
+ lines << ''
88
+
89
+ # Findings
90
+ lines << findings_section(report_hash[:findings])
91
+ lines << ''
92
+
93
+ # Footer hint is always present, including no-findings reports.
94
+ lines << footer_hint
95
+
96
+ "#{lines.join("\n").chomp}\n"
97
+ end
98
+
99
+ private
100
+
101
+ def header
102
+ "FiberAudit #{FiberAudit::VERSION} — static analysis"
103
+ end
104
+
105
+ def summary_section(summary)
106
+ counts = %i[critical high medium low info].map do |severity|
107
+ "#{severity}: #{summary[severity]}"
108
+ end
109
+
110
+ [
111
+ 'Summary',
112
+ " #{counts.join(' ')}",
113
+ " suppressed: #{summary[:suppressed]}",
114
+ " total: #{summary[:total]}"
115
+ ].join("\n")
116
+ end
117
+
118
+ def status_section(status)
119
+ " status: #{status}"
120
+ end
121
+
122
+ def disclaimer_section
123
+ Schema::DISCLAIMER
124
+ end
125
+
126
+ def findings_section(findings)
127
+ return 'No findings.' if findings.empty?
128
+
129
+ lines = ['Findings']
130
+ findings.each do |finding|
131
+ lines << format_finding(finding)
132
+ end
133
+ lines.join("\n")
134
+ end
135
+
136
+ def format_finding(finding)
137
+ lines = []
138
+ severity = finding[:severity].to_sym
139
+ label = ANSI.severity_label(severity, color: @color)
140
+
141
+ # RULE SEVERITY path:line
142
+ location_str = format_location(finding[:location])
143
+ lines << " #{finding[:rule_id]} #{label} #{location_str}"
144
+
145
+ # Optional: symbol — operation [context]
146
+ optional_parts = []
147
+ optional_parts << finding[:symbol] if finding[:symbol]
148
+ if finding[:operation]
149
+ if finding[:symbol]
150
+ optional_parts[-1] = "#{finding[:symbol]} — #{finding[:operation]}"
151
+ else
152
+ optional_parts << finding[:operation]
153
+ end
154
+ end
155
+ optional_parts << "[#{finding[:execution_context]}]" if finding[:execution_context]
156
+
157
+ lines << " #{optional_parts.join(' ')}" if optional_parts.any?
158
+
159
+ # message
160
+ lines << " #{finding[:message]}"
161
+
162
+ lines.join("\n")
163
+ end
164
+
165
+ def format_location(location)
166
+ return '(unknown location)' if location.nil?
167
+
168
+ path = location[:path].to_s
169
+ line = location[:line]
170
+
171
+ line ? "#{path}:#{line}" : path
172
+ end
173
+
174
+ def footer_hint
175
+ 'Run `fiber-audit explain <RULE_ID>` for rule details.'
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../findings/location'
4
+ require_relative '../findings/confidence'
5
+
6
+ module FiberAudit
7
+ module Static
8
+ # CallSite represents a single method call extracted from source code.
9
+ # Carries exactly 13 fields as specified in the remediation plan.
10
+ CallSite = Data.define(
11
+ :path, :line, :column,
12
+ :receiver_source, :receiver_constant, :method_name,
13
+ :arguments, :enclosing_symbol, :nesting,
14
+ :execution_context, :resolution, :confidence
15
+ ) do
16
+ # Override initialize to ensure proper contract:
17
+ # - method_name normalized to Symbol or nil
18
+ # - confidence validated via Confidence.coerce
19
+ # - arguments and nesting frozen to prevent caller mutation
20
+ def initialize(
21
+ path:, line:, column:,
22
+ receiver_source:, receiver_constant:, method_name:,
23
+ arguments:, enclosing_symbol:, nesting:,
24
+ execution_context:, resolution:, confidence:
25
+ )
26
+ # Normalize method_name to Symbol if it's a String, preserve nil
27
+ method_name_value = case method_name
28
+ when String
29
+ method_name.to_sym
30
+ when Symbol, nil
31
+ method_name
32
+ else
33
+ raise ArgumentError,
34
+ "method_name must be String, Symbol, or nil, got #{method_name.class}"
35
+ end
36
+
37
+ # Validate confidence via Confidence.coerce
38
+ validated_confidence = Confidence.coerce(confidence)
39
+
40
+ # Freeze duplicates of mutable collections to prevent caller mutation
41
+ frozen_arguments = arguments.dup.freeze
42
+ frozen_nesting = nesting.dup.freeze
43
+
44
+ super(
45
+ path: path,
46
+ line: line,
47
+ column: column,
48
+ receiver_source: receiver_source,
49
+ receiver_constant: receiver_constant,
50
+ method_name: method_name_value,
51
+ arguments: frozen_arguments,
52
+ enclosing_symbol: enclosing_symbol,
53
+ nesting: frozen_nesting,
54
+ execution_context: execution_context,
55
+ resolution: resolution,
56
+ confidence: validated_confidence
57
+ )
58
+ end
59
+
60
+ # Returns a Location object for this call site
61
+ def location
62
+ FiberAudit::Location.new(path: path, line: line, column: column)
63
+ end
64
+
65
+ # Returns method_name as a Symbol or nil (convenience accessor)
66
+ def method_name_sym
67
+ method_name
68
+ end
69
+ end
70
+ end
71
+ end