1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-07-27 17:28:35 +00:00
forgejo-runner/internal/pkg/report/mask.go
Earl Warren 190079b7f3
feat: reporter helper to mask secrets, including multiline
- the longest secret is masked first
- multiline secrets are masked before single line secrets
- O(multiline * log rows) to not degrade performances when there are
  no multiline secrets
2025-07-07 17:28:10 +02:00

146 lines
3.4 KiB
Go

// Copyright 2025 The Forgejo Authors.
// SPDX-License-Identifier: MIT
package report
import (
"cmp"
"slices"
"strings"
runnerv1 "code.forgejo.org/forgejo/actions-proto/runner/v1"
)
type masker struct {
replacer *strings.Replacer
lines []string
multiLines [][]string
}
func newMasker() *masker {
return &masker{
lines: make([]string, 0, 10),
multiLines: make([][]string, 0, 10),
}
}
func (o *masker) add(secret string) {
if len(secret) == 0 {
return
}
o.replacer = nil
lines := strings.Split(strings.ReplaceAll(secret, "\r\n", "\n"), "\n")
if len(lines) > 1 {
o.multiLines = append(o.multiLines, lines)
// make sure the longest secret are replaced first
slices.SortFunc(o.multiLines, func(a, b []string) int {
return cmp.Compare(len(b), len(a))
})
} else {
o.lines = append(o.lines, lines[0])
// make sure the longest secret are replaced first
slices.SortFunc(o.lines, func(a, b string) int {
return cmp.Compare(len(b), len(a))
})
}
}
func (o *masker) getReplacer() *strings.Replacer {
if o.replacer == nil {
oldnew := make([]string, 0, len(o.lines)*2)
for _, line := range o.lines {
oldnew = append(oldnew, line, "***")
}
o.replacer = strings.NewReplacer(oldnew...)
}
return o.replacer
}
func (o *masker) replaceLines(rows []*runnerv1.LogRow) {
r := o.getReplacer()
for _, row := range rows {
row.Content = r.Replace(row.Content)
}
}
func (o *masker) maybeReplaceMultiline(multiLine []string, rows []*runnerv1.LogRow) bool {
equal, needMore := o.equalMultiline(multiLine, rows)
if needMore {
return needMore
}
if equal {
o.replaceMultiline(multiLine, rows)
}
return false
}
func (o *masker) trimEOL(s string) string {
return strings.TrimRightFunc(s, func(r rune) bool { return r == '\r' || r == '\n' })
}
func (o *masker) equalMultiline(multiLine []string, rows []*runnerv1.LogRow) (equal, needMore bool) {
if len(rows) < 2 {
needMore = true
return
}
lastIndex := len(multiLine) - 1
first := multiLine[0]
if !strings.HasSuffix(o.trimEOL(rows[0].Content), first) {
return // unreachable because the caller checks that already
}
for i, line := range multiLine[1:lastIndex] {
rowIndex := i + 1
if rowIndex >= len(rows) {
needMore = true
return
}
if o.trimEOL(rows[rowIndex].Content) != line {
return
}
}
last := multiLine[lastIndex]
if lastIndex >= len(rows) {
needMore = true
return
}
if !strings.HasPrefix(o.trimEOL(rows[lastIndex].Content), last) {
return
}
equal = true
return
}
func (o *masker) replaceMultiline(multiLine []string, rows []*runnerv1.LogRow) {
lastIndex := len(multiLine) - 1
first := multiLine[0]
rows[0].Content = strings.TrimSuffix(rows[0].Content, first) + "***"
for _, row := range rows[1:lastIndex] {
row.Content = "***"
}
last := multiLine[lastIndex]
rows[lastIndex].Content = "***" + strings.TrimPrefix(rows[lastIndex].Content, last)
}
func (o *masker) replaceMultilines(rows []*runnerv1.LogRow) bool {
for _, multiLine := range o.multiLines {
for i, row := range rows {
if strings.HasSuffix(o.trimEOL(row.Content), multiLine[0]) {
needMore := o.maybeReplaceMultiline(multiLine, rows[i:])
if needMore {
return needMore
}
}
}
}
return false
}
func (o *masker) replace(rows []*runnerv1.LogRow, noMore bool) bool {
needMore := o.replaceMultilines(rows)
if !noMore && needMore {
return needMore
}
o.replaceLines(rows)
return false
}