redact

package
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package redact removes known secret values from text on its way somewhere that keeps it.

Why this exists

A task that holds a leased credential can echo it. Not maliciously — a `set -x` in a build script, a debug flag, an HTTP library that dumps its request headers, a stack trace carrying an argv. The credential then lands in three places that outlive the lease: the task artifact under .cloop/tasks/, the step log in state.db, and the live-log frame broadcast to every attached browser. The lease expires in minutes; the transcript does not.

pkg/audit already scans for this, which is the tell: the leak was expected and only reported afterwards. This package prevents it instead, at the point output is captured.

What it is not

It does not hunt for secret-shaped strings. There is no entropy heuristic and no regex for "looks like a token", because both are on a hot streaming path and both mangle ordinary text — a base64 blob in a diff is not a credential. It matches only values the caller already knows are secret, which for cloop means the material a lease just injected.

The short-value guard

A value shorter than MinLen is never matched. A four-character credential is not one, and replacing every occurrence of a short string would corrupt unrelated output for no security benefit. This is the same guard executor.RedactSecrets has always applied; it lives here now so there is one implementation rather than one per call site.

Index

Constants

View Source
const EnvKey = "CLOOP_REDACT_ENV"

EnvKey names the environment variable through which a lease declares which *other* variables hold credentials.

It carries names, never values — exactly like executor.SecretBinding.EnvKeys and for the same reason: it is inherited by every child process and would otherwise be one more copy of the secret. The indirection is what lets a workload scrub its own output without a heuristic: GITHUB_TOKEN holds a credential and CLOOP_GITHUB_REPO_ALLOWLIST holds a repository list, and nothing about the two values themselves says which is which.

View Source
const LeaseDirKey = "CLOOP_LEASE_DIR"

LeaseDirKey names the variable holding the lease's own credential directory. It is set by pkg/secretbroker; this package only reads it.

View Source
const Marker = "[redacted]"

Marker replaces a matched secret. It is the literal pkg/audit's scanners and the existing git-path redaction already use, so a transcript reads the same whichever layer caught the value.

View Source
const MinLen = 8

MinLen is the shortest value worth matching; see the package doc.

Variables

This section is empty.

Functions

func IsPlaceholder

func IsPlaceholder(v string) bool

IsPlaceholder reports whether v is something a redactor already left behind rather than a credential.

It matters because a Spec is stored with its leased variables replaced by a placeholder — see executorstore's redactLeasedEnv, which writes "<redacted:leased>" — and a spec rehydrated from that store after a hub restart would otherwise hand this package its own output as a secret to match. The result would not be a disclosure, but it would be a Set that exists to replace "[redacted]" with "[redacted]", doing per-chunk work on the streaming path for nothing and mangling a diagnostic string that is supposed to be legible.

A credential that genuinely looked like this would be skipped. That is the right trade: the value is already published in the source of two packages.

func Values

func Values(content []byte) []string

Values returns the raw and whitespace-trimmed forms of content, which is what a caller holding credential bytes in memory wants to put into a Set. It is the in-memory counterpart of what leaseDirValues does off disk.

Types

type Set

type Set struct {
	// contains filtered or unexported fields
}

Set is a collection of secret values to remove. A nil *Set is valid and redacts nothing, so callers with no lease need no special case.

Safe for concurrent use: it is immutable after New.

func FromEnviron

func FromEnviron(environ []string) *Set

FromEnviron builds a Set from a process environment: the values of every variable EnvKey names, plus the contents of every file in the lease directory.

This is how a workload redacts its *own* output. The hub knows what it injected and scrubs at the executor boundary, but the harness runs inside the sandbox and its output is captured there first — by the orchestrator, into an artifact — so the guarantee has to hold on both sides of that boundary.

Unreadable files are skipped rather than reported. A missing lease directory is the ordinary case for a project with no grants, and failing to redact is not a reason to fail a run.

func New

func New(values ...string) *Set

New returns a Set over values, ignoring those shorter than MinLen and any duplicates. It returns nil when nothing survives that filter, which callers may use to skip redaction entirely.

func (*Set) Bytes

func (s *Set) Bytes(b []byte) []byte

Bytes is String over a byte slice. The input is not modified; a copy is returned only when something matched.

func (*Set) Contains

func (s *Set) Contains(text string) bool

Contains reports whether text holds any value in the Set. It is the cheap question a test or an audit asks; callers scrubbing output want String.

func (*Set) Holdback

func (s *Set) Holdback(text string) int

Holdback returns how many trailing bytes of text a streaming caller must withhold because they could still turn out to be the start of a secret.

It is exported for callers that do their own buffering rather than writing through a Writer — a log bus fanning one chunk out to many subscribers cannot hand its bytes to an io.Writer and still number them. Zero means the whole of text is safe to publish now, which is the usual answer.

func (*Set) Len

func (s *Set) Len() int

Len reports how many values the Set will match.

func (*Set) String

func (s *Set) String(text string) string

String replaces every occurrence of every value in text with Marker.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer wraps an io.Writer so that nothing carrying a known secret reaches it.

Chunk boundaries are handled: a value split across two Writes is still caught, because the tail that could be the start of one is withheld until the next Write resolves it. Flush releases any such remainder, and callers that own the stream's end must call it or risk losing a trailing partial line.

Safe for concurrent use, because the streams it wraps — a driver's output pump and its reaper — race by nature.

func NewWriter

func NewWriter(dst io.Writer, set *Set) *Writer

NewWriter returns a Writer forwarding scrubbed output to dst. A nil Set yields a Writer that forwards verbatim, so the caller needs no branch.

func (*Writer) Flush

func (w *Writer) Flush() error

Flush writes any withheld remainder. It is safe to call more than once.

func (*Writer) Write

func (w *Writer) Write(p []byte) (int, error)

Write scrubs p and forwards it. It reports len(p) consumed on success even when part of p is being withheld, because the io.Writer contract is about what the caller may consider handed over, not about what has reached dst yet.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL