Class: Pikuri::Trifecta::Report

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/trifecta/report.rb

Overview

The result of walking one agent tree: a Node with its legs folded — its own tools unioned with everything propagated up from its sub-agents. What walk returns, mirroring the tree shape so a host can both classify and display it:

report = Pikuri::Trifecta.walk(node)
report.tree_verdict                  # => :soft
report.worst_path.map(&:label)       # => ["pikuri-computer", "RESEARCHER"]
puts report.render                   # the human-readable map

Implementation details

#verdict is derived, never stored: Data#with would otherwise let a copy carry a severity that contradicts its own legs. It reads presence (leg-absence, correct for any level ever added) and matches strength exhaustively across all three graded axes, so introducing a lattice level without placing it in the grid raises on the first wiring that reaches it rather than being silently absorbed into :soft.

Immutable.

Constant Summary collapse

SEVERITY =

Verdicts, least to most severe. The only ordering outside Pikuri::Tool::TrifectaLegs.

Returns:

  • (Array<Symbol>)
i[silent soft loud].freeze
'See book/trifecta-detector.md in this repo.'

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#channel_egressSymbol? (readonly)

Returns the parent edge's gate level (see Node#channel_egress).

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/pikuri/trifecta/report.rb', line 39

class Report < Data.define(:label, :legs, :tool_legs, :children, :channel_egress)
  # Verdicts, least to most severe. The only ordering outside
  # {Pikuri::Tool::TrifectaLegs}.
  #
  # @return [Array<Symbol>]
  SEVERITY = i[silent soft loud].freeze

  # Closing pointer of {#advisory}. Names the chapter by *slug*, not by
  # ordinal: book chapters are ordered only by `book/README.md`, so this
  # path survives a reordering that would silently falsify a number.
  #
  # @return [String]
  FOOTER = 'See book/trifecta-detector.md in this repo.'

  # How dangerous *this node* is — the tree's verdict is {#tree_verdict}.
  #
  # +:loud+ needs all three legs at full strength; +:soft+ is any complete
  # trifecta whose untrusted or egress leg is attenuated; +:silent+ is a
  # missing leg. Absence of a warning is not evidence of safety: this
  # reasons about capability presence, not data flow.
  #
  # @return [Symbol] +:silent+, +:soft+ or +:loud+
  # @raise [NoMatchingPatternError] if a lattice level exists that this grid
  #   does not place — deliberate, so a new level cannot be absorbed silently
  def verdict
    return :silent unless legs.private && legs.untrusted != :none && legs.egress?

    case [legs.untrusted, legs.egress_payload_review, legs.egress_destination]
    in [:hard,     :unreviewed,      :attacker_reachable] then :loud
    in [:hard,     :unreviewed,      :tool_vouched]       then :soft
    in [:hard,     :human_reviewed,  :attacker_reachable] then :soft
    in [:hard,     :human_reviewed,  :tool_vouched]       then :soft
    in [:weakened, :unreviewed,      :attacker_reachable] then :soft
    in [:weakened, :unreviewed,      :tool_vouched]       then :soft
    in [:weakened, :human_reviewed,  :attacker_reachable] then :soft
    in [:weakened, :human_reviewed,  :tool_vouched]       then :soft
    end
  end

  # Root-first path of Reports down to the most severe node in this subtree,
  # so the caller can both attribute the finding and read its cell:
  #
  #   path = report.worst_path
  #   path.map(&:label).join(' -> ')   # => "pikuri-computer -> RESEARCHER"
  #   path.last.legs                   # the cell that decided the verdict
  #
  # Ties go to the node encountered first in pre-order — the parent over its
  # own children, since a parent's trifecta is the more actionable finding.
  #
  # @return [Array<Report>] never empty; +[self]+ when nothing below is worse
  def worst_path
    best = [self]
    children.each do |child|
      candidate = [self, *child.worst_path]
      best = candidate if SEVERITY.index(candidate.last.verdict) > SEVERITY.index(best.last.verdict)
    end
    best
  end

  # @return [Symbol] the most severe {#verdict} anywhere in this subtree
  def tree_verdict = worst_path.last.verdict

  # The human-readable map: one line per agent and per tool, indented by
  # depth, with each entry's legs as tags.
  #
  #   Trifecta: soft
  #
  #   pikuri-computer [:private, :untrusted_hard, :egress_reviewed]
  #     mail_compose [:private, :egress_reviewed]
  #     calculator []
  #     RESEARCHER [gate: human_reviewed] [:untrusted_hard, :egress]
  #       fetch [:untrusted_hard, :egress]
  #
  #   See book/trifecta-detector.md in this repo for ...
  #
  # An agent line carries its *folded* legs and a tool line its own, so
  # propagation shows up as layout: the parent above holds egress that no
  # tool of its own contributes. Plain text by design — a host wanting color
  # or width walks this tree itself rather than asking for styled output.
  #
  # @return [String] newline-separated, no trailing newline
  def render
    [advisory, '', *render_lines].join("\n")
  end

  # The one-line conclusion: what was found, where, and the leg to break.
  # Separate from {#render} because a status line wants this without the
  # tree, and because it is the half that carries *meaning* — the map
  # deliberately carries only facts.
  #
  #   [TRIFECTA: HARD] pikuri-code — private(read, bash) + untrusted(read,
  #   bash) + egress(bash). An injection in what this agent reads can drive
  #   an exfiltration. Break a leg: sever egress (a network-off sandbox), or
  #   privilege-separate the step that reads untrusted content.
  #   See book/trifecta-detector.md in this repo.
  #
  # A clean verdict never says "no trifecta detected". This reasons about
  # capability presence, so a quiet result is not a clean bill of health —
  # it under-warns on a single tool that is all three internally, and on
  # private data staged into a file by one tool and read by another.
  # Reporting the *finding* names what would tip the wiring over and claims
  # nothing it cannot support.
  #
  # @return [String]
  def advisory
    path = worst_path
    where = path.map(&:label).join(' -> ')
    legs = path.last.legs
    return "Trifecta: #{where} — #{presence(legs)}." if tree_verdict == :silent

    "[TRIFECTA: #{tree_verdict == :loud ? 'HARD' : 'soft'}] #{where} — " \
      "#{attribution(path.last)}. An injection in what this agent reads can drive " \
      "an exfiltration. Break a leg: #{advice(legs)}. #{FOOTER}"
  end

  protected

  # @param depth [Integer] indentation level
  # @return [Array<String>]
  def render_lines(depth = 0)
    indent = '  ' * depth
    gate = channel_egress ? " [gate: #{channel_egress}]" : ''
    lines = ["#{indent}#{label}#{gate} #{legs.to_tags.inspect}"]
    tool_legs.each { |name, tool| lines << "#{indent}  #{name} #{tool.to_tags.inspect}" }
    children.each { |child| lines.concat(child.render_lines(depth + 1)) }
    lines
  end

  private

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String] e.g. +"2 of 3 legs (private, untrusted); no egress leg found"+
  def presence(legs)
    held = { 'private' => legs.private, 'untrusted' => legs.untrusted != :none,
             'egress' => legs.egress? }
    "#{held.count { |_, v| v }} of 3 legs (#{held.select { |_, v| v }.keys.join(', ')}); " \
      "no #{held.reject { |_, v| v }.keys.join('/')} leg found"
  end

  # Which tool contributed each leg, so the line names something the reader
  # can actually go and change.
  #
  # @param node [Report]
  # @return [String]
  def attribution(node)
    i[private untrusted egress].filter_map do |axis|
      contributors = node.tool_legs.select { |_, l| leg_present?(l, axis) }.keys
      "#{axis}(#{contributors.join(', ')})" unless contributors.empty?
    end.join(' + ')
  end

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @param axis [Symbol] +:private+, +:untrusted+ or +:egress+
  # @return [Boolean]
  def leg_present?(legs, axis)
    case axis
    in :private then legs.private
    in :untrusted then legs.untrusted != :none
    in :egress then legs.egress?
    end
  end

  # The fix, per cell of the verdict grid — the soft cells want different
  # answers, which is why this reads the legs rather than the verdict symbol.
  #
  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String]
  def advice(legs)
    # A vouched destination is fragile in a way the other attenuations are
    # not: it survives only while it is the *sole* egress leg, since the
    # union hardens the whole node the moment one attacker-reachable tool
    # joins it. Say so, rather than let the soft verdict read as settled.
    if legs.egress_destination == :tool_vouched
      return 'sever egress outright — bytes still leave, they merely reach a destination the ' \
             'attacker cannot read back, and one attacker-reachable tool re-hardens this node'
    end

    case [legs.untrusted, legs.egress_payload_review]
    in [:hard, :unreviewed]
      'sever egress (a network-off sandbox), or privilege-separate the step that reads untrusted content'
    in [:hard, _]
      'privilege-separate the step that reads untrusted content, or declare the workspace trusted ' \
        'if you vouch for every byte it can reach'
    in [:weakened, :unreviewed]
      'sever egress — the untrusted leg is already attenuated by delegation'
    in [:weakened, _]
      'both legs are already attenuated; narrow what counts as private, or drop a leg outright'
    end
  end
