diff --git a/act/runner/command.go b/act/runner/command.go index 477738c0..5ac20e95 100644 --- a/act/runner/command.go +++ b/act/runner/command.go @@ -51,7 +51,7 @@ func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler { logger.Infof("%s", line) return false } - arg = unescapeCommandData(arg) + arg = UnescapeCommandData(arg) kvPairs = unescapeKvPairs(kvPairs) switch command { case "set-env": @@ -151,7 +151,7 @@ func parseKeyValuePairs(kvPairs, separator string) map[string]string { return rtn } -func unescapeCommandData(arg string) string { +func UnescapeCommandData(arg string) string { escapeMap := map[string]string{ "%25": "%", "%0D": "\r", diff --git a/act/runner/logger.go b/act/runner/logger.go index fbf93732..d364420d 100644 --- a/act/runner/logger.go +++ b/act/runner/logger.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "os" + "slices" "strings" "sync" @@ -166,9 +167,29 @@ func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stage type entryProcessor func(entry *logrus.Entry) *logrus.Entry +func AppendSecretMasker(oldnew []string, v string) []string { + ret := oldnew + + for l := range strings.SplitSeq(v, "\n") { + tm := strings.TrimSpace(l) + // formatted JSON secrets could otherwise mask {,[,],} everywhere + if len(tm) > 1 { + ret = append(ret, tm, "***") + } + } + + return ret +} + // valueMasker applies secrets and ::add-mask:: patterns to every log entry, including // raw_output (command/stream) lines; there is no bypass by field. func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor { + var oldnew []string + for _, v := range secrets { + oldnew = AppendSecretMasker(oldnew, v) + } + oldnew = slices.Clip(oldnew) + defReplacer := strings.NewReplacer(oldnew...) return func(entry *logrus.Entry) *logrus.Entry { if insecureSecrets { return entry @@ -176,16 +197,16 @@ func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor masks := Masks(entry.Context) - for _, v := range secrets { - if v != "" { - entry.Message = strings.ReplaceAll(entry.Message, v, "***") - } - } + if len(*masks) == 0 { + entry.Message = defReplacer.Replace(entry.Message) + } else { + cmasker := oldnew - for _, v := range *masks { - if v != "" { - entry.Message = strings.ReplaceAll(entry.Message, v, "***") + for _, v := range *masks { + cmasker = AppendSecretMasker(cmasker, v) } + + entry.Message = strings.NewReplacer(cmasker...).Replace(entry.Message) } return entry diff --git a/act/runner/logger_test.go b/act/runner/logger_test.go new file mode 100644 index 00000000..de3fc9e9 --- /dev/null +++ b/act/runner/logger_test.go @@ -0,0 +1,52 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "strings" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestValueMasker(t *testing.T) { + table := []struct { + name string + lines string + secrets map[string]string + masks []string + disallowed []string + }{ + { + name: "Multiline Private Key", + lines: "cat << EOF > private.key\nPRIVATE_KEY_BEGIN\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\nPRIVATE_KEY_END\nEOF", + secrets: map[string]string{ + "PRIVATE_KEY": "PRIVATE_KEY_BEGIN\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\nPRIVATE_KEY_END", + }, + disallowed: []string{"KEY", "dsdfseffefsefes", "PRIVATE_KEY_END"}, + }, + { + name: "Multiline Private Key in masks", + lines: "cat << EOF > private.key\nPRIVATE_KEY_BEGIN\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\nPRIVATE_KEY_END\nEOF", + masks: []string{"PRIVATE_KEY_BEGIN\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\ndsdfseffefsefes\nPRIVATE_KEY_END"}, + disallowed: []string{"KEY", "dsdfseffefsefes", "PRIVATE_KEY_END"}, + }, + } + for _, entry := range table { + t.Run(entry.name, func(t *testing.T) { + ctx := WithMasks(t.Context(), &entry.masks) + masker := valueMasker(false, entry.secrets) + for line := range strings.SplitSeq(entry.lines, "\n") { + lentry := masker(&logrus.Entry{ + Context: ctx, + Message: line, + }) + for _, line := range entry.disallowed { + assert.NotContains(t, lentry.Message, line) + } + } + }) + } +} diff --git a/internal/pkg/report/reporter.go b/internal/pkg/report/reporter.go index 5dbea735..9f0c653f 100644 --- a/internal/pkg/report/reporter.go +++ b/internal/pkg/report/reporter.go @@ -13,6 +13,7 @@ import ( "sync/atomic" "time" + "gitea.com/gitea/runner/act/runner" "gitea.com/gitea/runner/internal/pkg/client" "gitea.com/gitea/runner/internal/pkg/config" "gitea.com/gitea/runner/internal/pkg/metrics" @@ -73,13 +74,13 @@ type Reporter struct { func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task, cfg *config.Config) *Reporter { var oldnew []string if v := task.Context.Fields["token"].GetStringValue(); v != "" { - oldnew = append(oldnew, v, "***") + oldnew = runner.AppendSecretMasker(oldnew, v) } if v := task.Context.Fields["gitea_runtime_token"].GetStringValue(); v != "" { - oldnew = append(oldnew, v, "***") + oldnew = runner.AppendSecretMasker(oldnew, v) } for _, v := range task.Secrets { - oldnew = append(oldnew, v, "***") + oldnew = runner.AppendSecretMasker(oldnew, v) } rv := &Reporter{ @@ -689,7 +690,7 @@ func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow { matches := cmdRegex.FindStringSubmatch(content) if matches != nil { - if output := r.handleCommand(content, matches[1], matches[3]); output != nil { + if output := r.handleCommand(content, matches[1], runner.UnescapeCommandData(matches[3])); output != nil { content = *output } else { return nil @@ -705,6 +706,6 @@ func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow { } func (r *Reporter) addMask(msg string) { - r.oldnew = append(r.oldnew, msg, "***") + r.oldnew = runner.AppendSecretMasker(r.oldnew, msg) r.logReplacer = strings.NewReplacer(r.oldnew...) } diff --git a/internal/pkg/report/reporter_test.go b/internal/pkg/report/reporter_test.go index 3d76ea1f..4c25dfa3 100644 --- a/internal/pkg/report/reporter_test.go +++ b/internal/pkg/report/reporter_test.go @@ -50,6 +50,19 @@ func TestReporter_parseLogRow(t *testing.T) { "foo *** bar", }, }, + { + "Add-mask-multiline", false, + []string{ + "foo mysecret bar", + "::add-mask::LINE1%0ALINE2", + "foo LINE1 bar", + }, + []string{ + "foo mysecret bar", + "", + "foo *** bar", + }, + }, { "Debug enabled", true, []string{