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,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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberAudit
4
+ Location = Data.define(:path, :line, :column) do
5
+ def to_h_for_json
6
+ { path: path, line: line, column: column }
7
+ end
8
+ end
9
+ 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