fix Task1 publication and secret preflight
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user