end

#childrenArray<Report> (readonly)

Returns one per sub-agent, already walked.

Returns:

  • (Array<Report>)

    one per sub-agent, already walked



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/pikuri/trifecta/report.rb', line 39

class Report < Data.define(:label, :legs, :tool_legs, :children, :channel_egress)
  # Verdicts, least to most severe. The only ordering outside
  # {Pikuri::Tool::TrifectaLegs}.
  #
  # @return [Array<Symbol>]
  SEVERITY = i[silent soft loud].freeze

  # Closing pointer of {#advisory}. Names the chapter by *slug*, not by
  # ordinal: book chapters are ordered only by `book/README.md`, so this
  # path survives a reordering that would silently falsify a number.
  #
  # @return [String]
  FOOTER = 'See book/trifecta-detector.md in this repo.'

  # How dangerous *this node* is — the tree's verdict is {#tree_verdict}.
  #
  # +:loud+ needs all three legs at full strength; +:soft+ is any complete
  # trifecta whose untrusted or egress leg is attenuated; +:silent+ is a
  # missing leg. Absence of a warning is not evidence of safety: this
  # reasons about capability presence, not data flow.
  #
  # @return [Symbol] +:silent+, +:soft+ or +:loud+
  # @raise [NoMatchingPatternError] if a lattice level exists that this grid
  #   does not place — deliberate, so a new level cannot be absorbed silently
  def verdict
    return :silent unless legs.private && legs.untrusted != :none && legs.egress?

    case [legs.untrusted, legs.egress_payload_review, legs.egress_destination]
    in [:hard,     :unreviewed,      :attacker_reachable] then :loud
    in [:hard,     :unreviewed,      :tool_vouched]       then :soft
    in [:hard,     :human_reviewed,  :attacker_reachable] then :soft
    in [:hard,     :human_reviewed,  :tool_vouched]       then :soft
    in [:weakened, :unreviewed,      :attacker_reachable] then :soft
    in [:weakened, :unreviewed,      :tool_vouched]       then :soft
    in [:weakened, :human_reviewed,  :attacker_reachable] then :soft
    in [:weakened, :human_reviewed,  :tool_vouched]       then :soft
    end
  end

  # Root-first path of Reports down to the most severe node in this subtree,
  # so the caller can both attribute the finding and read its cell:
  #
  #   path = report.worst_path
  #   path.map(&:label).join(' -> ')   # => "pikuri-computer -> RESEARCHER"
  #   path.last.legs                   # the cell that decided the verdict
  #
  # Ties go to the node encountered first in pre-order — the parent over its
  # own children, since a parent's trifecta is the more actionable finding.
  #
  # @return [Array<Report>] never empty; +[self]+ when nothing below is worse
  def worst_path
    best = [self]
    children.each do |child|
      candidate = [self, *child.worst_path]
      best = candidate if SEVERITY.index(candidate.last.verdict) > SEVERITY.index(best.last.verdict)
    end
    best
  end

  # @return [Symbol] the most severe {#verdict} anywhere in this subtree
  def tree_verdict = worst_path.last.verdict

  # The human-readable map: one line per agent and per tool, indented by
  # depth, with each entry's legs as tags.
  #
  #   Trifecta: soft
  #
  #   pikuri-computer [:private, :untrusted_hard, :egress_reviewed]
  #     mail_compose [:private, :egress_reviewed]
  #     calculator []
  #     RESEARCHER [gate: human_reviewed] [:untrusted_hard, :egress]
  #       fetch [:untrusted_hard, :egress]
  #
  #   See book/trifecta-detector.md in this repo for ...
  #
  # An agent line carries its *folded* legs and a tool line its own, so
  # propagation shows up as layout: the parent above holds egress that no
  # tool of its own contributes. Plain text by design — a host wanting color
  # or width walks this tree itself rather than asking for styled output.
  #
  # @return [String] newline-separated, no trailing newline
  def render
    [advisory, '', *render_lines].join("\n")
  end

  # The one-line conclusion: what was found, where, and the leg to break.
  # Separate from {#render} because a status line wants this without the
  # tree, and because it is the half that carries *meaning* — the map
  # deliberately carries only facts.
  #
  #   [TRIFECTA: HARD] pikuri-code — private(read, bash) + untrusted(read,
  #   bash) + egress(bash). An injection in what this agent reads can drive
  #   an exfiltration. Break a leg: sever egress (a network-off sandbox), or
  #   privilege-separate the step that reads untrusted content.
  #   See book/trifecta-detector.md in this repo.
  #
  # A clean verdict never says "no trifecta detected". This reasons about
  # capability presence, so a quiet result is not a clean bill of health —
  # it under-warns on a single tool that is all three internally, and on
  # private data staged into a file by one tool and read by another.
  # Reporting the *finding* names what would tip the wiring over and claims
  # nothing it cannot support.
  #
  # @return [String]
  def advisory
    path = worst_path
    where = path.map(&:label).join(' -> ')
    legs = path.last.legs
    return "Trifecta: #{where} — #{presence(legs)}." if tree_verdict == :silent

    "[TRIFECTA: #{tree_verdict == :loud ? 'HARD' : 'soft'}] #{where} — " \
      "#{attribution(path.last)}. An injection in what this agent reads can drive " \
      "an exfiltration. Break a leg: #{advice(legs)}. #{FOOTER}"
  end

  protected

  # @param depth [Integer] indentation level
  # @return [Array<String>]
  def render_lines(depth = 0)
    indent = '  ' * depth
    gate = channel_egress ? " [gate: #{channel_egress}]" : ''
    lines = ["#{indent}#{label}#{gate} #{legs.to_tags.inspect}"]
    tool_legs.each { |name, tool| lines << "#{indent}  #{name} #{tool.to_tags.inspect}" }
    children.each { |child| lines.concat(child.render_lines(depth + 1)) }
    lines
  end

  private

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String] e.g. +"2 of 3 legs (private, untrusted); no egress leg found"+
  def presence(legs)
    held = { 'private' => legs.private, 'untrusted' => legs.untrusted != :none,
             'egress' => legs.egress? }
    "#{held.count { |_, v| v }} of 3 legs (#{held.select { |_, v| v }.keys.join(', ')}); " \
      "no #{held.reject { |_, v| v }.keys.join('/')} leg found"
  end

  # Which tool contributed each leg, so the line names something the reader
  # can actually go and change.
  #
  # @param node [Report]
  # @return [String]
  def attribution(node)
    i[private untrusted egress].filter_map do |axis|
      contributors = node.tool_legs.select { |_, l| leg_present?(l, axis) }.keys
      "#{axis}(#{contributors.join(', ')})" unless contributors.empty?
    end.join(' + ')
  end

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @param axis [Symbol] +:private+, +:untrusted+ or +:egress+
  # @return [Boolean]
  def leg_present?(legs, axis)
    case axis
    in :private then legs.private
    in :untrusted then legs.untrusted != :none
    in :egress then legs.egress?
    end
  end

  # The fix, per cell of the verdict grid — the soft cells want different
  # answers, which is why this reads the legs rather than the verdict symbol.
  #
  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String]
  def advice(legs)
    # A vouched destination is fragile in a way the other attenuations are
    # not: it survives only while it is the *sole* egress leg, since the
    # union hardens the whole node the moment one attacker-reachable tool
    # joins it. Say so, rather than let the soft verdict read as settled.
    if legs.egress_destination == :tool_vouched
      return 'sever egress outright — bytes still leave, they merely reach a destination the ' \
             'attacker cannot read back, and one attacker-reachable tool re-hardens this node'
    end

    case [legs.untrusted, legs.egress_payload_review]
    in [:hard, :unreviewed]
      'sever egress (a network-off sandbox), or privilege-separate the step that reads untrusted content'
    in [:hard, _]
      'privilege-separate the step that reads untrusted content, or declare the workspace trusted ' \
        'if you vouch for every byte it can reach'
    in [:weakened, :unreviewed]
      'sever egress — the untrusted leg is already attenuated by delegation'
    in [:weakened, _]
      'both legs are already attenuated; narrow what counts as private, or drop a leg outright'
    end
  end
