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_test.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

257 lines
5.8 KiB
Go

// Copyright 2025 The Forgejo Authors.
// SPDX-License-Identifier: MIT
package report
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestReporterMask(t *testing.T) {
lineOne := "secretOne"
lineTwo := lineOne + "secretTwoIsLongerThanAndStartsWithIt"
multiLineMixedSeparators := "A\nB\r\nC\r\nD"
multiLineOne := lineOne + `
TWO
THREE`
multiLineTwo := multiLineOne + `
FOUR
FIVE
SIX`
for _, testCase := range []struct {
name string
secrets []string
in string
out string
noMore bool
needMore bool
}{
{
//
// a multiline secret is masked
//
name: "MultilineIsMasked",
secrets: []string{
multiLineOne,
},
in: fmt.Sprintf("line before\n%[1]s\nline after", multiLineOne),
out: "line before\n***\n***\n***\nline after\n",
needMore: false,
},
{
//
// in a multiline secret \r\n is equivalent to \n and does
// not change how it is masked
//
name: "MultilineWithMixedLineSeparatorsIsMasked",
secrets: []string{
multiLineMixedSeparators,
},
in: fmt.Sprintf("line before\n%[1]s\nline after", multiLineMixedSeparators),
out: "line before\n***\n***\n***\n***\nline after\n",
needMore: false,
},
{
//
// the last line of a multline secret is not a match
//
//
name: "MultilineLastLineDoesNotMatch",
secrets: []string{
multiLineOne,
},
in: fmt.Sprintf("%s\nTWO\nsomethingelse", lineOne),
out: lineOne + "\nTWO\nsomethingelse\n",
needMore: false,
},
{
//
// non-multine secrets are masked
//
name: "SecretsAreMasked",
secrets: []string{
"",
lineOne,
lineTwo,
},
in: fmt.Sprintf("line before\n%[1]s\n%[2]s\nline after", lineOne, lineTwo),
out: "line before\n***\n***\nline after\n",
needMore: false,
},
{
//
// the first line of a multiline secret may be found
// at the end of a line and the last line may be followed
// by a suffix, e.g.
//
// >>>ONE
// TWO
// THREE<<<
//
// and is expected to be replaced with
//
// >>>***
// ***
// ***<<<
//
name: "MultilineWithSuffixAndPrefix",
secrets: []string{
multiLineOne,
},
in: fmt.Sprintf(">>>%[1]s<<<", multiLineOne),
out: ">>>***\n***\n***<<<\n",
needMore: false,
},
{
//
// multiline secrets are considered first
// since only the first line of the multiLineOne secret
// is found, it needs more input to decide and does not
// mask anything.
//
// non-multiline secrets are not considered at all if
// a multiline secret needs more input and the
// lineOne secret is not masked even though it is found
//
// the first lines is found but not the second
//
name: "NeedMoreLines",
secrets: []string{
lineOne,
multiLineOne,
},
in: lineOne,
out: lineOne + "\n",
needMore: true,
},
{
//
// the lines up to but not including the last are found
//
// See NeedMoreLines
//
name: "NeedMoreLinesVariation1",
secrets: []string{
multiLineOne,
},
in: fmt.Sprintf("%s\nTWO", lineOne),
out: lineOne + "\nTWO\n",
needMore: true,
},
{
//
// the lines up to the third out of six are found
//
// See NeedMoreLines
//
name: "NeedMoreLinesVariation2",
secrets: []string{
multiLineTwo,
},
in: multiLineOne,
out: multiLineOne + "\n",
needMore: true,
},
{
//
// a multiline secret will be masked if it is found
// even when another multiline secret needs more input
//
// however non-multiline secrets will not be masked
//
name: "NeedMoreLinesAndMultilinePartialMasking",
secrets: []string{
lineOne,
multiLineOne,
},
in: fmt.Sprintf(`%[1]s %[2]s
>>>%[3]s<<<
%[1]s`, lineOne, lineTwo, multiLineOne),
out: "secretOne secretOnesecretTwoIsLongerThanAndStartsWithIt\n>>>***\n***\n***<<<\nsecretOne\n",
needMore: true,
},
{
//
// - oneline overlaps with lineTwo
// - oneLine overlaps with multiLineOne and multiLineTwo
// - multiLineOne overlaps with multiLineTwo
//
// they are all masked because the longest secret is masked
// first
//
name: "OverlappingSecrets",
secrets: []string{
lineOne,
lineTwo,
multiLineOne,
multiLineTwo,
},
in: fmt.Sprintf(`[[[%[1]s]]] {{{%[2]s}}}
>>>%[3]s<<<
(((%[4]s)))`, lineOne, lineTwo, multiLineOne, multiLineTwo),
out: `[[[***]]] {{{***}}}
>>>***
***
***<<<
(((***
***
***
***
***
***)))
`,
needMore: false,
},
{
//
// A multiline secret needing more lines does not
// prevent single line secrets from being masked
// when there is no more lines / these are the last
// available lines and there is no sense in hoping
// more will be available later.
//
name: "NeedMoreButNoMore",
secrets: []string{
lineTwo,
multiLineOne,
},
in: fmt.Sprintf(`[[[%[1]s]]] {{{%[2]s\nTWO}}}`, lineTwo, lineOne),
out: "[[[***]]] {{{secretOne\\nTWO}}}\n",
noMore: true,
needMore: false,
},
{
//
// A variation where the partial multiline secret also
// happens to contain a single line secret (TWO) which
// needs to be masked.
//
// See NeedMoreButNoMore
//
name: "NeedMoreButNoMoreAndOverlappingSecret",
secrets: []string{
"TWO",
lineTwo,
multiLineOne,
},
in: fmt.Sprintf(`[[[%[1]s]]] {{{%[2]s\nTWO}}}`, lineTwo, lineOne),
out: "[[[***]]] {{{secretOne\\n***}}}\n",
noMore: true,
needMore: false,
},
} {
t.Run(testCase.name, func(t *testing.T) {
m := newMasker()
for _, secret := range testCase.secrets {
m.add(secret)
}
rows := stringToRows(testCase.in)
needMore := m.replace(rows, testCase.noMore)
assert.Equal(t, testCase.needMore, needMore)
assert.Equal(t, testCase.out, rowsToString(rows))
})
}
}