Monorepo for Tangled tangled.org
1

Configure Feed

Select the types of activity you want to include in your feed.

Labels

None yet.

Participants 1
AT URI
at://did:plc:dfl62fgb7wtjj3fcbb72naae/sh.tangled.repo.pull/3mrmmthi3ml22
+165 -8
Diff #0
+41 -8
spindle/models/logger.go
··· 24 24 } 25 25 26 26 type FileWorkflowLogger struct { 27 - file *os.File 28 - encoder *json.Encoder 29 - mask *SecretMask 27 + file *os.File 28 + encoder *json.Encoder 29 + mask *SecretMask 30 + dataWriters []*dataWriter 30 31 } 31 32 32 33 func NewFileWorkflowLogger(baseDir string, wid WorkflowId, secretValues []string) (WorkflowLogger, error) { ··· 48 49 } 49 50 50 51 func (l *FileWorkflowLogger) Close() error { 52 + for _, w := range l.dataWriters { 53 + if err := w.flush(); err != nil { 54 + return err 55 + } 56 + } 51 57 return l.file.Close() 52 58 } 53 59 54 60 func (l *FileWorkflowLogger) DataWriter(idx int, stream string) io.Writer { 55 - return &dataWriter{ 61 + w := &dataWriter{ 56 62 logger: l, 57 63 idx: idx, 58 64 stream: stream, 59 65 } 66 + l.dataWriters = append(l.dataWriters, w) 67 + return w 60 68 } 61 69 62 70 func (l *FileWorkflowLogger) ControlWriter(idx int, step Step, stepStatus StepStatus) io.Writer { ··· 72 80 logger *FileWorkflowLogger 73 81 idx int 74 82 stream string 83 + // trailing bytes held back so a secret split across writes still 84 + // matches, flushed on Close or once enough data arrives 85 + pending []byte 75 86 } 76 87 77 88 func (w *dataWriter) Write(p []byte) (int, error) { 89 + w.pending = append(w.pending, p...) 90 + window := w.logger.mask.Window() 91 + // anything within window of the tail might be half a secret, keep 92 + // it buffered 93 + if len(w.pending) <= window { 94 + return len(p), nil 95 + } 96 + emit := w.pending[:len(w.pending)-window] 97 + // copy the tail out, emit still aliases the same backing array 98 + w.pending = append([]byte(nil), w.pending[len(w.pending)-window:]...) 99 + return len(p), w.emit(emit) 100 + } 101 + 102 + // the writer is done, so a buffered tail can no longer grow into a 103 + // full secret and goes out as-is 104 + func (w *dataWriter) flush() error { 105 + if len(w.pending) == 0 { 106 + return nil 107 + } 108 + pending := w.pending 109 + w.pending = nil 110 + return w.emit(pending) 111 + } 112 + 113 + func (w *dataWriter) emit(p []byte) error { 78 114 line := strings.TrimRight(string(p), "\r\n") 79 115 if w.logger.mask != nil { 80 116 line = w.logger.mask.Mask(line) 81 117 } 82 118 entry := NewDataLogLine(w.idx, line, w.stream) 83 - if err := w.logger.encoder.Encode(entry); err != nil { 84 - return 0, err 85 - } 86 - return len(p), nil 119 + return w.logger.encoder.Encode(entry) 87 120 } 88 121 89 122 type controlWriter struct {
+102
spindle/models/logger_test.go
··· 1 + package models 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "strings" 7 + "testing" 8 + ) 9 + 10 + func testWorkflowId(name string) WorkflowId { 11 + return WorkflowId{PipelineId: PipelineId{Knot: "knot1", Rkey: "rkey1"}, Name: name} 12 + } 13 + 14 + func TestDataWriterMasksSecretSplitAcrossWrites(t *testing.T) { 15 + dir := t.TempDir() 16 + secret := "hunter2-super-secret-token" 17 + wid := testWorkflowId("mask") 18 + logger, err := NewFileWorkflowLogger(dir, wid, []string{secret}) 19 + if err != nil { 20 + t.Fatal(err) 21 + } 22 + w := logger.DataWriter(0, "stdout") 23 + 24 + for _, ch := range strings.Split("prefix "+secret+" suffix", "") { 25 + if _, err := w.Write([]byte(ch)); err != nil { 26 + t.Fatal(err) 27 + } 28 + } 29 + if err := logger.Close(); err != nil { 30 + t.Fatal(err) 31 + } 32 + 33 + raw, err := os.ReadFile(filepath.Join(dir, wid.String()+".log")) 34 + if err != nil { 35 + t.Fatal(err) 36 + } 37 + if strings.Contains(string(raw), secret) { 38 + t.Errorf("log contains raw secret: %s", raw) 39 + } 40 + if !strings.Contains(string(raw), "***") { 41 + t.Errorf("log does not contain masked marker: %s", raw) 42 + } 43 + // trailing bytes land in the final flush entry contiguously 44 + if !strings.Contains(string(raw), "suffix") { 45 + t.Errorf("log lost trailing output: %s", raw) 46 + } 47 + } 48 + 49 + func TestDataWriterMasksSingleFrame(t *testing.T) { 50 + dir := t.TempDir() 51 + secret := "hunter2-super-secret-token" 52 + wid := testWorkflowId("frame") 53 + logger, err := NewFileWorkflowLogger(dir, wid, []string{secret}) 54 + if err != nil { 55 + t.Fatal(err) 56 + } 57 + w := logger.DataWriter(0, "stdout") 58 + if _, err := w.Write([]byte("token is " + secret + " ok")); err != nil { 59 + t.Fatal(err) 60 + } 61 + if err := logger.Close(); err != nil { 62 + t.Fatal(err) 63 + } 64 + 65 + raw, err := os.ReadFile(filepath.Join(dir, wid.String()+".log")) 66 + if err != nil { 67 + t.Fatal(err) 68 + } 69 + if strings.Contains(string(raw), secret) { 70 + t.Errorf("log contains raw secret: %s", raw) 71 + } 72 + if !strings.Contains(string(raw), "en is *** ok") { 73 + t.Errorf("masked entry mangled: %s", raw) 74 + } 75 + } 76 + 77 + func TestDataWriterNoMaskPassthrough(t *testing.T) { 78 + dir := t.TempDir() 79 + wid := testWorkflowId("plain") 80 + logger, err := NewFileWorkflowLogger(dir, wid, nil) 81 + if err != nil { 82 + t.Fatal(err) 83 + } 84 + w := logger.DataWriter(0, "stdout") 85 + if _, err := w.Write([]byte("hello")); err != nil { 86 + t.Fatal(err) 87 + } 88 + if _, err := w.Write([]byte(" world")); err != nil { 89 + t.Fatal(err) 90 + } 91 + if err := logger.Close(); err != nil { 92 + t.Fatal(err) 93 + } 94 + 95 + raw, err := os.ReadFile(filepath.Join(dir, wid.String()+".log")) 96 + if err != nil { 97 + t.Fatal(err) 98 + } 99 + if !strings.Contains(string(raw), "hello") || !strings.Contains(string(raw), " world") { 100 + t.Errorf("log missing output: %s", raw) 101 + } 102 + }
+22
spindle/models/secret_mask.go
··· 8 8 // SecretMask replaces secret values in strings with "***". 9 9 type SecretMask struct { 10 10 replacer *strings.Replacer 11 + // length of the longest secret. writers keep the last window-1 12 + // bytes unflushed so a secret split across writes can still match 13 + // whole 14 + window int 11 15 } 12 16 13 17 // NewSecretMask creates a mask for the given secret values. ··· 37 41 return nil 38 42 } 39 43 44 + window := 0 45 + for i := 0; i < len(pairs); i += 2 { 46 + window = max(window, len(pairs[i])) 47 + } 48 + 40 49 return &SecretMask{ 41 50 replacer: strings.NewReplacer(pairs...), 51 + window: window, 52 + } 53 + } 54 + 55 + // trailing bytes a streaming caller must keep unflushed so a secret 56 + // spanning a write boundary still matches 57 + func (m *SecretMask) Window() int { 58 + if m == nil { 59 + return 0 60 + } 61 + if m.window <= 1 { 62 + return 0 42 63 } 64 + return m.window - 1 43 65 } 44 66 45 67 // Mask replaces all registered secret values with "***".

History

1 round 0 comments
Sign up or Login to add to the discussion
ptr.pet submitted #0
1 commit
Expand
spindle/models: hold back a trailing window so secrets split across log writes stay masked
1/3 success, 2/3 timeout
Expand
Checking mergeability…
Expand 0 comments