end

#labelString (readonly)

Returns this agent's name (see Node#label).

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/pikuri/trifecta/report.rb', line 39

class Report < Data.define(:label, :legs, :tool_legs, :children, :channel_egress)
  # Verdicts, least to most severe. The only ordering outside
  # {Pikuri::Tool::TrifectaLegs}.
  #
  # @return [Array<Symbol>]
  SEVERITY = i[silent soft loud].freeze

  # Closing pointer of {#advisory}. Names the chapter by *slug*, not by
  # ordinal: book chapters are ordered only by `book/README.md`, so this
  # path survives a reordering that would silently falsify a number.
  #
  # @return [String]
  FOOTER = 'See book/trifecta-detector.md in this repo.'

  # How dangerous *this node* is — the tree's verdict is {#tree_verdict}.
  #
  # +:loud+ needs all three legs at full strength; +:soft+ is any complete
  # trifecta whose untrusted or egress leg is attenuated; +:silent+ is a
  # missing leg. Absence of a warning is not evidence of safety: this
  # reasons about capability presence, not data flow.
  #
  # @return [Symbol] +:silent+, +:soft+ or +:loud+
  # @raise [NoMatchingPatternError] if a lattice level exists that this grid
  #   does not place — deliberate, so a new level cannot be absorbed silently
  def verdict
    return :silent unless legs.private && legs.untrusted != :none && legs.egress?

    case [legs.untrusted, legs.egress_payload_review, legs.egress_destination]
    in [:hard,     :unreviewed,      :attacker_reachable] then :loud
    in [:hard,     :unreviewed,      :tool_vouched]       then :soft
    in [:hard,     :human_reviewed,  :attacker_reachable] then :soft
    in [:hard,     :human_reviewed,  :tool_vouched]       then :soft
    in [:weakened, :unreviewed,      :attacker_reachable] then :soft
    in [:weakened, :unreviewed,      :tool_vouched]       then :soft
    in [:weakened, :human_reviewed,  :attacker_reachable] then :soft
    in [:weakened, :human_reviewed,  :tool_vouched]       then :soft
    end
  end

  # Root-first path of Reports down to the most severe node in this subtree,
  # so the caller can both attribute the finding and read its cell:
  #
  #   path = report.worst_path
  #   path.map(&:label).join(' -> ')   # => "pikuri-computer -> RESEARCHER"
  #   path.last.legs                   # the cell that decided the verdict
  #
  # Ties go to the node encountered first in pre-order — the parent over its
  # own children, since a parent's trifecta is the more actionable finding.
  #
  # @return [Array<Report>] never empty; +[self]+ when nothing below is worse
  def worst_path
    best = [self]
    children.each do |child|
      candidate = [self, *child.worst_path]
      best = candidate if SEVERITY.index(candidate.last.verdict) > SEVERITY.index(best.last.verdict)
    end
    best
  end

  # @return [Symbol] the most severe {#verdict} anywhere in this subtree
  def tree_verdict = worst_path.last.verdict

  # The human-readable map: one line per agent and per tool, indented by
  # depth, with each entry's legs as tags.
  #
  #   Trifecta: soft
  #
  #   pikuri-computer [:private, :untrusted_hard, :egress_reviewed]
  #     mail_compose [:private, :egress_reviewed]
  #     calculator []
  #     RESEARCHER [gate: human_reviewed] [:untrusted_hard, :egress]
  #       fetch [:untrusted_hard, :egress]
  #
  #   See book/trifecta-detector.md in this repo for ...
  #
  # An agent line carries its *folded* legs and a tool line its own, so
  # propagation shows up as layout: the parent above holds egress that no
  # tool of its own contributes. Plain text by design — a host wanting color
  # or width walks this tree itself rather than asking for styled output.
  #
  # @return [String] newline-separated, no trailing newline
  def render
    [advisory, '', *render_lines].join("\n")
  end

  # The one-line conclusion: what was found, where, and the leg to break.
  # Separate from {#render} because a status line wants this without the
  # tree, and because it is the half that carries *meaning* — the map
  # deliberately carries only facts.
  #
  #   [TRIFECTA: HARD] pikuri-code — private(read, bash) + untrusted(read,
  #   bash) + egress(bash). An injection in what this agent reads can drive
  #   an exfiltration. Break a leg: sever egress (a network-off sandbox), or
  #   privilege-separate the step that reads untrusted content.
  #   See book/trifecta-detector.md in this repo.
  #
  # A clean verdict never says "no trifecta detected". This reasons about
  # capability presence, so a quiet result is not a clean bill of health —
  # it under-warns on a single tool that is all three internally, and on
  # private data staged into a file by one tool and read by another.
  # Reporting the *finding* names what would tip the wiring over and claims
  # nothing it cannot support.
  #
  # @return [String]
  def advisory
    path = worst_path
    where = path.map(&:label).join(' -> ')
    legs = path.last.legs
    return "Trifecta: #{where} — #{presence(legs)}." if tree_verdict == :silent

    "[TRIFECTA: #{tree_verdict == :loud ? 'HARD' : 'soft'}] #{where} — " \
      "#{attribution(path.last)}. An injection in what this agent reads can drive " \
      "an exfiltration. Break a leg: #{advice(legs)}. #{FOOTER}"
  end

  protected

  # @param depth [Integer] indentation level
  # @return [Array<String>]
  def render_lines(depth = 0)
    indent = '  ' * depth
    gate = channel_egress ? " [gate: #{channel_egress}]" : ''
    lines = ["#{indent}#{label}#{gate} #{legs.to_tags.inspect}"]
    tool_legs.each { |name, tool| lines << "#{indent}  #{name} #{tool.to_tags.inspect}" }
    children.each { |child| lines.concat(child.render_lines(depth + 1)) }
    lines
  end

  private

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String] e.g. +"2 of 3 legs (private, untrusted); no egress leg found"+
  def presence(legs)
    held = { 'private' => legs.private, 'untrusted' => legs.untrusted != :none,
             'egress' => legs.egress? }
    "#{held.count { |_, v| v }} of 3 legs (#{held.select { |_, v| v }.keys.join(', ')}); " \
      "no #{held.reject { |_, v| v }.keys.join('/')} leg found"
  end

  # Which tool contributed each leg, so the line names something the reader
  # can actually go and change.
  #
  # @param node [Report]
  # @return [String]
  def attribution(node)
    i[private untrusted egress].filter_map do |axis|
      contributors = node.tool_legs.select { |_, l| leg_present?(l, axis) }.keys
      "#{axis}(#{contributors.join(', ')})" unless contributors.empty?
    end.join(' + ')
  end

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @param axis [Symbol] +:private+, +:untrusted+ or +:egress+
  # @return [Boolean]
  def leg_present?(legs, axis)
    case axis
    in :private then legs.private
    in :untrusted then legs.untrusted != :none
    in :egress then legs.egress?
    end
  end

  # The fix, per cell of the verdict grid — the soft cells want different
  # answers, which is why this reads the legs rather than the verdict symbol.
  #
  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String]
  def advice(legs)
    # A vouched destination is fragile in a way the other attenuations are
    # not: it survives only while it is the *sole* egress leg, since the
    # union hardens the whole node the moment one attacker-reachable tool
    # joins it. Say so, rather than let the soft verdict read as settled.
    if legs.egress_destination == :tool_vouched
      return 'sever egress outright — bytes still leave, they merely reach a destination the ' \
             'attacker cannot read back, and one attacker-reachable tool re-hardens this node'
    end

    case [legs.untrusted, legs.egress_payload_review]
    in [:hard, :unreviewed]
      'sever egress (a network-off sandbox), or privilege-separate the step that reads untrusted content'
    in [:hard, _]
      'privilege-separate the step that reads untrusted content, or declare the workspace trusted ' \
        'if you vouch for every byte it can reach'
    in [:weakened, :unreviewed]
      'sever egress — the untrusted leg is already attenuated by delegation'
    in [:weakened, _]
      'both legs are already attenuated; narrow what counts as private, or drop a leg outright'
    end
  end
