fix Task1 publication and secret preflight

This commit is contained in:
2026-08-11 04:06:23 +02:00
parent fcc45520ad
commit c7f7a6e1b0
8 changed files with 324 additions and 215 deletions
@@ -471,6 +471,14 @@ func Run(ctx context.Context, installation config.Installation, runner compose.R
// RunWithProjector validates the optional projected envelope before publishing
// any host export. This ordering is part of the workspace boundary contract.
func RunWithProjector(ctx context.Context, installation config.Installation, runner compose.Runner, command Command, stdin io.Reader, projector ResultProjector) (Result, error) {
return RunWithProjectorAndSecrets(ctx, installation, runner, command, stdin, projector, nil, nil)
}
// RunWithProjectorAndSecrets adds the host's universal no-secret boundary and a
// final-public-encoding preflight. prePublish runs after projection but before
// WriteCanonicalExclusive, so deterministic output rejection cannot consume the
// exclusive candidate name.
func RunWithProjectorAndSecrets(ctx context.Context, installation config.Installation, runner compose.Runner, command Command, stdin io.Reader, projector ResultProjector, secrets []string, prePublish func(Result) error) (Result, error) {
env, generated, e := makeInput(command)
if e != nil {
return Result{}, e
@@ -555,6 +563,9 @@ func RunWithProjector(ctx context.Context, installation config.Installation, run
}
result = projected
}
if err := rejectDeclaredSecrets(result, secrets); err != nil {
return Result{}, err
}
if hasExport {
if export == nil {
return Result{}, errors.New("invalid candidate export")
@@ -562,11 +573,24 @@ func RunWithProjector(ctx context.Context, installation config.Installation, run
if !candidateBoundToResult(*export, result) {
return Result{}, errors.New("invalid candidate identity")
}
if err := publishCandidate(export, result, outPath(command)); err != nil {
candidate, err := decodeCandidateExport(export)
if err != nil || containsDeclaredSecret(candidate, secrets) {
return Result{}, errors.New("invalid candidate export")
}
if prePublish != nil {
if err := prePublish(result); err != nil {
return Result{}, err
}
}
if err := publishCandidateBytes(export, result, outPath(command), candidate); err != nil {
return Result{}, err
}
} else if outPath(command) != "" {
return Result{}, errors.New("candidate export is required")
} else if prePublish != nil {
if err := prePublish(result); err != nil {
return Result{}, err
}
}
return result, nil
}
@@ -595,39 +619,80 @@ func candidateBoundToResult(x hostExport, result Result) bool {
}
func publishCandidate(x *hostExport, result Result, path string) error {
if x.MediaType != "application/yaml" && x.MediaType != "text/yaml" {
return errors.New("invalid candidate export")
b, err := decodeCandidateExport(x)
if err != nil {
return err
}
if !digestPattern.MatchString(x.SHA256) {
return errors.New("invalid candidate export")
return publishCandidateBytes(x, result, path, b)
}
func decodeCandidateExport(x *hostExport) ([]byte, error) {
if x.MediaType != "application/yaml" && x.MediaType != "text/yaml" || !digestPattern.MatchString(x.SHA256) {
return nil, errors.New("invalid candidate export")
}
b, e := base64.StdEncoding.DecodeString(x.ContentBase64)
if e != nil || len(b) > maxCandidate || !utf8.Valid(b) {
return errors.New("invalid candidate export")
b, err := base64.StdEncoding.DecodeString(x.ContentBase64)
if err != nil || len(b) > maxCandidate || !utf8.Valid(b) || DigestBytes(b) != x.SHA256 {
return nil, errors.New("invalid candidate export")
}
if DigestBytes(b) != x.SHA256 {
return errors.New("invalid candidate export")
}
var doc any
decoder := yaml.NewDecoder(bytes.NewReader(b))
var doc any
if decoder.Decode(&doc) != nil {
return errors.New("invalid candidate export")
return nil, errors.New("invalid candidate export")
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
return errors.New("invalid candidate export")
return nil, errors.New("invalid candidate export")
}
return b, nil
}
func publishCandidateBytes(x *hostExport, result Result, path string, b []byte) error {
if result.RunID == "" || !runIDPattern.MatchString(result.RunID) {
return errors.New("invalid candidate identity")
}
if path == "" {
return nil
}
if e = safeio.WriteCanonicalExclusive(path, b, 0o600); e != nil {
if err := safeio.WriteCanonicalExclusive(path, b, 0o600); err != nil {
return errors.New("unsafe output file")
}
return nil
}
func containsDeclaredSecret(contents []byte, secrets []string) bool {
for _, secret := range secrets {
if secret != "" && bytes.Contains(contents, []byte(secret)) {
return true
}
}
return false
}
// rejectDeclaredSecrets traverses the typed public envelope rather than its JSON
// encoding: JSON escaping must not turn a declared value into an apparent non-match.
func rejectDeclaredSecrets(result Result, secrets []string) error {
values := make([]string, 0, 16)
values = append(values, result.Status, result.Code, result.WorkspaceID, result.WorkspaceRevision, result.DescriptorBlob, result.Operation, result.RunID)
values = append(values, result.CompletedStages...)
values = append(values, result.Warnings...)
for key, value := range result.ChildRuns {
values = append(values, key, value)
}
for key := range result.Counts {
values = append(values, key)
}
for _, artifact := range result.ArtifactIdentities {
values = append(values, artifact.Kind, artifact.Digest)
}
for _, value := range values {
for _, secret := range secrets {
if secret != "" && strings.Contains(value, secret) {
return errors.New("workspace result contains a declared secret")
}
}
}
return nil
}
func validateResult(r Result, workspace, operation string) error {
if r.SchemaVersion != 1 || r.WorkspaceID != workspace || r.Operation != operation || !validStatus(r.Status) || !validCode(r.Code) || !revisionPattern.MatchString(r.WorkspaceRevision) || !revisionPattern.MatchString(r.DescriptorBlob) {
return errors.New("invalid workspace result")