vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
e82bbe7d
Keenan Nemetz
7d ago
|
1 | // Package redact keeps a job's secret values out of the text it produces. |
| 2 | // | |
| 3 | // The attempt log is written on the runner and streamed to the instance, so the | |
| 4 | // runner is the last place a value can be removed before it becomes part of a | |
| 5 | // record somebody reads. A local run has the same problem with a terminal and a | |
| 6 | // scrollback buffer. This package is the one implementation both use: it replaces | |
| 7 | // each delivered value with Placeholder as the bytes go by, and it holds back the | |
| 8 | // last few bytes of a write so a value split across two writes is still caught. | |
| 9 | // | |
| 10 | // It is not a guarantee. A value that has been re-encoded — base64, hex, a URL | |
| 11 | // escape, one piece of a connection string — no longer matches the bytes that were | |
| 12 | // delivered, and no amount of scanning finds it. A value shorter than MinBytes is | |
| 13 | // not masked at all: replacing every occurrence of "1" or "yes" would rewrite the | |
| 14 | // output and protect nothing that was worth calling a secret. | |
| 15 | package redact | |
| 16 | ||
| 17 | import ( | |
| 18 | "bytes" | |
| 19 | "io" | |
| 20 | "sort" | |
| 21 | "sync" | |
| 22 | ) | |
| 23 | ||
| 24 | const ( | |
| 25 | // Placeholder is what a masked value becomes. It is short on purpose: a masked | |
| 26 | // log should still read as a log. | |
| 27 | Placeholder = "***" | |
| 28 | // MinBytes is the shortest value worth masking. A three-byte string has 32 | |
| 29 | // possibilities per byte and is a flag, not a credential. | |
| 30 | MinBytes = 4 | |
| 31 | ) | |
| 32 | ||
| 33 | // Mask is a set of values to remove from a stream of bytes. | |
| 34 | // | |
| 35 | // A Mask is not safe for concurrent use. A stream that is written from more than | |
| 36 | // one goroutine uses a Writer, which serialises writes and owns its Mask. | |
| 37 | type Mask struct { | |
| 38 | patterns [][]byte | |
| 39 | longest int | |
| 40 | pending []byte | |
| 41 | } | |
| 42 | ||
| 43 | // New builds a mask from the values a job was given. Short values are dropped, and | |
| 44 | // the longest match wins where one value is a prefix of another, so the tail of a | |
| 45 | // longer value is not left behind. | |
| 46 | func New(values []string) *Mask { | |
| 47 | m := &Mask{} | |
| 48 | seen := make(map[string]bool, len(values)) | |
| 49 | for _, value := range values { | |
| 50 | if len(value) < MinBytes || seen[value] { | |
| 51 | continue | |
| 52 | } | |
| 53 | seen[value] = true | |
| 54 | m.patterns = append(m.patterns, []byte(value)) | |
| 55 | } | |
| 56 | sort.Slice(m.patterns, func(i, j int) bool { return len(m.patterns[i]) > len(m.patterns[j]) }) | |
| 57 | for _, pattern := range m.patterns { | |
| 58 | m.longest = max(m.longest, len(pattern)) | |
| 59 | } | |
| 60 | return m | |
| 61 | } | |
| 62 | ||
| 63 | // Empty reports whether there is nothing to mask, so a caller can skip the work. | |
| 64 | func (m *Mask) Empty() bool { return m == nil || len(m.patterns) == 0 } | |
| 65 | ||
| 66 | // Write takes one write's bytes and returns the prefix that can be emitted now, | |
| 67 | // already masked. The last longest-1 bytes are held back, because a value may | |
| 68 | // continue in the next write; Flush emits them. | |
| 69 | func (m *Mask) Write(p []byte) []byte { | |
| 70 | if m.Empty() { | |
| 71 | return p | |
| 72 | } | |
| 73 | m.pending = append(m.pending, p...) | |
| 74 | keep := m.longest - 1 | |
| 75 | if len(m.pending) <= keep { | |
| 76 | return nil | |
| 77 | } | |
| 78 | cut := len(m.pending) - keep | |
| 79 | out, taken := m.redactTo(m.pending, cut) | |
| 80 | // A match may have started before the cut and ended after it; it was emitted | |
| 81 | // whole, so it is consumed whole. | |
| 82 | m.pending = append(m.pending[:0], m.pending[taken:]...) | |
| 83 | return out | |
| 84 | } | |
| 85 | ||
| 86 | // Flush returns the held-back bytes, masked, and clears them. It is called when a | |
| 87 | // step ends and when the log is sealed, so the tail is never lost. | |
| 88 | func (m *Mask) Flush() []byte { | |
| 89 | if m.Empty() || len(m.pending) == 0 { | |
| 90 | return nil | |
| 91 | } | |
| 92 | out, _ := m.redactTo(m.pending, len(m.pending)) | |
| 93 | m.pending = nil | |
| 94 | return out | |
| 95 | } | |
| 96 | ||
| 97 | // String masks one complete message, such as a runner's own error text, without | |
| 98 | // touching the carry of the stream. It is for callers that have a whole string and | |
| 99 | // not a stream. | |
| 100 | func (m *Mask) String(s string) string { | |
| 101 | if m.Empty() || s == "" { | |
| 102 | return s | |
| 103 | } | |
| 104 | buf := []byte(s) | |
| 105 | out, _ := m.redactTo(buf, len(buf)) | |
| 106 | return string(out) | |
| 107 | } | |
| 108 | ||
| 109 | // redactTo masks occurrences in buf that start before cut and reports how many | |
| 110 | // input bytes it consumed, which can run past cut when a match spans it. | |
| 111 | func (m *Mask) redactTo(buf []byte, cut int) (out []byte, taken int) { | |
| 112 | out = make([]byte, 0, cut) | |
| 113 | i := 0 | |
| 114 | for i < cut { | |
| 115 | if n := m.matchAt(buf, i); n > 0 { | |
| 116 | out = append(out, Placeholder...) | |
| 117 | i += n | |
| 118 | continue | |
| 119 | } | |
| 120 | out = append(out, buf[i]) | |
| 121 | i++ | |
| 122 | } | |
| 123 | return out, i | |
| 124 | } | |
| 125 | ||
| 126 | // matchAt is the length of the longest value that starts at i, or zero. | |
| 127 | func (m *Mask) matchAt(buf []byte, i int) int { | |
| 128 | for _, pattern := range m.patterns { | |
| 129 | if len(pattern) <= len(buf)-i && bytes.Equal(buf[i:i+len(pattern)], pattern) { | |
| 130 | return len(pattern) | |
| 131 | } | |
| 132 | } | |
| 133 | return 0 | |
| 134 | } | |
| 135 | ||
| 136 | // Writer masks a stream and appends it to another writer. A command's stdout and | |
| 137 | // stderr are copied by two goroutines, so one Writer may be written from both; the | |
| 138 | // mutex makes that a serialised stream, which is what the log is anyway. | |
| 139 | type Writer struct { | |
| 140 | mu sync.Mutex | |
| 141 | m *Mask | |
| 142 | out io.Writer | |
| 143 | } | |
| 144 | ||
| 145 | // NewWriter wraps out and masks these values. | |
| 146 | func NewWriter(out io.Writer, values []string) *Writer { | |
| 147 | return &Writer{m: New(values), out: out} | |
| 148 | } | |
| 149 | ||
| 150 | // Write masks as much of p as can be emitted and reports the whole write as taken, | |
| 151 | // so a masked value never stops a program with a short write. | |
| 152 | func (w *Writer) Write(p []byte) (int, error) { | |
| 153 | w.mu.Lock() | |
| 154 | defer w.mu.Unlock() | |
| 155 | if !w.m.Empty() { | |
| 156 | if err := w.write(w.m.Write(p)); err != nil { | |
| 157 | return 0, err | |
| 158 | } | |
| 159 | return len(p), nil | |
| 160 | } | |
| 161 | return w.out.Write(p) | |
| 162 | } | |
| 163 | ||
| 164 | // Flush writes whatever the mask held back. A caller calls it when the stream ends. | |
| 165 | func (w *Writer) Flush() error { | |
| 166 | w.mu.Lock() | |
| 167 | defer w.mu.Unlock() | |
| 168 | if w.m.Empty() { | |
| 169 | return nil | |
| 170 | } | |
| 171 | return w.write(w.m.Flush()) | |
| 172 | } | |
| 173 | ||
| 174 | func (w *Writer) write(b []byte) error { | |
| 175 | if len(b) == 0 { | |
| 176 | return nil | |
| 177 | } | |
| 178 | _, err := w.out.Write(b) | |
| 179 | return err | |
| 180 | } |