Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/safepush.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"schemaVersion": "cloudring.safepush.github/v1",
"repository": "opencloudtech/CloudRING",
"repositoryId": 1288130713,
"workflows": [
{
"path": ".github/workflows/ci.yml",
"jobs": [
"docscheck",
"go-build-module-integrity",
"go-test",
"go-test-race",
"go-vet",
"ocs-conformance",
"ocs-validate",
"platform-manifests",
"postgres-integration",
"source-safety",
"windows-go-test"
]
},
{
"path": ".github/workflows/cla-dco.yml",
"jobs": [
"cla",
"dco"
]
},
{
"path": ".github/workflows/license.yml",
"jobs": [
"license-check"
]
},
{
"path": ".github/workflows/security.yml",
"jobs": [
"codeql",
"gosec",
"govulncheck",
"secret-scan"
]
},
{
"path": ".github/workflows/supply-chain.yml",
"jobs": [
"supply-chain-policy"
]
}
],
"excludedWorkflows": [
".github/workflows/release-provenance.yml"
]
}
8 changes: 4 additions & 4 deletions .github/scripts/build-release-etcdutl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ if [[ -e "$1" || -L "$1" ]]; then
exit 1
fi

version='3.6.14'
source_commit='fc04cf702b0a46c2fd85547a2be05705b100a496'
source_archive_sha256='c02ebbf6af5f9266f111009fb8d585649a9fcafe5788c200f5a28a7f00d438f0'
version='3.6.15'
source_commit='a3346427fbe6e456e9b6d6e429346481c6be7c71'
source_archive_sha256='32b1cb031a84067ae451cad1b060a07298bdd4e4ba290897ec0c73a02001e0bb'
source_url="https://codeload.github.com/etcd-io/etcd/tar.gz/${source_commit}"
go_version='go1.26.8'