end

#legsPikuri::Tool::TrifectaLegs (readonly)

Returns folded legs — own tools plus everything propagated up from #children. What #verdict reads.

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/pikuri/trifecta/report.rb', line 39

class Report < Data.define(:label, :legs, :tool_legs, :children, :channel_egress)
  # Verdicts, least to most severe. The only ordering outside
  # {Pikuri::Tool::TrifectaLegs}.
  #
  # @return [Array<Symbol>]
  SEVERITY = i[silent soft loud].freeze

  # Closing pointer of {#advisory}. Names the chapter by *slug*, not by
  # ordinal: book chapters are ordered only by `book/README.md`, so this
  # path survives a reordering that would silently falsify a number.
  #
  # @return [String]
  FOOTER = 'See book/trifecta-detector.md in this repo.'

  # How dangerous *this node* is — the tree's verdict is {#tree_verdict}.
  #
  # +:loud+ needs all three legs at full strength; +:soft+ is any complete
  # trifecta whose untrusted or egress leg is attenuated; +:silent+ is a
  # missing leg. Absence of a warning is not evidence of safety: this
  # reasons about capability presence, not data flow.
  #
  # @return [Symbol] +:silent+, +:soft+ or +:loud+
  # @raise [NoMatchingPatternError] if a lattice level exists that this grid
  #   does not place — deliberate, so a new level cannot be absorbed silently
  def verdict
    return :silent unless legs.private && legs.untrusted != :none && legs.egress?

    case [legs.untrusted, legs.egress_payload_review, legs.egress_destination]
    in [:hard,     :unreviewed,      :attacker_reachable] then :loud
    in [:hard,     :unreviewed,      :tool_vouched]       then :soft
    in [:hard,     :human_reviewed,  :attacker_reachable] then :soft
    in [:hard,     :human_reviewed,  :tool_vouched]       then :soft
    in [:weakened, :unreviewed,      :attacker_reachable] then :soft
    in [:weakened, :unreviewed,      :tool_vouched]       then :soft
    in [:weakened, :human_reviewed,  :attacker_reachable] then :soft
    in [:weakened, :human_reviewed,  :tool_vouched]       then :soft
    end
  end

  # Root-first path of Reports down to the most severe node in this subtree,
  # so the caller can both attribute the finding and read its cell:
  #
  #   path = report.worst_path
  #   path.map(&:label).join(' -> ')   # => "pikuri-computer -> RESEARCHER"
  #   path.last.legs                   # the cell that decided the verdict
  #
  # Ties go to the node encountered first in pre-order — the parent over its
  # own children, since a parent's trifecta is the more actionable finding.
  #
  # @return [Array<Report>] never empty; +[self]+ when nothing below is worse
  def worst_path
    best = [self]
    children.each do |child|
      candidate = [self, *child.worst_path]
      best = candidate if SEVERITY.index(candidate.last.verdict) > SEVERITY.index(best.last.verdict)
    end
    best
  end

  # @return [Symbol] the most severe {#verdict} anywhere in this subtree
  def tree_verdict = worst_path.last.verdict

  # The human-readable map: one line per agent and per tool, indented by
  # depth, with each entry's legs as tags.
  #
  #   Trifecta: soft
  #
  #   pikuri-computer [:private, :untrusted_hard, :egress_reviewed]
  #     mail_compose [:private, :egress_reviewed]
  #     calculator []
  #     RESEARCHER [gate: human_reviewed] [:untrusted_hard, :egress]
  #       fetch [:untrusted_hard, :egress]
  #
  #   See book/trifecta-detector.md in this repo for ...
  #
  # An agent line carries its *folded* legs and a tool line its own, so
  # propagation shows up as layout: the parent above holds egress that no
  # tool of its own contributes. Plain text by design — a host wanting color
  # or width walks this tree itself rather than asking for styled output.
  #
  # @return [String] newline-separated, no trailing newline
  def render
    [advisory, '', *render_lines].join("\n")
  end

  # The one-line conclusion: what was found, where, and the leg to break.
  # Separate from {#render} because a status line wants this without the
  # tree, and because it is the half that carries *meaning* — the map
  # deliberately carries only facts.
  #
  #   [TRIFECTA: HARD] pikuri-code — private(read, bash) + untrusted(read,
  #   bash) + egress(bash). An injection in what this agent reads can drive
  #   an exfiltration. Break a leg: sever egress (a network-off sandbox), or
  #   privilege-separate the step that reads untrusted content.
  #   See book/trifecta-detector.md in this repo.
  #
  # A clean verdict never says "no trifecta detected". This reasons about
  # capability presence, so a quiet result is not a clean bill of health —
  # it under-warns on a single tool that is all three internally, and on
  # private data staged into a file by one tool and read by another.
  # Reporting the *finding* names what would tip the wiring over and claims
  # nothing it cannot support.
  #
  # @return [String]
  def advisory
    path = worst_path
    where = path.map(&:label).join(' -> ')
    legs = path.last.legs
    return "Trifecta: #{where} — #{presence(legs)}." if tree_verdict == :silent

    "[TRIFECTA: #{tree_verdict == :loud ? 'HARD' : 'soft'}] #{where} — " \
      "#{attribution(path.last)}. An injection in what this agent reads can drive " \
      "an exfiltration. Break a leg: #{advice(legs)}. #{FOOTER}"
  end

  protected

  # @param depth [Integer] indentation level
  # @return [Array<String>]
  def render_lines(depth = 0)
    indent = '  ' * depth
    gate = channel_egress ? " [gate: #{channel_egress}]" : ''
    lines = ["#{indent}#{label}#{gate} #{legs.to_tags.inspect}"]
    tool_legs.each { |name, tool| lines << "#{indent}  #{name} #{tool.to_tags.inspect}" }
    children.each { |child| lines.concat(child.render_lines(depth + 1)) }
    lines
  end

  private

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String] e.g. +"2 of 3 legs (private, untrusted); no egress leg found"+
  def presence(legs)
    held = { 'private' => legs.private, 'untrusted' => legs.untrusted != :none,
             'egress' => legs.egress? }
    "#{held.count { |_, v| v }} of 3 legs (#{held.select { |_, v| v }.keys.join(', ')}); " \
      "no #{held.reject { |_, v| v }.keys.join('/')} leg found"
  end

  # Which tool contributed each leg, so the line names something the reader
  # can actually go and change.
  #
  # @param node [Report]
  # @return [String]
  def attribution(node)
    i[private untrusted egress].filter_map do |axis|
      contributors = node.tool_legs.select { |_, l| leg_present?(l, axis) }.keys
      "#{axis}(#{contributors.join(', ')})" unless contributors.empty?
    end.join(' + ')
  end

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @param axis [Symbol] +:private+, +:untrusted+ or +:egress+
  # @return [Boolean]
  def leg_present?(legs, axis)
    case axis
    in :private then legs.private
    in :untrusted then legs.untrusted != :none
    in :egress then legs.egress?
    end
  end

  # The fix, per cell of the verdict grid — the soft cells want different
  # answers, which is why this reads the legs rather than the verdict symbol.
  #
  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String]
  def advice(legs)
    # A vouched destination is fragile in a way the other attenuations are
    # not: it survives only while it is the *sole* egress leg, since the
    # union hardens the whole node the moment one attacker-reachable tool
    # joins it. Say so, rather than let the soft verdict read as settled.
    if legs.egress_destination == :tool_vouched
      return 'sever egress outright — bytes still leave, they merely reach a destination the ' \
             'attacker cannot read back, and one attacker-reachable tool re-hardens this node'
    end

    case [legs.untrusted, legs.egress_payload_review]
    in [:hard, :unreviewed]
      'sever egress (a network-off sandbox), or privilege-separate the step that reads untrusted content'
    in [:hard, _]
      'privilege-separate the step that reads untrusted content, or declare the workspace trusted ' \
        'if you vouch for every byte it can reach'
    in [:weakened, :unreviewed]
      'sever egress — the untrusted leg is already attenuated by delegation'
    in [:weakened, _]
      'both legs are already attenuated; narrow what counts as private, or drop a leg outright'
    end
  end
