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,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
Finding = Data.define(
|
|
5
|
+
:rule_id,
|
|
6
|
+
:title,
|
|
7
|
+
:category,
|
|
8
|
+
:severity,
|
|
9
|
+
:confidence,
|
|
10
|
+
:location,
|
|
11
|
+
:symbol,
|
|
12
|
+
:operation,
|
|
13
|
+
:execution_context,
|
|
14
|
+
:message,
|
|
15
|
+
:evidence,
|
|
16
|
+
:remediation,
|
|
17
|
+
:fingerprint
|
|
18
|
+
) do
|
|
19
|
+
def initialize(
|
|
20
|
+
rule_id:,
|
|
21
|
+
category:, severity:, confidence:, message:, title: nil,
|
|
22
|
+
location: nil,
|
|
23
|
+
symbol: nil,
|
|
24
|
+
operation: nil,
|
|
25
|
+
execution_context: nil,
|
|
26
|
+
evidence: [],
|
|
27
|
+
remediation: nil,
|
|
28
|
+
fingerprint: nil
|
|
29
|
+
)
|
|
30
|
+
severity = Severity.coerce(severity)
|
|
31
|
+
confidence = Confidence.coerce(confidence)
|
|
32
|
+
evidence = Array(evidence).freeze
|
|
33
|
+
|
|
34
|
+
fingerprint ||= Correlation::Fingerprint.call(
|
|
35
|
+
rule_id: rule_id,
|
|
36
|
+
path: location&.path,
|
|
37
|
+
enclosing_symbol: symbol,
|
|
38
|
+
operation: operation
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
super(
|
|
42
|
+
rule_id: rule_id,
|
|
43
|
+
title: title,
|
|
44
|
+
category: category,
|
|
45
|
+
severity: severity,
|
|
46
|
+
confidence: confidence,
|
|
47
|
+
location: location,
|
|
48
|
+
symbol: symbol,
|
|
49
|
+
operation: operation,
|
|
50
|
+
execution_context: execution_context,
|
|
51
|
+
message: message,
|
|
52
|
+
evidence: evidence,
|
|
53
|
+
remediation: remediation,
|
|
54
|
+
fingerprint: fingerprint
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h_for_json
|
|
59
|
+
{
|
|
60
|
+
rule_id: rule_id,
|
|
61
|
+
title: title,
|
|
62
|
+
category: category,
|
|
63
|
+
severity: severity,
|
|
64
|
+
confidence: confidence,
|
|
65
|
+
location: location&.to_h_for_json,
|
|
66
|
+
symbol: symbol,
|
|
67
|
+
operation: operation,
|
|
68
|
+
execution_context: execution_context,
|
|
69
|
+
message: message,
|
|
70
|
+
evidence: evidence.map(&:to_h_for_json),
|
|
71
|
+
remediation: remediation,
|
|
72
|
+
fingerprint: fingerprint
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Severity
|
|
5
|
+
LEVELS = %i[critical high medium low info].freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def coerce(value)
|
|
10
|
+
return value if LEVELS.include?(value)
|
|
11
|
+
|
|
12
|
+
raise ArgumentError, "unknown severity: #{value.inspect}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def index(severity)
|
|
16
|
+
LEVELS.index(severity) || LEVELS.size
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
|
|
5
|
+
module FiberAudit
|
|
6
|
+
class Project
|
|
7
|
+
MARKERS = %w[Gemfile gems.rb config/application.rb].freeze
|
|
8
|
+
CONFIG_FILENAME = '.fiber-audit.yml'
|
|
9
|
+
|
|
10
|
+
attr_reader :root, :invocation_path, :note
|
|
11
|
+
|
|
12
|
+
def initialize(root:, invocation_path:, known:, note:)
|
|
13
|
+
@root = root.freeze
|
|
14
|
+
@invocation_path = invocation_path.freeze
|
|
15
|
+
@known = known
|
|
16
|
+
@note = note&.freeze
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def known?
|
|
21
|
+
@known
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def config_path(override = nil)
|
|
25
|
+
if override
|
|
26
|
+
resolve_override(override)
|
|
27
|
+
else
|
|
28
|
+
File.join(root, CONFIG_FILENAME)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def detect(start_path: Dir.pwd)
|
|
34
|
+
validate_start_path(start_path)
|
|
35
|
+
|
|
36
|
+
invocation_path = resolve_invocation_path(start_path)
|
|
37
|
+
detected_root = find_project_root(invocation_path)
|
|
38
|
+
|
|
39
|
+
if detected_root
|
|
40
|
+
new(
|
|
41
|
+
root: detected_root,
|
|
42
|
+
invocation_path: invocation_path,
|
|
43
|
+
known: true,
|
|
44
|
+
note: nil
|
|
45
|
+
)
|
|
46
|
+
else
|
|
47
|
+
new(
|
|
48
|
+
root: invocation_path,
|
|
49
|
+
invocation_path: invocation_path,
|
|
50
|
+
known: false,
|
|
51
|
+
note: :unknown_project
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def validate_start_path(path)
|
|
59
|
+
return if File.exist?(path)
|
|
60
|
+
|
|
61
|
+
raise ProjectError, "start_path does not exist: #{path}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def resolve_invocation_path(start_path)
|
|
65
|
+
expanded = File.expand_path(start_path)
|
|
66
|
+
File.file?(start_path) ? File.dirname(expanded) : expanded
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_project_root(start_dir)
|
|
70
|
+
current = start_dir
|
|
71
|
+
loop do
|
|
72
|
+
return current if markers_present?(current)
|
|
73
|
+
|
|
74
|
+
parent = File.dirname(current)
|
|
75
|
+
return nil if parent == current
|
|
76
|
+
|
|
77
|
+
current = parent
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def markers_present?(dir)
|
|
82
|
+
MARKERS.any? { |marker| File.exist?(File.join(dir, marker)) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def resolve_override(override)
|
|
89
|
+
if File.absolute_path?(override)
|
|
90
|
+
File.expand_path(override)
|
|
91
|
+
else
|
|
92
|
+
File.expand_path(override, invocation_path)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Reporters
|
|
5
|
+
# Base class for all reporters. Subclasses must implement #render.
|
|
6
|
+
class Base
|
|
7
|
+
def render(_result)
|
|
8
|
+
raise NotImplementedError, "#{self.class}#render must be implemented"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'schema'
|
|
6
|
+
|
|
7
|
+
module FiberAudit
|
|
8
|
+
module Reporters
|
|
9
|
+
# JSON reporter that outputs deterministic, schema-validated JSON.
|
|
10
|
+
# Calls Schema.build then Schema.validate! to enforce the contract.
|
|
11
|
+
class JSON < Base
|
|
12
|
+
def initialize(pretty: false)
|
|
13
|
+
super()
|
|
14
|
+
@pretty = pretty
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render(result)
|
|
18
|
+
# Build normalized primitive report hash from result protocol
|
|
19
|
+
report_hash = Schema.build(result)
|
|
20
|
+
|
|
21
|
+
# Validate the hash and return it (also freezes it)
|
|
22
|
+
Schema.validate!(report_hash)
|
|
23
|
+
|
|
24
|
+
json_string = if @pretty
|
|
25
|
+
::JSON.pretty_generate(report_hash)
|
|
26
|
+
else
|
|
27
|
+
::JSON.generate(report_hash)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
"#{json_string}\n"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|