Skip to content

Commit 39588fb

Browse files
committed
feat(outputs,console): Implement console output colorization
The console output allows to configure output colorization, which is much more intuitive for analysts as it helps to explore the events and callstacks.
1 parent 0328ab7 commit 39588fb

10 files changed

Lines changed: 793 additions & 7 deletions

File tree

configs/fibratus.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ output:
313313
# Indicates whether the console output is active
314314
enabled: true
315315

316+
# Indicates if the console output is colorized
317+
colorize: true
318+
316319
# Specifies the console output format. The "pretty" format dictates that formatting is accomplished
317320
# by replacing the specifiers in the template. The "json" format outputs the event as a raw JSON string
318321
format: pretty

pkg/callstack/callstack.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021-2022 by Nedim Sabic Sabic
2+
* Copyright 2021-present by Nedim Sabic Sabic
33
* https://www.fibratus.io
44
* All Rights Reserved.
55
*
@@ -30,6 +30,15 @@ import (
3030
"golang.org/x/sys/windows"
3131
)
3232

33+
// FrameProvenance designates the frame provenance
34+
type FrameProvenance uint8
35+
36+
const (
37+
Kernel FrameProvenance = iota
38+
System
39+
User
40+
)
41+
3342
// unbacked represents the identifier for unbacked regions in stack frames
3443
const unbacked = "unbacked"
3544

@@ -48,6 +57,20 @@ type Frame struct {
4857
ModuleAddress va.Address // module base address
4958
}
5059

60+
// Provenance resolves the frame provenance.
61+
func (f Frame) Provenance() FrameProvenance {
62+
if f.Addr.InSystemRange() {
63+
return Kernel
64+
}
65+
66+
mod := filepath.Base(strings.ToLower(f.Module))
67+
if mod == "ntdll.dll" || mod == "kernel32.dll" || mod == "kernelbase.dll" {
68+
return System
69+
}
70+
71+
return User
72+
}
73+
5174
// IsUnbacked returns true if this frame is originated
5275
// from unbacked memory section
5376
func (f Frame) IsUnbacked() bool { return f.Module == unbacked }

pkg/callstack/colorize.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright 2021-present by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package callstack
20+
21+
import (
22+
"fmt"
23+
"path/filepath"
24+
"strings"
25+
26+
"github.com/rabbitstack/fibratus/pkg/util/colorizer"
27+
)
28+
29+
// Colorize renders a callstack as a multi-line,
30+
// ANSI-colourised string. It works directly with the typed Frame slice so
31+
// no string parsing is required.
32+
//
33+
// Visual hierarchy per frame (left > right, dim > bright):
34+
//
35+
// <frame#> <dim addr> <muted dir\><module>!<bold symbol> <dim +offset>
36+
//
37+
// Frame tiers
38+
// ───────────
39+
//
40+
// kernel – frame.Addr.InSystemRange() > magenta
41+
// unbacked – frame.IsUnbacked() > red (highest suspicion)
42+
// system – system module > teal
43+
// user – everything else > amber
44+
//
45+
// Consecutive unresolved frames (kernel-space with no symbol) are collapsed
46+
// into a single dim counter line to avoid flooding the view.
47+
func (s Callstack) Colorize() string {
48+
if s.IsEmpty() {
49+
return ""
50+
}
51+
52+
// iterate in reverse so the outermost frame comes first
53+
depth := s.Depth()
54+
l := s.maxAddrLength()
55+
56+
var idx int
57+
var unresolved int
58+
var b strings.Builder
59+
b.Grow(depth * 100)
60+
61+
flushUnresolved := func() {
62+
if unresolved == 0 {
63+
return
64+
}
65+
line := fmt.Sprintf(" %s %d unresolved %s",
66+
colorizer.SpanDim("▸"),
67+
unresolved,
68+
"frame(s)",
69+
)
70+
b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, line)))
71+
b.WriteByte('\n')
72+
unresolved = 0
73+
}
74+
75+
for i := depth - 1; i >= 0; i-- {
76+
f := s.FrameAt(i)
77+
78+
// frames in kernel range with no resolved symbol are unresolved so
79+
// we can collapse them into a counter
80+
if f.Addr.InSystemRange() && (f.Symbol == "" || f.Symbol == "?") {
81+
unresolved++
82+
continue
83+
}
84+
85+
flushUnresolved()
86+
idx++
87+
88+
// draw gutter
89+
b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, fmt.Sprintf(" %3d ", idx))))
90+
91+
// draw address
92+
addrStr := "0x" + f.Addr.String()
93+
paddedAddr := addrStr + strings.Repeat(" ", l-len(addrStr))
94+
b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, paddedAddr)))
95+
b.WriteString(" ")
96+
97+
// unbacked means execution from anonymous memory which is the highest-
98+
// suspicion tier, rendered red regardless of address range.
99+
if f.IsUnbacked() {
100+
b.WriteString(f.colorizeUnbacked())
101+
b.WriteByte('\n')
102+
continue
103+
}
104+
105+
clr := f.Provenance().color()
106+
107+
dir := filepath.Dir(f.Module)
108+
mod := filepath.Base(f.Module)
109+
if dir == "." {
110+
dir = ""
111+
}
112+
// module directory
113+
if dir != "" {
114+
dir += `\`
115+
b.WriteString(colorizer.SpanDim(colorizer.Span(clr, dir)))
116+
}
117+
// module name
118+
b.WriteString(colorizer.Span(clr, mod))
119+
120+
// symbol
121+
b.WriteString(colorizer.SpanDim("!"))
122+
123+
sym := f.Symbol
124+
if sym == "" || sym == "?" {
125+
sym = "?"
126+
}
127+
b.WriteString(colorizer.SpanBold(clr, sym))
128+
129+
// offset
130+
if f.Offset != 0 {
131+
b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, fmt.Sprintf("+0x%x", f.Offset))))
132+
}
133+
134+
b.WriteByte('\n')
135+
}
136+
137+
flushUnresolved()
138+
139+
return strings.TrimRight(b.String(), "\n")
140+
}
141+
142+
// maxAddrLength measure the widest address string
143+
func (s Callstack) maxAddrLength() int {
144+
maxw := 0
145+
for _, f := range s {
146+
w := len("0x") + len(f.Addr.String())
147+
maxw = max(maxw, w)
148+
}
149+
return maxw
150+
}
151+
152+
// colorizeUnbackedFrame renders the unbacked frame.
153+
func (f Frame) colorizeUnbacked() string {
154+
var b strings.Builder
155+
b.WriteString(colorizer.SpanBold(colorizer.Red, "unbacked"))
156+
b.WriteString(colorizer.SpanDim("!"))
157+
b.WriteString(colorizer.SpanBold(colorizer.Red, "?"))
158+
if f.Offset != 0 {
159+
b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, fmt.Sprintf("+0x%x", f.Offset))))
160+
}
161+
return b.String()
162+
}
163+
164+
// color return frame provenance color to fill the the module
165+
// directory, base module name, and the symbol respectively.
166+
func (p FrameProvenance) color() uint8 {
167+
switch p {
168+
case Kernel:
169+
return colorizer.Magenta
170+
case System:
171+
return colorizer.Teal
172+
default:
173+
return colorizer.Amber
174+
}
175+
}

pkg/config/config.schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,9 @@
498498
"enabled": {
499499
"type": "boolean"
500500
},
501+
"colorize": {
502+
"type": "boolean"
503+
},
501504
"format": {
502505
"type": "string",
503506
"enum": [

0 commit comments

Comments
 (0)