end

#tool_legsHash{String=>Pikuri::Tool::TrifectaLegs} (readonly)

Returns carried through from the Node so #render can print the tools as leaves.

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/pikuri/trifecta/report.rb', line 39

class Report < Data.define(:label, :legs, :tool_legs, :children, :channel_egress)
  # Verdicts, least to most severe. The only ordering outside
  # {Pikuri::Tool::TrifectaLegs}.
  #
  # @return [Array<Symbol>]
  SEVERITY = i[silent soft loud].freeze

  # Closing pointer of {#advisory}. Names the chapter by *slug*, not by
  # ordinal: book chapters are ordered only by `book/README.md`, so this
  # path survives a reordering that would silently falsify a number.
  #
  # @return [String]
  FOOTER = 'See book/trifecta-detector.md in this repo.'

  # How dangerous *this node* is — the tree's verdict is {#tree_verdict}.
  #
  # +:loud+ needs all three legs at full strength; +:soft+ is any complete
  # trifecta whose untrusted or egress leg is attenuated; +:silent+ is a
  # missing leg. Absence of a warning is not evidence of safety: this
  # reasons about capability presence, not data flow.
  #
  # @return [Symbol] +:silent+, +:soft+ or +:loud+
  # @raise [NoMatchingPatternError] if a lattice level exists that this grid
  #   does not place — deliberate, so a new level cannot be absorbed silently
  def verdict
    return :silent unless legs.private && legs.untrusted != :none && legs.egress?

    case [legs.untrusted, legs.egress_payload_review, legs.egress_destination]
    in [:hard,     :unreviewed,      :attacker_reachable] then :loud
    in [:hard,     :unreviewed,      :tool_vouched]       then :soft
    in [:hard,     :human_reviewed,  :attacker_reachable] then :soft
    in [:hard,     :human_reviewed,  :tool_vouched]       then :soft
    in [:weakened, :unreviewed,      :attacker_reachable] then :soft
    in [:weakened, :unreviewed,      :tool_vouched]       then :soft
    in [:weakened, :human_reviewed,  :attacker_reachable] then :soft
    in [:weakened, :human_reviewed,  :tool_vouched]       then :soft
    end
  end

  # Root-first path of Reports down to the most severe node in this subtree,
  # so the caller can both attribute the finding and read its cell:
  #
  #   path = report.worst_path
  #   path.map(&:label).join(' -> ')   # => "pikuri-computer -> RESEARCHER"
  #   path.last.legs                   # the cell that decided the verdict
  #
  # Ties go to the node encountered first in pre-order — the parent over its
  # own children, since a parent's trifecta is the more actionable finding.
  #
  # @return [Array<Report>] never empty; +[self]+ when nothing below is worse
  def worst_path
    best = [self]
    children.each do |child|
      candidate = [self, *child.worst_path]
      best = candidate if SEVERITY.index(candidate.last.verdict) > SEVERITY.index(best.last.verdict)
    end
    best
  end

  # @return [Symbol] the most severe {#verdict} anywhere in this subtree
  def tree_verdict = worst_path.last.verdict

  # The human-readable map: one line per agent and per tool, indented by
  # depth, with each entry's legs as tags.
  #
  #   Trifecta: soft
  #
  #   pikuri-computer [:private, :untrusted_hard, :egress_reviewed]
  #     mail_compose [:private, :egress_reviewed]
  #     calculator []
  #     RESEARCHER [gate: human_reviewed] [:untrusted_hard, :egress]
  #       fetch [:untrusted_hard, :egress]
  #
  #   See book/trifecta-detector.md in this repo for ...
  #
  # An agent line carries its *folded* legs and a tool line its own, so
  # propagation shows up as layout: the parent above holds egress that no
  # tool of its own contributes. Plain text by design — a host wanting color
  # or width walks this tree itself rather than asking for styled output.
  #
  # @return [String] newline-separated, no trailing newline
  def render
    [advisory, '', *render_lines].join("\n")
  end

  # The one-line conclusion: what was found, where, and the leg to break.
  # Separate from {#render} because a status line wants this without the
  # tree, and because it is the half that carries *meaning* — the map
  # deliberately carries only facts.
  #
  #   [TRIFECTA: HARD] pikuri-code — private(read, bash) + untrusted(read,
  #   bash) + egress(bash). An injection in what this agent reads can drive
  #   an exfiltration. Break a leg: sever egress (a network-off sandbox), or
  #   privilege-separate the step that reads untrusted content.
  #   See book/trifecta-detector.md in this repo.
  #
  # A clean verdict never says "no trifecta detected". This reasons about
  # capability presence, so a quiet result is not a clean bill of health —
  # it under-warns on a single tool that is all three internally, and on
  # private data staged into a file by one tool and read by another.
  # Reporting the *finding* names what would tip the wiring over and claims
  # nothing it cannot support.
  #
  # @return [String]
  def advisory
    path = worst_path
    where = path.map(&:label).join(' -> ')
    legs = path.last.legs
    return "Trifecta: #{where} — #{presence(legs)}." if tree_verdict == :silent

    "[TRIFECTA: #{tree_verdict == :loud ? 'HARD' : 'soft'}] #{where} — " \
      "#{attribution(path.last)}. An injection in what this agent reads can drive " \
      "an exfiltration. Break a leg: #{advice(legs)}. #{FOOTER}"
  end

  protected

  # @param depth [Integer] indentation level
  # @return [Array<String>]
  def render_lines(depth = 0)
    indent = '  ' * depth
    gate = channel_egress ? " [gate: #{channel_egress}]" : ''
    lines = ["#{indent}#{label}#{gate} #{legs.to_tags.inspect}"]
    tool_legs.each { |name, tool| lines << "#{indent}  #{name} #{tool.to_tags.inspect}" }
    children.each { |child| lines.concat(child.render_lines(depth + 1)) }
    lines
  end

  private

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String] e.g. +"2 of 3 legs (private, untrusted); no egress leg found"+
  def presence(legs)
    held = { 'private' => legs.private, 'untrusted' => legs.untrusted != :none,
             'egress' => legs.egress? }
    "#{held.count { |_, v| v }} of 3 legs (#{held.select { |_, v| v }.keys.join(', ')}); " \
      "no #{held.reject { |_, v| v }.keys.join('/')} leg found"
  end

  # Which tool contributed each leg, so the line names something the reader
  # can actually go and change.
  #
  # @param node [Report]
  # @return [String]
  def attribution(node)
    i[private untrusted egress].filter_map do |axis|
      contributors = node.tool_legs.select { |_, l| leg_present?(l, axis) }.keys
      "#{axis}(#{contributors.join(', ')})" unless contributors.empty?
    end.join(' + ')
  end

  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @param axis [Symbol] +:private+, +:untrusted+ or +:egress+
  # @return [Boolean]
  def leg_present?(legs, axis)
    case axis
    in :private then legs.private
    in :untrusted then legs.untrusted != :none
    in :egress then legs.egress?
    end
  end

  # The fix, per cell of the verdict grid — the soft cells want different
  # answers, which is why this reads the legs rather than the verdict symbol.
  #
  # @param legs [Pikuri::Tool::TrifectaLegs]
  # @return [String]
  def advice(legs)
    # A vouched destination is fragile in a way the other attenuations are
    # not: it survives only while it is the *sole* egress leg, since the
    # union hardens the whole node the moment one attacker-reachable tool
    # joins it. Say so, rather than let the soft verdict read as settled.
    if legs.egress_destination == :tool_vouched
      return 'sever egress outright — bytes still leave, they merely reach a destination the ' \
             'attacker cannot read back, and one attacker-reachable tool re-hardens this node'
    end

    case [legs.untrusted, legs.egress_payload_review]
    in [:hard, :unreviewed]
      'sever egress (a network-off sandbox), or privilege-separate the step that reads untrusted content'
    in [:hard, _]
      'privilege-separate the step that reads untrusted content, or declare the workspace trusted ' \
        'if you vouch for every byte it can reach'
    in [:weakened, :unreviewed]
      'sever egress — the untrusted leg is already attenuated by delegation'
    in [:weakened, _]
      'both legs are already attenuated; narrow what counts as private, or drop a leg outright'
    end
  end
end

Instance Method Details

#advisoryString

The one-line conclusion: what was found, where, and the leg to break. Separate from #render because a status line wants this without the tree, and because it is the half that carries meaning — the map deliberately carries only facts.

[TRIFECTA: HARD] pikuri-code 

A clean verdict never says "no trifecta detected". This reasons about capability presence, so a quiet result is not a clean bill of health — it under-warns on a single tool that is all three internally, and on private data staged into a file by one tool and read by another. Reporting the finding names what would tip the wiring over and claims nothing it cannot support.

Returns:

  • (String)


143
144
145
146
147
148
149
150
151
152
# File 'lib/pikuri/trifecta/report.rb', line 143

def advisory
  path = worst_path
  where = path.map(&:label).join(' -> ')
  legs = path.last.legs
  return "Trifecta: #{where} — #{presence(legs)}." if tree_verdict == :silent

  "[TRIFECTA: #{tree_verdict == :loud ? 'HARD' : 'soft'}] #{where} — " \
    "#{attribution(path.last)}. An injection in what this agent reads can drive " \
    "an exfiltration. Break a leg: #{advice(legs)}. #{FOOTER}"
end

#renderString

The human-readable map: one line per agent and per tool, indented by depth, with each entry's legs as tags.

Trifecta: soft

pikuri-computer [:private, :untrusted_hard, :egress_reviewed]
mail_compose [:private, :egress_reviewed]
calculator []
RESEARCHER [gate: human_reviewed] [:untrusted_hard, :egress]
  fetch [:untrusted_hard, :egress]