Expand Down Expand Up @@ -52,7 +52,7 @@ for attempt in 1 2; do
test -d "${source_root}/etcdutl"
# The verified source archive binds the complete tree; these checks also
# prevent a version-label or module-path mismatch during a future pin bump.
grep -Eq '^[[:space:]]*Version[[:space:]]*=[[:space:]]*"3\.6\.14"$' \
grep -Eq '^[[:space:]]*Version[[:space:]]*=[[:space:]]*"3\.6\.15"$' \
"${source_root}/api/version/version.go"
grep -Fxq 'module go.etcd.io/etcd/etcdutl/v3' "${source_root}/etcdutl/go.mod"
(
Expand Down
6 changes: 3 additions & 3 deletions .github/scripts/check-release-binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ EOF
# block in build-release-etcdutl.sh. That script builds the binary under
# test from this exact source; drifting copies silently adjudicate against
# the wrong source tree.
etcd_version='3.6.14'
source_commit='fc04cf702b0a46c2fd85547a2be05705b100a496'
source_archive_sha256='c02ebbf6af5f9266f111009fb8d585649a9fcafe5788c200f5a28a7f00d438f0'
etcd_version='3.6.15'
source_commit='a3346427fbe6e456e9b6d6e429346481c6be7c71'
source_archive_sha256='32b1cb031a84067ae451cad1b060a07298bdd4e4ba290897ec0c73a02001e0bb'
go_version='go1.26.8'

etcd_version_arg=''
Expand Down
2 changes: 1 addition & 1 deletion .github/workflow-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
".github/workflows/ci.yml": "680cb5fe8f2fff0989e33f5c2da93d19963d9fd8280967b2baa5e045ea2f4e49",
".github/workflows/cla-dco.yml": "5c3437cfa6ec997d97a3e9218a87114c1bb14f524e648d0df775f5bc6340430f",
".github/workflows/license.yml": "813e0774ad869ca4898449c0ba087b005a3187b32455d0dc2f3a62d89b4e8b81",
".github/workflows/release-provenance.yml": "36712b212df0f999a1a699a41498030d5d634161d35de2a3ed3db80ffb7bbee9",
".github/workflows/release-provenance.yml": "0c5617215ef38dd86037e97c88220ed4af9d865b97121ef3a4ef84cfa6c06d1d",
".github/workflows/security.yml": "0c77bba18ea6e3d1bd1cf3447549fc0bbb4aaa3a1f94a2763dd53a3f42390e9e",
".github/workflows/supply-chain.yml": "2d90a0d90de0f9dffa40005ae9134105ac9198bbee58416d570ed16f96e58097"
}
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/release-provenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,10 @@ jobs:
cp "${RUNNER_TEMP}/release-etcdutl/etcdutl-source.json" dist/
install -d -m 0755 dist/etcd-licenses
cp "${RUNNER_TEMP}/release-etcdutl/etcd-LICENSE" dist/etcd-licenses/LICENSE
test "$(dist/etcd-recovery-worker/etcdutl version)" = $'etcdutl version: 3.6.14\nAPI version: 3.6'
test "$(dist/etcd-recovery-worker/etcdutl version)" = $'etcdutl version: 3.6.15\nAPI version: 3.6'
tool_digest="$(sha256sum dist/etcd-recovery-worker/etcdutl | cut -d ' ' -f 1)"
worker_digest="$(sha256sum dist/etcd-recovery-worker/cloudring-etcd-recovery-worker | cut -d ' ' -f 1)"
test "${tool_digest}" = '2b7fea46e31fda1989168fbd8f7ec73f5839155f4b1af7bab274950e337e996d'
test "${tool_digest}" = 'aaebd9806bd5d614b16de6bbff4aaf9d7c1986f48d52068e578d93451d000835'
bash .github/scripts/check-release-binaries.sh dist/etcd-recovery-worker/*
jq -n -cS \
--arg source_commit "${GITHUB_SHA}" \
Expand Down Expand Up @@ -278,10 +278,10 @@ jobs:
{
type: "application",
name: "etcdutl",
version: "3.6.14",
purl: "pkg:github/etcd-io/etcd@v3.6.14",
version: "3.6.15",
purl: "pkg:github/etcd-io/etcd@v3.6.15",
properties: [
{name: "cloudring.upstream.source-commit", value: "fc04cf702b0a46c2fd85547a2be05705b100a496"},
{name: "cloudring.upstream.source-commit", value: "a3346427fbe6e456e9b6d6e429346481c6be7c71"},
{name: "cloudring.upstream.build-compiler", value: "go1.26.8"}
],
hashes: [{alg: "SHA-256", content: $tool_digest}]
Expand Down Expand Up @@ -452,7 +452,7 @@ jobs:
componentSbomSha256: $component_sbom_digest,
containerfileSha256: $containerfile_digest,
etcdutlSha256: $etcdutl_digest,
etcdutlVersion: "3.6.14",
etcdutlVersion: "3.6.15",
imageDigest: $image_digest,
imageName: "ghcr.io/opencloudtech/cloudring-etcd-recovery-worker",
imageSbomSha256: $image_sbom_digest,
Expand Down Expand Up @@ -482,8 +482,8 @@ jobs:
(.imageDigest | test("^sha256:[0-9a-f]{64}$")) and
(.imageSubjectDigest | test("^sha256:[0-9a-f]{64}$")) and
(.workerExecutableSha256 | test("^[0-9a-f]{64}$")) and
.etcdutlVersion == "3.6.14" and
.etcdutlSha256 == "2b7fea46e31fda1989168fbd8f7ec73f5839155f4b1af7bab274950e337e996d" and
.etcdutlVersion == "3.6.15" and
.etcdutlSha256 == "aaebd9806bd5d614b16de6bbff4aaf9d7c1986f48d52068e578d93451d000835" and
.baseImageName == "gcr.io/distroless/static-debian12:nonroot" and
.baseImageDigest == "sha256:f5b485ea962d9bd1186b2f6b3a061191539b905b82ec395de78cbfae51f20e35" and
(.containerfileSha256 | test("^[0-9a-f]{64}$")) and
Expand Down
130 changes: 130 additions & 0 deletions cmd/cloudring-safepush/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) IURII TRUKHIN 2012-2022, Elena Trukhina 2023-2026. Project and trademarks: Elena Trukhina ZZP.

package main

import (
"context"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"time"

"github.com/opencloudtech/CloudRING/pkg/safepush"
)

type githubVerifier func(context.Context, *http.Client, string, safepush.GitHubPolicy, safepush.GitHubCandidate) error
type githubPolicyReader func(context.Context, *http.Client, string, string, string) (safepush.GitHubPolicy, error)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
os.Exit(run(ctx, os.Args[1:], os.Getenv, os.Stdout, os.Stderr, safepush.ReadGitHubPolicy, safepush.VerifyGitHub))
}

// The native required workflow owns these arguments. There is deliberately no
// caller-supplied check-results file, success flag, API host, or policy override.
func run(ctx context.Context, args []string, getenv func(string) string, stdout, stderr io.Writer, read githubPolicyReader, verify githubVerifier) int {
if len(args) == 0 || args[0] != "github" {
fmt.Fprintln(stderr, "usage: cloudring-safepush github --repository OWNER/REPO --pr NUMBER --head SHA --base SHA --branch main|master")
return 2
}
flags := flag.NewFlagSet("github", flag.ContinueOnError)
flags.SetOutput(io.Discard)
repository := flags.String("repository", "", "target GitHub repository")
pr := flags.Int("pr", 0, "pull request number")
head := flags.String("head", "", "exact proposed commit")
base := flags.String("base", "", "exact accepted target commit")
branch := flags.String("branch", "", "protected target branch")
timeout := flags.Duration("timeout", 20*time.Minute, "bounded wait for native CI, at most 30 minutes")
if flags.Parse(args[1:]) != nil || flags.NArg() != 0 || *repository == "" || *pr <= 0 ||
*head == "" || *base == "" || (*branch != "main" && *branch != "master") ||
*timeout <= 0 || *timeout > 30*time.Minute {
fmt.Fprintln(stderr, "SafePush request is invalid")
return 2
}
candidate := safepush.GitHubCandidate{PullRequest: *pr, HeadSHA: *head, BaseSHA: *base, TargetBranch: *branch}
if safepush.ValidateGitHubCandidate(*repository, candidate) != nil {
fmt.Fprintln(stderr, "SafePush request is invalid")
return 2
}
bearer := getenv("GITHUB_TOKEN")
if bearer == "" {
fmt.Fprintln(stderr, "SafePush requires the workflow's read-only GITHUB_TOKEN")
return 2
}
ctx, cancel := context.WithTimeout(ctx, *timeout)
defer cancel()
client := &http.Client{Timeout: 30 * time.Second}
policy, err := read(ctx, client, bearer, *repository, *base)
if err != nil {
fmt.Fprintln(stderr, "SafePush blocked:", safeDiagnostic(err))
return 1
}
if policy.Repository != *repository {
fmt.Fprintln(stderr, "SafePush blocked: accepted policy belongs to another repository")
return 1
}
err = waitForChecks(ctx, 30*time.Second, func() error { return verify(ctx, client, bearer, policy, candidate) })
if err != nil {
fmt.Fprintln(stderr, "SafePush blocked:", safeDiagnostic(err))
return 1
}
if _, err := fmt.Fprintln(stdout, "SafePush: the exact required CI checks passed for the current candidate and base"); err != nil {
return 1
}
return 0
}

func safeDiagnostic(err error) string {
switch {
case errors.Is(err, context.Canceled):
return "request canceled"
case errors.Is(err, context.DeadlineExceeded):
return "verification deadline exceeded"
case errors.Is(err, safepush.ErrPolicy):
return "accepted repository policy is invalid"
case errors.Is(err, safepush.ErrGitHubAPI):
return "GitHub API is unavailable or returned an invalid response"
case errors.Is(err, safepush.ErrGitHubSource):
return "workflow or policy source does not match the accepted revision"
case errors.Is(err, safepush.ErrGitHubState):
return "pull request or protected target is stale or changed during verification"
case errors.Is(err, safepush.ErrGitHubRun):
return "CI run, job, or step provenance is invalid"
case errors.Is(err, safepush.ErrMissingCheck):
return "a required CI job is missing"
case errors.Is(err, safepush.ErrUnexpectedCheck), errors.Is(err, safepush.ErrDuplicateObservation):
return "the CI job set is unexpected or ambiguous"
case errors.Is(err, safepush.ErrCheckNotSuccessful):
return "a required CI job did not complete successfully"
default:
return "verification failed"
}
}

func waitForChecks(ctx context.Context, interval time.Duration, verify func() error) error {
for {
if err := ctx.Err(); err != nil {
return err
}
err := verify()
if contextErr := ctx.Err(); contextErr != nil {
return contextErr
}
if !errors.Is(err, safepush.ErrGitHubPending) {
return err
}
timer := time.NewTimer(interval)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
}
90 changes: 90 additions & 0 deletions cmd/cloudring-safepush/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) IURII TRUKHIN 2012-2022, Elena Trukhina 2023-2026. Project and trademarks: Elena Trukhina ZZP.

package main

import (
"bytes"
"context"
"errors"
"net/http"
"strings"
"testing"
"time"

"github.com/opencloudtech/CloudRING/pkg/safepush"
)

func TestPendingIsTheOnlyRetriableAdmissionResult(t *testing.T) {
for _, terminal := range []error{nil, safepush.ErrCheckNotSuccessful, safepush.ErrPolicy, errors.New("unavailable")} {
calls := 0
err := waitForChecks(context.Background(), time.Nanosecond, func() error {
calls++
if calls == 1 {
return safepush.ErrGitHubPending
}
return terminal
})
if calls != 2 || !errors.Is(err, terminal) {
t.Fatalf("calls=%d error=%v", calls, err)
}
}
}

func TestCancellationStopsPendingAdmission(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
calls := 0
err := waitForChecks(ctx, time.Hour, func() error { calls++; cancel(); return safepush.ErrGitHubPending })
if !errors.Is(err, context.Canceled) || calls != 1 {
t.Fatalf("calls=%d error=%v", calls, err)
}
}

func TestSuccessAfterCancellationIsNotAccepted(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
err := waitForChecks(ctx, time.Nanosecond, func() error { cancel(); return nil })
if !errors.Is(err, context.Canceled) {
t.Fatalf("late success was accepted: %v", err)
}
}

func TestCommandDoesNotEchoSensitiveProviderErrors(t *testing.T) {
args := []string{"github", "--repository", "synthetic/project", "--pr", "1", "--head", strings.Repeat("a", 40), "--base", strings.Repeat("b", 40), "--branch", "main"}
marker := "synthetic-confidential-marker"
read := func(context.Context, *http.Client, string, string, string) (safepush.GitHubPolicy, error) {
return safepush.GitHubPolicy{Repository: "synthetic/project"}, nil
}
for _, failure := range []error{nil, errors.New(marker)} {
var out, errout bytes.Buffer
code := run(context.Background(), args, func(string) string { return marker }, &out, &errout, read,
func(context.Context, *http.Client, string, safepush.GitHubPolicy, safepush.GitHubCandidate) error {
return failure
})
if (code == 0) != (failure == nil) {
t.Fatalf("unexpected exit %d", code)
}
if strings.Contains(out.String()+errout.String(), marker) {
t.Fatal("provider error leaked sensitive input")
}
}
}

func TestInvalidInvocationMakesNoProviderCall(t *testing.T) {
for _, args := range [][]string{
{}, {"github", "--trusted=true"},
{"github", "--repository", "synthetic/project", "--pr", "1", "--head", "a", "--base", "b", "--branch", "feature"},
{"github", "--repository", "synthetic/project", "--pr", "1", "--head", "a", "--base", "b", "--branch", "main", "--timeout", "31m"},
{"github", "--repository", "synthetic/project", "--pr", "1", "--head", "a", "--base", "main", "--branch", "main"},
{"github", "--repository", "synthetic/../project", "--pr", "1", "--head", strings.Repeat("a", 40), "--base", strings.Repeat("b", 40), "--branch", "main"},
{"github", "--repository", "synthetic/project", "--pr", "1", "--head", strings.Repeat("0", 40), "--base", strings.Repeat("b", 40), "--branch", "main"},
} {
var out bytes.Buffer
read := func(context.Context, *http.Client, string, string, string) (safepush.GitHubPolicy, error) {
t.Fatal("provider called for invalid request")
return safepush.GitHubPolicy{}, nil
}
if code := run(context.Background(), args, func(string) string { return "synthetic-test-value" }, &out, &out, read, nil); code != 2 {
t.Fatalf("exit=%d", code)
}
}
}
4 changes: 2 additions & 2 deletions contracts/etcd-recovery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ GET with system CA validation, no proxy and no redirect. Raw endpoint, bucket,
object key/version and credentials never appear in tool arguments, environment,
stdout, stderr or receipts.

`etcdutl` 3.6.14 is content-pinned to
`2b7fea46e31fda1989168fbd8f7ec73f5839155f4b1af7bab274950e337e996d`
`etcdutl` 3.6.15 is content-pinned to
`aaebd9806bd5d614b16de6bbff4aaf9d7c1986f48d52068e578d93451d000835`
before execution. This executable is rebuilt from the unchanged official source
with Go 1.26.8 and retained symbols using the [release build procedure](../../docs/releasing.md);
its source and build identity are retained as `etcdutl-source.json`.
Expand Down
4 changes: 2 additions & 2 deletions contracts/etcd-recovery/image-identity.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"imageDigest": {"$ref": "#/$defs/imageDigest"},
"imageSubjectDigest": {"$ref": "#/$defs/imageDigest"},
"workerExecutableSha256": {"$ref": "#/$defs/sha"},
"etcdutlVersion": {"const": "3.6.14"},
"etcdutlSha256": {"const": "2b7fea46e31fda1989168fbd8f7ec73f5839155f4b1af7bab274950e337e996d"},
"etcdutlVersion": {"const": "3.6.15"},
"etcdutlSha256": {"const": "aaebd9806bd5d614b16de6bbff4aaf9d7c1986f48d52068e578d93451d000835"},
"baseImageName": {"const": "gcr.io/distroless/static-debian12:nonroot"},
"baseImageDigest": {"const": "sha256:f5b485ea962d9bd1186b2f6b3a061191539b905b82ec395de78cbfae51f20e35"},
"containerfileSha256": {"$ref": "#/$defs/sha"},
Expand Down
Loading
Loading