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.
- checksums.yaml +7 -0
- data/.fiber-audit.example.yml +33 -0
- data/CHANGELOG.md +32 -0
- data/README.md +150 -0
- data/bin/fiber-audit +5 -0
- data/lib/fiber_audit/audit.rb +288 -0
- data/lib/fiber_audit/cli.rb +245 -0
- data/lib/fiber_audit/configuration.rb +236 -0
- data/lib/fiber_audit/correlation/fingerprint.rb +26 -0
- data/lib/fiber_audit/errors.rb +8 -0
- data/lib/fiber_audit/execution_context.rb +47 -0
- data/lib/fiber_audit/findings/collection.rb +58 -0
- data/lib/fiber_audit/findings/confidence.rb +19 -0
- data/lib/fiber_audit/findings/evidence.rb +13 -0
- data/lib/fiber_audit/findings/finding.rb +76 -0
- data/lib/fiber_audit/findings/location.rb +9 -0
- data/lib/fiber_audit/findings/severity.rb +19 -0
- data/lib/fiber_audit/project.rb +96 -0
- data/lib/fiber_audit/reporters/base.rb +12 -0
- data/lib/fiber_audit/reporters/json.rb +34 -0
- data/lib/fiber_audit/reporters/schema.rb +574 -0
- data/lib/fiber_audit/reporters/text.rb +179 -0
- data/lib/fiber_audit/static/call_site.rb +71 -0
- data/lib/fiber_audit/static/call_site_extractor.rb +524 -0
- data/lib/fiber_audit/static/execution_context_resolver.rb +266 -0
- data/lib/fiber_audit/static/rules/base.rb +185 -0
- data/lib/fiber_audit/static/rules/blocking_subprocess.rb +94 -0
- data/lib/fiber_audit/static/rules/built_ins.rb +36 -0
- data/lib/fiber_audit/static/rules/direct_socket.rb +112 -0
- data/lib/fiber_audit/static/rules/io_select.rb +104 -0
- data/lib/fiber_audit/static/rules/net_http_in_request.rb +116 -0
- data/lib/fiber_audit/static/rules/registry.rb +123 -0
- data/lib/fiber_audit/static/rules/synchronization.rb +124 -0
- data/lib/fiber_audit/static/rules/thread_current_state.rb +113 -0
- data/lib/fiber_audit/static/rules/thread_join.rb +96 -0
- data/lib/fiber_audit/static/semantic_index.rb +300 -0
- data/lib/fiber_audit/suppressions/parser.rb +146 -0
- data/lib/fiber_audit/suppressions/store.rb +63 -0
- data/lib/fiber_audit/version.rb +5 -0
- data/lib/fiber_audit.rb +40 -0
- 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
|