redact

package
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 10 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.

Two halves, and which one is the defence

A Set matches exact values: the material a lease just injected. It is the primary defence and the only thing on the streaming path. A Set does not hunt for secret-shaped strings — no entropy heuristic, no "looks like a token" regex — because both would cost on every streamed token and both mangle ordinary text: a base64 blob in a diff is not a credential.

The second half, in patterns.go, is a registry of credential shapes for the places that cannot know the values: an error message from three packages down, a browser's stack trace, a commit made last year. cloop audit, the provider-call audit, the secret broker's audit reasons and the telemetry scrubber all ask it, so a credential shape is recognised everywhere or nowhere. It is a backstop for text that never passed through a Set, not a substitute for one, and nothing on the streaming path calls it.

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 Scrub added in v0.0.5

func Scrub(text string) string

Scrub replaces every credential in text with Marker.

func ScrubFunc added in v0.0.5

func ScrubFunc(text string, repl func(Match) string) string

ScrubFunc returns text with every credential replaced by what repl returns for it. Overlapping matches are merged into one span first, so a token inside a URL inside a header is replaced once, and the merged Match carries the detector that starts earliest — the most specific on a tie.

It repeats until nothing more changes, at most maxPasses times. One pass is not always enough: a credential glued to another one — a key straight after a PEM block's last dash — fails its boundary check against bytes that the same pass replaces, and only the next pass sees it follow the replacement. Text with nothing in it costs one pass, as before.

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 Detector added in v0.0.5

type Detector struct {
	// Name is the kebab-case identifier, e.g. "github-token-long".
	Name string
	// Label is a short human description for a report line.
	Label string
	// Recognises says, for the reference page, what the detector matches.
	Recognises string
	// contains filtered or unexported fields
}

Detector is one named credential shape.

Name is stable: it is what cloop audit reports and what the fixture corpus keys on, so a detector is never renamed — retire it and add another.

func Detectors added in v0.0.5

func Detectors() []Detector

Detectors returns the registry in precedence order. The copies are safe to keep; matching does not depend on anything a caller can change.

type Live added in v0.0.5

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

Live is a Set over a process's lease that follows rewrites of its files. The zero value is not usable; call NewLive. Safe for concurrent use.

func NewLive added in v0.0.5

func NewLive(environ []string) *Live

NewLive returns a Live seeded from environ exactly as FromEnviron would be.

func (*Live) Active added in v0.0.5

func (l *Live) Active() bool

Active reports whether the process holds anything to scrub. A process with no lease — the control plane, a workload with no grants — gets false, and a caller can skip redaction entirely.

func (*Live) Current added in v0.0.5

func (l *Live) Current() *Set

Current returns the Set to scrub with now: everything the environment and the lease directory held at the start, plus every value the directory has held since. Nil when there is nothing to scrub.

type Match added in v0.0.5

type Match struct {
	// Detector is the Name of the detector that matched; Label is its Label.
	Detector, Label string
	// Start and End delimit the credential: the bytes a scanner removes.
	// Context a detector needed in order to recognise it — "Bearer ", a URL's
	// scheme and host, a "token:" key — lies outside and is kept.
	Start, End int
	// Kind is the credential's public lead when its format has one — "ghs_",
	// "sk-ant-api03-", "cloop_pat_" — and empty otherwise. It sits at Start.
	// A replacement may keep it: it says which kind of credential was removed
	// and nothing about the value.
	Kind string
	// contains filtered or unexported fields
}

Match is one credential found in a text.

func Find added in v0.0.5

func Find(text string) []Match

Find returns every credential in text, ordered by position.

Detectors are independent, so the same bytes can be reported twice — a GitHub token inside an Authorization header is both. A scanner that only needs to know what is there (cloop audit) wants exactly that; one that rewrites the text wants ScrubFunc, which merges overlaps first.

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.

func (*Set) With added in v0.0.5

func (s *Set) With(values ...string) *Set

With returns a Set over s's values and values, or s itself when values add nothing that survives New's filter. s is not modified — a Set is immutable, which is what lets one be read without a lock — so a holder whose credential changes while it runs (a GitHub App token refreshed mid-run, Task 20375) installs the result in place of the old Set. The old values stay: the credential a workload printed before the refresh must stay scrubbed after it.

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 NewFollowingWriter added in v0.0.5

func NewFollowingWriter(dst io.Writer, current func() *Set) *Writer

NewFollowingWriter is NewWriter over a Set that may grow while the stream is open: current is asked on every Write and Flush, and its answer scrubs that chunk together with whatever an earlier one withheld. A value printed before it joined the set has already gone; nothing can call it back.

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.

Directories

Path Synopsis
Package redacttest is the credential corpus every pattern scanner in cloop is held to: realistic credentials in the context they leak in, and ordinary output that has to come through untouched.
Package redacttest is the credential corpus every pattern scanner in cloop is held to: realistic credentials in the context they leak in, and ordinary output that has to come through untouched.

Jump to

Keyboard shortcuts

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