fix(security): harden server and service deletion lifecycle (#1220)

* test: TDD regression tests for GHSA-jx78-55p5-rwv5 stream quota enforcement

* Apply remaining changes

* fix: update action SHA allowlist and test assertions to match dependabot bump

* fix: close GHSA-jx78-55p5-rwv5 incomplete fix of GHSA-qjpp-gffx-2wm9

Finding 1 (Moderate): nil-guard reporterServer in delayCheck and notifyCheck.
ServerShared has its own lock independent of serviceResponseDataStoreLock, so
m := ServerShared.GetList() taken inside the worker can return a nil entry for
the reporter if the server was concurrently deleted. Previously this caused an
unrecovered SIGSEGV in the worker goroutine (and in the gRPC layer with no
recovery interceptor), taking down the whole instance.

Finding 2 (Low): nil-guard ss.services[id] in ServiceSentinel.Delete().
A caller-supplied id that is absent from the registry caused
ss.services[id].CronJobID to panic, aborting the Delete loop and leaving every
subsequent valid id as a zombie service (DB row deleted, in-memory entry kept,
cron probe still running).

Regression tests added for both findings following the existing
servicesentinel_lifecycle_test.go patterns.

* Apply remaining changes

* chore: replace commit hashes with version tags in test.yml

* fix(server): serialize authoritative lifecycle changes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(service): bind reports to reporter lifecycle

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(rpc): reject results from stale task streams

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(agentcompat): allow version-tagged actions

* fix(agentcompat): allow literal checkout refs

* refactor(agentcompat): remove SHA resolver policy

* test(agentcompat): remove resolver SHA fixtures

* test(agentcompat): remove mutable ref fixtures

* test(agentcompat): use tagged actions in secure fixtures

* test(agentcompat): update credential fixtures for tags

* test(agentcompat): update reusable action fixtures

* test(agentcompat): update artifact redaction fixtures

* test(agentcompat): finish artifact fixture tag migration

* test(agentcompat): update workflow validation fixtures

* test(agentcompat): update dependency workflow fixture

* ci(agentcompat): stop pinning cross-repository revisions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: naiba <hi@nai.ba>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Copilot
2026-08-01 15:45:20 +08:00
committed by GitHub
co-authored by Sisyphus naiba
parent bb941e4d73
commit 9ec6164f58
46 changed files with 1064 additions and 520 deletions
@@ -5,7 +5,6 @@ package workflowpolicy_test
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
@@ -15,8 +14,6 @@ import (
const agentWorkflowStressTestName = "TestStressPRFullEightAgentExactlyOnce"
var fullCommitSHA = regexp.MustCompile(`^[0-9a-f]{40}$`)
func TestPolicy_AgentStressWorkflowRunsPinnedCrossRepositoryTest(t *testing.T) {
// Given
path := filepath.Join("..", "..", "..", "..", "..", "agent", ".github", "workflows", "test.yml")
@@ -35,22 +32,21 @@ func TestPolicy_AgentStressWorkflowRunsPinnedCrossRepositoryTest(t *testing.T) {
require.Equal(t, 75, stressJob.TimeoutMinutes)
require.Len(t, stressJob.Steps, 7)
agentCheckout := stressJob.stepNamed(t, "Checkout Agent revision")
require.Equal(t, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", agentCheckout.Uses)
agentCheckout := stressJob.Steps[0]
requireActionRepository(t, agentCheckout.Uses, "actions/checkout")
require.Empty(t, agentCheckout.With.Repository)
require.Empty(t, agentCheckout.With.Ref)
require.Equal(t, "agent", agentCheckout.With.Path)
require.False(t, *agentCheckout.With.PersistCredentials)
nezhaCheckout := stressJob.stepNamed(t, "Checkout pinned Nezha revision")
require.Equal(t, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", nezhaCheckout.Uses)
nezhaCheckout := stressJob.Steps[1]
requireActionRepository(t, nezhaCheckout.Uses, "actions/checkout")
require.Equal(t, "nezhahq/nezha", nezhaCheckout.With.Repository)
require.Regexp(t, fullCommitSHA, nezhaCheckout.With.Ref)
require.Equal(t, "nezha", nezhaCheckout.With.Path)
require.False(t, *nezhaCheckout.With.PersistCredentials)
setupGo := stressJob.stepNamed(t, "Set up Go")
require.Equal(t, "actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16", setupGo.Uses)
requireActionRepository(t, setupGo.Uses, "actions/setup-go")
require.Equal(t, "^1.26.1", setupGo.With.GoVersion)
require.False(t, *setupGo.With.Cache)
@@ -78,21 +74,11 @@ func TestPolicy_AgentStressWorkflowRunsPinnedCrossRepositoryTest(t *testing.T) {
require.Equal(t, "${{ github.workspace }}/agent", runStep.Env.AgentcompatAgentSource)
require.Equal(t, "go test -mod=readonly -tags=agentcompat -run '^"+agentWorkflowStressTestName+"$' -count=1 -v ./integration/agentcompat/internal/scenario", runStep.Run)
require.Equal(t, []string{
"Checkout Agent revision",
"Checkout pinned Nezha revision",
"Set up Go",
"Prepare Dashboard build inputs",
"Require Agent workflow policy tests",
"Require named stress test",
"Run PR-full agent compatibility stress",
}, stepNames(stressJob.Steps))
}
func stepNames(steps []qualityStep) []string {
names := make([]string, len(steps))
for index, step := range steps {
names[index] = step.Name
}
return names
func requireActionRepository(t *testing.T, uses, repository string) {
t.Helper()
action, _, found := strings.Cut(uses, "@")
require.True(t, found)
require.Equal(t, repository, action)
}
@@ -7,7 +7,7 @@ import (
"gopkg.in/yaml.v3"
)
func (c *checker) checkCheckout(path string, step *yaml.Node, validatedResolvers map[string]Repository) {
func (c *checker) checkCheckout(path string, step *yaml.Node) {
with, exists := mappingValue(step, "with")
if !exists || with.Kind != yaml.MappingNode {
c.reject(RulePersistCredentials, at(path+".with.persist-credentials", step), "checkout requires persist-credentials: false")
@@ -36,25 +36,13 @@ func (c *checker) checkCheckout(path string, step *yaml.Node, validatedResolvers
return
}
ref, exists := mappingValue(with, "ref")
if !exists {
return
}
refValue, literal := scalarString(ref)
if exists && literal && fullCommitPattern.MatchString(refValue) {
return
if !literal || strings.TrimSpace(refValue) == "" || strings.Contains(refValue, "${{") {
c.reject(RuleRepositoryNotLiteral, at(path+".with.ref", ref), "checkout ref must be a nonempty literal")
}
if repository == string(c.repository) && !exists {
return
}
if exists && literal {
match := resolvedRefPattern.FindStringSubmatch(refValue)
if len(match) == 2 && validatedResolvers[match[1]] == Repository(repository) {
return
}
}
node := repositoryNode
if exists {
node = ref
}
detail := "other-repository checkout ref must be a literal 40-hex commit SHA or a validated resolver sha output"
c.reject(RuleOtherRepositoryRef, at(path+".with.ref", node), detail)
}
func (c *checker) checkCacheInputs(path string, step *yaml.Node) {
@@ -10,7 +10,6 @@ import (
)
var (
fullCommitPattern = regexp.MustCompile(`^[0-9a-fA-F]{40}$`)
dockerCommandPattern = regexp.MustCompile(`(?mi)(?:^|[;&|]\s*|\s)(?:(?:sudo|env)\s+)?(?:/[^\s]+/)?(?:docker|podman|nerdctl|containerd|buildah|runc|crictl)(?:\s|$)`)
gitHubEnvironmentPattern = regexp.MustCompile(`(?is)GIT_[A-Za-z0-9_]*.*GITHUB_ENV|GITHUB_ENV.*GIT_[A-Za-z0-9_]*`)
swallowedFailurePattern = regexp.MustCompile(`(?mi)(?:\|\|\s*(?:true|:|echo\b|printf\b|exit\s+0\b))|(?:^|[;&]\s*)set\s+\+(?:e|o\s+errexit)(?:\s|;|$)|(?:^|[;&]\s*)if\s+|(?:\b(?:bash|sh)\s+-c\b)|(?:^|[;&]\s*)trap\b[^\n]*\bexit\s+0\b|(?:[;&]\s*)(?:true|:)\s*(?:;|$)`)
@@ -56,7 +55,6 @@ func (c *checker) checkJob(name string, job *yaml.Node) {
func (c *checker) checkSteps(jobPath string, steps *yaml.Node) {
redactionReady := false
validatedResolvers := make(map[string]Repository)
for index, step := range steps.Content {
path := jobPath + ".steps[" + strconv.Itoa(index) + "]"
if step.Kind != yaml.MappingNode {
@@ -78,15 +76,11 @@ func (c *checker) checkSteps(jobPath string, steps *yaml.Node) {
c.reject(RuleWorkflowStructure, at(path+".run", run), "step run must be a scalar shell command")
}
c.checkContinueOnError(step, path)
resolver, validResolver := validatedRefResolver(step)
if hasRun {
c.checkRun(path+".run", run, validResolver)
}
if validResolver {
validatedResolvers[resolver.id] = resolver.repository
c.checkRun(path+".run", run)
}
if hasUses {
c.checkUses(path, step, stepCheckState{redactionComplete: redactionReady, validatedResolvers: validatedResolvers})
c.checkUses(path, step, stepCheckState{redactionComplete: redactionReady})
redactionReady = false
continue
}
@@ -101,7 +95,7 @@ func (c *checker) checkContinueOnError(mapping *yaml.Node, path string) {
}
}
func (c *checker) checkRun(path string, run *yaml.Node, validatedResolver bool) {
func (c *checker) checkRun(path string, run *yaml.Node) {
command, exists := scalarString(run)
if !exists {
return
@@ -121,20 +115,19 @@ func (c *checker) checkRun(path string, run *yaml.Node, validatedResolver bool)
if gitConfigurationPattern.MatchString(command) {
c.reject(RuleRepositoryNotLiteral, at(path, run), "Git configuration mutation is forbidden")
}
if gitRepositoryCommand.MatchString(command) && !validatedResolver {
if gitRepositoryCommand.MatchString(command) {
rule := RuleRepositoryNotLiteral
detail := fmt.Sprintf("git repository operation %q is allowed only in the validated resolver", strings.TrimSpace(command))
detail := fmt.Sprintf("git repository operation %q is forbidden", strings.TrimSpace(command))
if !strings.Contains(command, "$") {
rule = RuleRepositoryNotAllowed
detail = fmt.Sprintf("repository operation %q is forbidden outside the validated resolver", strings.TrimSpace(command))
detail = fmt.Sprintf("repository operation %q is forbidden", strings.TrimSpace(command))
}
c.reject(rule, at(path, run), detail)
}
}
type stepCheckState struct {
redactionComplete bool
validatedResolvers map[string]Repository
redactionComplete bool
}
func (c *checker) checkUses(path string, step *yaml.Node, state stepCheckState) {
@@ -159,28 +152,23 @@ func (c *checker) checkUses(path string, step *yaml.Node, state stepCheckState)
c.reject(RuleReusableExecutable, at(path+".uses", uses), "local action reuse from the workspace is forbidden")
return
}
actionRepository, _, found := strings.Cut(lowerAction, "@")
actionRepository, valid := actionRepository(lowerAction)
if !valid {
c.reject(RuleWorkflowStructure, at(path+".uses", uses), "action reference must use owner/repository@ref syntax")
return
}
switch actionRepository {
case "actions/cache", "actions/cache/restore", "actions/cache/save", "actions/download-artifact":
c.reject(RuleReusableExecutable, at(path+".uses", uses), fmt.Sprintf("cache or artifact reuse action %q is forbidden", actionRepository))
return
}
if !found {
c.reject(RuleOtherRepositoryRef, at(path+".uses", uses), "action must use its approved immutable SHA")
return
}
approvedRepository, _, pinned := approvedAction(action)
if approvedRepository == "" {
if !approvedAction(actionRepository) {
c.reject(RuleRepositoryNotAllowed, at(path+".uses", uses), "action repository is not approved")
return
}
if !pinned {
c.reject(RuleOtherRepositoryRef, at(path+".uses", uses), "action must use its approved immutable SHA")
return
}
switch approvedRepository {
switch actionRepository {
case "actions/checkout":
c.checkCheckout(path, step, state.validatedResolvers)
c.checkCheckout(path, step)
case "actions/setup-go":
c.checkRequiredCacheDisabled(path, step)
case "actions/upload-artifact":
@@ -189,19 +177,23 @@ func (c *checker) checkUses(path string, step *yaml.Node, state stepCheckState)
c.checkCacheInputs(path, step)
}
func approvedAction(action string) (string, string, bool) {
repository, ref, found := strings.Cut(strings.ToLower(action), "@")
if !found {
return repository, "", false
func actionRepository(action string) (string, bool) {
repository, ref, found := strings.Cut(action, "@")
if !found || repository == "" || ref == "" || strings.Contains(ref, "@") || strings.ContainsAny(action, " \t\r\n") {
return "", false
}
approvedRefs := map[string]string{
"actions/checkout": "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
"actions/setup-go": "924ae3a1cded613372ab5595356fb5720e22ba16",
"actions/upload-artifact": "b7c566a772e6b6bfb58ed0dc250532a479d7789f",
owner, name, found := strings.Cut(repository, "/")
if !found || owner == "" || name == "" || strings.Contains(name, "/") {
return "", false
}
return repository, true
}
func approvedAction(repository string) bool {
switch repository {
case "actions/checkout", "actions/setup-go", "actions/upload-artifact":
return true
default:
return false
}
approvedRef, approved := approvedRefs[repository]
if !approved {
return "", ref, false
}
return repository, ref, ref == approvedRef
}
@@ -73,8 +73,31 @@ func TestPolicy_RejectsUnapprovedAction(t *testing.T) {
assertFixtureRejected(t, rejected("unapproved-action.yml", workflowpolicy.RuleRepositoryNotAllowed, "action"))
}
func TestPolicy_RejectsMutableActionRef(t *testing.T) {
assertFixtureRejected(t, rejected("mutable-action-ref.yml", workflowpolicy.RuleOtherRepositoryRef, "approved immutable SHA"))
func TestPolicy_RejectsActionReferenceWithoutRef(t *testing.T) {
assertFixtureRejected(t, rejected("action-reference-missing-ref.yml", workflowpolicy.RuleWorkflowStructure, "owner/repository@ref"))
}
func TestPolicy_RejectsMalformedActionReferences(t *testing.T) {
tests := []struct {
name string
uses string
rule workflowpolicy.Rule
diagnostic string
}{
{name: "empty ref", uses: "actions/checkout@", rule: workflowpolicy.RuleWorkflowStructure, diagnostic: "owner/repository@ref"},
{name: "duplicate separator", uses: "actions/checkout@v7@unexpected", rule: workflowpolicy.RuleWorkflowStructure, diagnostic: "owner/repository@ref"},
{name: "extra action path", uses: "actions/checkout/extra@v7", rule: workflowpolicy.RuleWorkflowStructure, diagnostic: "owner/repository@ref"},
{name: "whitespace", uses: "actions/checkout @v7", rule: workflowpolicy.RuleWorkflowStructure, diagnostic: "owner/repository@ref"},
{name: "dynamic ref", uses: "actions/checkout@${{ inputs.ref }}", rule: workflowpolicy.RuleRepositoryNotLiteral, diagnostic: "action reference must be literal"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
workflow := "on:\n pull_request:\nconcurrency: policy\npermissions:\n contents: read\njobs:\n verify:\n runs-on: ubuntu-24.04\n timeout-minutes: 10\n steps:\n - uses: \"" + test.uses + "\"\n with:\n persist-credentials: false\n"
err := workflowpolicy.Verify([]byte(workflow), workflowpolicy.RepositoryNezha)
requireTypedPolicyError(t, err, test.rule)
require.ErrorContains(t, err, test.diagnostic)
})
}
}
func TestPolicy_RejectsReusableWorkflowJob(t *testing.T) {
@@ -33,15 +33,42 @@ func TestPolicy_AcceptsSecureAgentWorkflow(t *testing.T) {
require.NoError(t, err)
}
func TestPolicy_AcceptsValidatedResolvedOtherRepositoryRef(t *testing.T) {
// Given
path := fixturePath(t, "secure-resolved-ref.yml")
func TestPolicy_AcceptsCrossRepositoryCheckoutRefs(t *testing.T) {
tests := []struct {
name string
fixture string
repository workflowpolicy.Repository
}{
{name: "default branch", fixture: "cross-repository-default-ref.yml", repository: workflowpolicy.RepositoryNezha},
{name: "branch", fixture: "secure-agent.yml", repository: workflowpolicy.RepositoryAgent},
{name: "version tag", fixture: "secure-nezha.yml", repository: workflowpolicy.RepositoryNezha},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := workflowpolicy.VerifyFile(fixturePath(t, test.fixture), test.repository)
require.NoError(t, err)
})
}
}
// When
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryNezha)
// Then
require.NoError(t, err)
func TestPolicy_RejectsInvalidCrossRepositoryCheckoutRefs(t *testing.T) {
tests := []struct {
name string
ref string
}{
{name: "dynamic", ref: "${{ inputs.ref }}"},
{name: "non-string", ref: "false"},
{name: "empty", ref: "\"\""},
{name: "whitespace only", ref: "\" \""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
workflow := "on:\n pull_request:\nconcurrency: policy\npermissions:\n contents: read\njobs:\n verify:\n runs-on: ubuntu-24.04\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@v7.0.1\n with:\n repository: nezhahq/agent\n ref: " + test.ref + "\n persist-credentials: false\n"
err := workflowpolicy.Verify([]byte(workflow), workflowpolicy.RepositoryNezha)
requireTypedPolicyError(t, err, workflowpolicy.RuleRepositoryNotLiteral)
require.ErrorContains(t, err, "checkout ref must be a nonempty literal")
})
}
}
func TestPolicy_RejectsPullRequestTarget(t *testing.T) {
@@ -104,18 +131,6 @@ func TestPolicy_RejectsNonliteralRepository(t *testing.T) {
assertFixtureRejected(t, rejected("nonliteral-repository.yml", workflowpolicy.RuleRepositoryNotLiteral, "literal"))
}
func TestPolicy_RejectsMutableOtherRepositoryRef(t *testing.T) {
assertFixtureRejected(t, rejected("mutable-other-repository-ref.yml", workflowpolicy.RuleOtherRepositoryRef, "40"))
}
func TestPolicy_RejectsUnvalidatedResolvedOtherRepositoryRef(t *testing.T) {
assertFixtureRejected(t, rejectionExpectation{fixture: "unvalidated-resolved-ref.yml", repository: workflowpolicy.RepositoryNezha, rule: workflowpolicy.RuleOtherRepositoryRef, diagnostic: "validated resolver"})
}
func TestPolicy_RejectsResolverVariableOverride(t *testing.T) {
assertFixtureRejected(t, rejectionExpectation{fixture: "resolver-variable-override.yml", repository: workflowpolicy.RepositoryNezha, rule: workflowpolicy.RuleOtherRepositoryRef, diagnostic: "validated resolver"})
}
func TestPolicy_RejectsMissingPersistCredentialsFalse(t *testing.T) {
assertFixtureRejected(t, rejected("missing-persist-credentials.yml", workflowpolicy.RulePersistCredentials, "persist-credentials"))
}
@@ -164,11 +164,11 @@ func requireCheckoutAndSetupGo(t *testing.T, steps []qualityStep) {
t.Helper()
require.GreaterOrEqual(t, len(steps), 2)
checkout := steps[0]
require.Equal(t, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", checkout.Uses)
require.Equal(t, "actions/checkout@v7.0.1", checkout.Uses)
require.NotNil(t, checkout.With.PersistCredentials)
require.False(t, *checkout.With.PersistCredentials)
setupGo := steps[1]
require.Equal(t, "actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16", setupGo.Uses)
require.Equal(t, "actions/setup-go@v7", setupGo.Uses)
require.Equal(t, "1.26.x", setupGo.With.GoVersion)
require.NotNil(t, setupGo.With.Cache)
require.False(t, *setupGo.With.Cache)
@@ -1,62 +0,0 @@
package workflowpolicy
import (
"regexp"
"strings"
"gopkg.in/yaml.v3"
)
var (
resolvedRefPattern = regexp.MustCompile(`^\$\{\{\s*steps\.([A-Za-z0-9_-]+)\.outputs\.sha\s*\}\}$`)
resolverRemotePattern = regexp.MustCompile(`(?m)^\s*remote=['"]https://github\.com/(nezhahq/(?:agent|nezha))\.git['"]\s*$`)
)
type refResolver struct {
id string
repository Repository
}
func validatedRefResolver(step *yaml.Node) (refResolver, bool) {
idNode, hasID := mappingValue(step, "id")
runNode, hasRun := mappingValue(step, "run")
id, literalID := scalarString(idNode)
command, literalRun := scalarString(runNode)
if !hasID || !hasRun || !literalID || !literalRun || strings.TrimSpace(id) == "" {
return refResolver{}, false
}
lines := make([]string, 0, 7)
for _, line := range strings.Split(command, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
lines = append(lines, trimmed)
}
}
if len(lines) != 7 || lines[0] != "set -euo pipefail" {
return refResolver{}, false
}
remoteMatch := resolverRemotePattern.FindStringSubmatch(lines[1])
if len(remoteMatch) != 2 {
return refResolver{}, false
}
repository := Repository(remoteMatch[1])
branch := "main"
if repository == RepositoryNezha {
branch = "master"
}
expectedLines := []string{
lines[0],
lines[1],
"mapfile -t refs < <(git ls-remote \"$remote\" refs/heads/" + branch + ")",
"(( ${#refs[@]} == 1 ))",
"sha=${refs[0]%%$'\\t'*}",
`[[ "$sha" =~ ^[0-9a-f]{40}$ ]]`,
`printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT"`,
}
for index, expected := range expectedLines {
if lines[index] != expected {
return refResolver{}, false
}
}
return refResolver{id: id, repository: repository}, true
}
@@ -10,7 +10,7 @@ import (
const agentcompatStressTestName = "TestStressPRFullEightAgentExactlyOnce"
func TestPolicy_NezhaStressWorkflowRunsPinnedCrossRepositoryTest(t *testing.T) {
func TestPolicy_NezhaStressWorkflowRunsCrossRepositoryTest(t *testing.T) {
// Given
data := readNezhaQualityWorkflow(t)
var workflow qualityWorkflow
@@ -27,7 +27,7 @@ func TestPolicy_NezhaStressWorkflowRunsPinnedCrossRepositoryTest(t *testing.T) {
require.Len(t, stressJob.Steps, 6)
require.Equal(t, []string{
"Checkout Nezha revision",
"Checkout pinned Agent revision",
"Checkout Agent repository",
"Set up Go",
"Prepare Dashboard build inputs",
"Require named stress test",
@@ -42,21 +42,21 @@ func TestPolicy_NezhaStressWorkflowRunsPinnedCrossRepositoryTest(t *testing.T) {
})
nezhaCheckout := stressJob.stepNamed(t, "Checkout Nezha revision")
require.Equal(t, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", nezhaCheckout.Uses)
require.Equal(t, "actions/checkout@v7.0.1", nezhaCheckout.Uses)
require.Empty(t, nezhaCheckout.With.Repository)
require.Empty(t, nezhaCheckout.With.Ref)
require.Equal(t, "nezha", nezhaCheckout.With.Path)
require.False(t, *nezhaCheckout.With.PersistCredentials)
agentCheckout := stressJob.stepNamed(t, "Checkout pinned Agent revision")
require.Equal(t, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", agentCheckout.Uses)
agentCheckout := stressJob.stepNamed(t, "Checkout Agent repository")
require.Equal(t, "actions/checkout@v7.0.1", agentCheckout.Uses)
require.Equal(t, "nezhahq/agent", agentCheckout.With.Repository)
require.Equal(t, "667e1dd5e166ffef808ec26dc20de85bc33a0a0f", agentCheckout.With.Ref)
require.Empty(t, agentCheckout.With.Ref)
require.Equal(t, "agent", agentCheckout.With.Path)
require.False(t, *agentCheckout.With.PersistCredentials)
setupGo := stressJob.stepNamed(t, "Set up Go")
require.Equal(t, "actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16", setupGo.Uses)
require.Equal(t, "actions/setup-go@v7", setupGo.Uses)
require.Equal(t, "1.26.x", setupGo.With.GoVersion)
require.False(t, *setupGo.With.Cache)
@@ -8,6 +8,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/checkout
with:
persist-credentials: false
@@ -9,6 +9,6 @@ jobs:
timeout-minutes: 10
steps:
- run: go test ./...
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v6
with:
path: ${{ runner.temp }}/results
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/cache@0123456789abcdef0123456789abcdef01234567
- uses: actions/cache@v4
with:
path: bin
key: executable-cache
@@ -12,7 +12,7 @@ jobs:
id: redact-evidence
if: false
run: go run ./integration/agentcompat/cmd/redact --output "$RUNNER_TEMP/nezha-agentcompat-redacted"
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v6
if: always()
with:
path: ${{ runner.temp }}/nezha-agentcompat-redacted
@@ -8,8 +8,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
repository: nezhahq/agent
ref: main
persist-credentials: false
@@ -8,4 +8,4 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/download-artifact@0123456789abcdef0123456789abcdef01234567
- uses: actions/download-artifact@v7
@@ -8,4 +8,4 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
@@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
- uses: actions/setup-go@v7
with:
go-version: "1.26.x"
cache: false
@@ -25,10 +25,10 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
- uses: actions/setup-go@v7
with:
go-version: "1.26.x"
cache: false
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Redact evidence
run: go run ./integration/agentcompat/cmd/redact
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v6
with:
path: |
${{ runner.temp }}/redacted-results
@@ -10,6 +10,6 @@ jobs:
steps:
- name: Redact evidence
run: "true"
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v6
with:
path: ${{ runner.temp }}/redacted-results
@@ -8,8 +8,8 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
repository: ${{ inputs.repository }}
ref: 0123456789abcdef0123456789abcdef01234567
ref: main
persist-credentials: false
@@ -9,4 +9,4 @@ jobs:
timeout-minutes: 10
steps:
- uses:
- actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- actions/checkout@v7.0.1
@@ -8,6 +8,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10.5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
@@ -8,6 +8,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
persist-credentials: true
@@ -8,6 +8,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
persist-credentials: "false"
@@ -13,7 +13,7 @@ jobs:
if: always()
run: go run ./integration/agentcompat/cmd/redact --output "$RUNNER_TEMP/nezha-agentcompat-redacted"
- run: cp raw-secret "$RUNNER_TEMP/nezha-agentcompat-redacted/raw-secret"
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v6
if: always()
with:
path: ${{ runner.temp }}/nezha-agentcompat-redacted
@@ -14,7 +14,7 @@ jobs:
run: |
go run ./integration/agentcompat/cmd/redact --output "$RUNNER_TEMP/nezha-agentcompat-redacted"
cp raw-secret "$RUNNER_TEMP/nezha-agentcompat-redacted/raw-secret"
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v6
if: always()
with:
path: ${{ runner.temp }}/nezha-agentcompat-redacted
@@ -1,25 +0,0 @@
on:
pull_request:
concurrency: policy
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- id: resolve-agent
run: |
set -euo pipefail
remote='https://github.com/nezhahq/agent.git'
mapfile -t refs < <(git ls-remote "$remote" refs/heads/main)
(( ${#refs[@]} == 1 ))
sha=${refs[0]%%$'\t'*}
[[ "$sha" =~ ^[0-9a-f]{40}$ ]]
sha=attacker-controlled
printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
repository: nezhahq/agent
ref: ${{ steps.resolve-agent.outputs.sha }}
persist-credentials: false
@@ -9,13 +9,13 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
repository: nezhahq/nezha
ref: fedcba9876543210fedcba9876543210fedcba98
ref: master
path: nezha
persist-credentials: false
- run: go test ./integration/agentcompat/...
@@ -15,14 +15,14 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout Nezha
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Checkout Agent
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@v7.0.1
with:
repository: nezhahq/agent
ref: 0123456789abcdef0123456789abcdef01234567
ref: v1.2.3
path: agent
persist-credentials: false
- name: Test
@@ -34,7 +34,7 @@ jobs:
if: always()
run: go run ./integration/agentcompat/cmd/redact --output "$RUNNER_TEMP/nezha-agentcompat-redacted"
- name: Upload redacted evidence
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
uses: actions/upload-artifact@v6
if: always()
with:
path: ${{ runner.temp }}/nezha-agentcompat-redacted
@@ -1,27 +0,0 @@
name: Secure resolved ref
on:
pull_request:
concurrency: resolved-${{ github.ref }}
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- id: resolve-agent
name: Resolve Agent commit
shell: bash
run: |
set -euo pipefail
remote='https://github.com/nezhahq/agent.git'
mapfile -t refs < <(git ls-remote "$remote" refs/heads/main)
(( ${#refs[@]} == 1 ))
sha=${refs[0]%%$'\t'*}
[[ "$sha" =~ ^[0-9a-f]{40}$ ]]
printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
repository: nezhahq/agent
ref: ${{ steps.resolve-agent.outputs.sha }}
persist-credentials: false
@@ -8,6 +8,6 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
- uses: actions/setup-go@v7
with:
go-version: 1.26.3
@@ -8,4 +8,4 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: attacker/exfiltrate@0123456789abcdef0123456789abcdef01234567
- uses: attacker/exfiltrate@v1
@@ -8,8 +8,8 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@v7.0.1
with:
repository: attacker/fork
ref: 0123456789abcdef0123456789abcdef01234567
ref: main
persist-credentials: false
@@ -11,6 +11,6 @@ jobs:
- name: Redact evidence
id: redact-evidence
run: go run ./integration/agentcompat/cmd/redact
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v6
with:
path: ${{ runner.temp }}/results
@@ -1,18 +0,0 @@
on:
pull_request:
concurrency: policy
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- id: resolve-agent
run: |
echo "sha=$(git ls-remote https://github.com/nezhahq/agent.git refs/heads/main | cut -f1)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
repository: nezhahq/agent
ref: ${{ steps.resolve-agent.outputs.sha }}
persist-credentials: false
@@ -24,7 +24,6 @@ const (
RuleContainerizedExecution Rule = "containerized-execution"
RuleRepositoryNotAllowed Rule = "repository-not-allowed"
RuleRepositoryNotLiteral Rule = "repository-not-literal"
RuleOtherRepositoryRef Rule = "other-repository-ref"
RulePersistCredentials Rule = "persist-credentials" // #nosec G101 -- GitHub Actions configuration key, not a credential.
RuleReusableExecutable Rule = "reusable-executable"
RuleContinueOnError Rule = "continue-on-error"