See book/trifecta-detector.md in this repo for ...

An agent line carries its folded legs and a tool line its own, so propagation shows up as layout: the parent above holds egress that no tool of its own contributes. Plain text by design — a host wanting color or width walks this tree itself rather than asking for styled output.

Returns:

  • (String)

    newline-separated, no trailing newline



120
121
122
# File 'lib/pikuri/trifecta/report.rb', line 120

def render
  [advisory, '', *render_lines].join("\n")
end

#tree_verdictSymbol

Returns the most severe #verdict anywhere in this subtree.

Returns:

  • (Symbol)

    the most severe #verdict anywhere in this subtree



99
# File 'lib/pikuri/trifecta/report.rb', line 99

def tree_verdict = worst_path.last.verdict

#verdictSymbol

How dangerous this node is — the tree's verdict is #tree_verdict.

:loud needs all three legs at full strength; :soft is any complete trifecta whose untrusted or egress leg is attenuated; :silent is a missing leg. Absence of a warning is not evidence of safety: this reasons about capability presence, not data flow.

Returns:

  • (Symbol)

    :silent, :soft or :loud

Raises:

  • (NoMatchingPatternError)

    if a lattice level exists that this grid does not place — deliberate, so a new level cannot be absorbed silently



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/pikuri/trifecta/report.rb', line 63

