mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-07-27 17:28:35 +00:00
- 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
257 lines
5.8 KiB
Go
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))
|
|
})
|
|
}
|
|
}
|