def verdict
  return :silent unless legs.private && legs.untrusted != :none && legs.egress?

  case [legs.untrusted, legs.egress_payload_review, legs.egress_destination]
  in [:hard,     :unreviewed,      :attacker_reachable] then :loud
  in [:hard,     :unreviewed,      :tool_vouched]       then :soft
  in [:hard,     :human_reviewed,  :attacker_reachable] then :soft
  in [:hard,     :human_reviewed,  :tool_vouched]       then :soft
  in [:weakened, :unreviewed,      :attacker_reachable] then :soft
  in [:weakened, :unreviewed,      :tool_vouched]       then :soft
  in [:weakened, :human_reviewed,  :attacker_reachable] then :soft
  in [:weakened, :human_reviewed,  :tool_vouched]       then :soft
  end
end

#worst_pathArray<Report>

Root-first path of Reports down to the most severe node in this subtree, so the caller can both attribute the finding and read its cell:

path = report.worst_path
path.map(&:label).join(' -> ')   # => "pikuri-computer -> RESEARCHER"
path.last.legs                   # the cell that decided the verdict

Ties go to the node encountered first in pre-order — the parent over its own children, since a parent's trifecta is the more actionable finding.

Returns:

  • (Array<Report>)

    never empty; [self] when nothing below is worse



89
90
91
92
93
94
95
96
# File 'lib/pikuri/trifecta/report.rb', line 89

def worst_path
  best = [self]
  children.each do |child|
    candidate = [self, *child.worst_path]
    best = candidate if SEVERITY.index(candidate.last.verdict) > SEVERITY.index(best.last.verdict)
  end
  best
end