mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
ci: require agentcompat stress validation
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
package workflowpolicy_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/workflowpolicy"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestPolicy_AcceptsSecureNezhaWorkflow(t *testing.T) {
|
||||
// Given
|
||||
path := fixturePath(t, "secure-nezha.yml")
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryNezha)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPolicy_AcceptsSecureAgentWorkflow(t *testing.T) {
|
||||
// Given
|
||||
path := fixturePath(t, "secure-agent.yml")
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryAgent)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPolicy_AcceptsValidatedResolvedOtherRepositoryRef(t *testing.T) {
|
||||
// Given
|
||||
path := fixturePath(t, "secure-resolved-ref.yml")
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryNezha)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsPullRequestTarget(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("pull-request-target.yml", workflowpolicy.RulePrivilegedTrigger, "pull_request_target"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsPrivilegedWorkflowRun(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("workflow-run.yml", workflowpolicy.RulePrivilegedTrigger, "workflow_run"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsSecretContext(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("secret-context.yml", workflowpolicy.RuleSecretContext, "secrets"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsAggregateSecretContext(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("aggregate-secret-context.yml", workflowpolicy.RuleSecretContext, "secrets"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsWritePermission(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("write-permission.yml", workflowpolicy.RuleWritePermission, "contents"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsIDTokenPermission(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("id-token-write.yml", workflowpolicy.RuleWritePermission, "id-token"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsMissingRootPermissions(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("missing-root-permissions.yml", workflowpolicy.RuleWritePermission, "root permissions"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsPermissionsSequence(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("permissions-sequence.yml", workflowpolicy.RuleWritePermission, "mapping"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsQuotedFalsePersistCredentials(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("quoted-false-security-controls.yml", workflowpolicy.RulePersistCredentials, "boolean"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsFractionalTimeout(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("numeric-timeout.yml", workflowpolicy.RuleMissingJobTimeout, "positive literal"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsNonScalarPermissionValue(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("mapping-permission-value.yml", workflowpolicy.RuleWritePermission, "read or none"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsNonMappingUsesStep(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("nonmapping-uses.yml", workflowpolicy.RuleWorkflowStructure, "string action reference"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsNonStringUsesReference(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("boolean-uses.yml", workflowpolicy.RuleWorkflowStructure, "string action reference"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsMutableRepositoryInput(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("unapproved-repository.yml", workflowpolicy.RuleRepositoryNotAllowed, "attacker/fork"))
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsPersistCredentialsTrue(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("persist-credentials-true.yml", workflowpolicy.RulePersistCredentials, "false"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsMalformedYAML(t *testing.T) {
|
||||
// Given
|
||||
path := fixturePath(t, "malformed.yml")
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryNezha)
|
||||
|
||||
// Then
|
||||
var parseError *workflowpolicy.ParseError
|
||||
require.ErrorAs(t, err, &parseError)
|
||||
require.Contains(t, err.Error(), "parse workflow")
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsDuplicateKeys(t *testing.T) {
|
||||
// Given
|
||||
path := fixturePath(t, "duplicate-jobs.yml")
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryNezha)
|
||||
|
||||
// Then
|
||||
var parseError *workflowpolicy.ParseError
|
||||
require.ErrorAs(t, err, &parseError)
|
||||
require.Contains(t, err.Error(), "duplicate key")
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsYAMLAlias(t *testing.T) {
|
||||
// Given
|
||||
path := fixturePath(t, "yaml-alias.yml")
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryNezha)
|
||||
|
||||
// Then
|
||||
var parseError *workflowpolicy.ParseError
|
||||
require.ErrorAs(t, err, &parseError)
|
||||
require.Contains(t, err.Error(), "aliases are not allowed")
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsTrailingEmptyYAMLDocument(t *testing.T) {
|
||||
assertFixtureParseRejected(t, "trailing-empty-document.yml", "multiple YAML documents")
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsBareYAMLAnchor(t *testing.T) {
|
||||
assertFixtureParseRejected(t, "bare-anchor.yml", "anchors are not allowed")
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsMalformedStepValues(t *testing.T) {
|
||||
for _, fixture := range []string{"empty-step.yml", "empty-run.yml", "nonstring-run.yml", "empty-uses.yml"} {
|
||||
t.Run(fixture, func(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected(fixture, workflowpolicy.RuleWorkflowStructure, "step"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsSecretTokenSources(t *testing.T) {
|
||||
for _, fixture := range []string{"github-token.yml", "github-token-environment.yml"} {
|
||||
t.Run(fixture, func(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected(fixture, workflowpolicy.RuleSecretContext, "token"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsNonBooleanConcurrencyCancellation(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("nonboolean-concurrency-cancel.yml", workflowpolicy.RuleMissingConcurrency, "boolean"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsMissingJobs(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("missing-jobs.yml", workflowpolicy.RuleWorkflowStructure, "nonempty mapping"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsNonmappingStep(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("nonmapping-step.yml", workflowpolicy.RuleWorkflowStructure, "step must be a mapping"))
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsNonsequenceSteps(t *testing.T) {
|
||||
assertFixtureRejected(t, rejected("nonsequence-steps.yml", workflowpolicy.RuleWorkflowStructure, "steps must be a sequence"))
|
||||
}
|
||||
|
||||
func TestPolicy_MissingFutureWorkflowReturnsTypedReadError(t *testing.T) {
|
||||
// Given
|
||||
path := filepath.Join(t.TempDir(), "agent-compatibility.yml")
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, workflowpolicy.RepositoryNezha)
|
||||
|
||||
// Then
|
||||
var readError *workflowpolicy.ReadError
|
||||
require.ErrorAs(t, err, &readError)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
func TestPolicy_RejectsUnsupportedRepositoryBeforeWorkflowChecks(t *testing.T) {
|
||||
// Given
|
||||
data, err := os.ReadFile(fixturePath(t, "secure-nezha.yml"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// When
|
||||
err = workflowpolicy.Verify(data, workflowpolicy.Repository("attacker/fork"))
|
||||
|
||||
// Then
|
||||
var policyError *workflowpolicy.PolicyError
|
||||
require.ErrorAs(t, err, &policyError)
|
||||
require.True(t, policyError.Has(workflowpolicy.RuleRepositoryNotAllowed))
|
||||
}
|
||||
|
||||
type rejectionExpectation struct {
|
||||
fixture string
|
||||
repository workflowpolicy.Repository
|
||||
rule workflowpolicy.Rule
|
||||
diagnostic string
|
||||
}
|
||||
|
||||
func rejected(fixture string, rule workflowpolicy.Rule, diagnostic string) rejectionExpectation {
|
||||
return rejectionExpectation{fixture: fixture, repository: workflowpolicy.RepositoryNezha, rule: rule, diagnostic: diagnostic}
|
||||
}
|
||||
|
||||
func assertFixtureRejected(t *testing.T, expectation rejectionExpectation) {
|
||||
t.Helper()
|
||||
|
||||
// Given
|
||||
path := fixturePath(t, expectation.fixture)
|
||||
|
||||
// When
|
||||
err := workflowpolicy.VerifyFile(path, expectation.repository)
|
||||
|
||||
// Then
|
||||
var policyError *workflowpolicy.PolicyError
|
||||
require.ErrorAs(t, err, &policyError)
|
||||
require.True(t, policyError.Has(expectation.rule), "diagnostic: %v", err)
|
||||
require.Contains(t, err.Error(), expectation.diagnostic)
|
||||
}
|
||||
|
||||
func assertFixtureParseRejected(t *testing.T, fixture string, diagnostic string) {
|
||||
t.Helper()
|
||||
err := workflowpolicy.VerifyFile(fixturePath(t, fixture), workflowpolicy.RepositoryNezha)
|
||||
var parseError *workflowpolicy.ParseError
|
||||
require.ErrorAs(t, err, &parseError)
|
||||
require.Contains(t, err.Error(), diagnostic)
|
||||
}
|
||||
|
||||
func fixturePath(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
return filepath.Join("testdata", name)
|
||||
}
|
||||
|
||||
func mappingNodeValue(node *yaml.Node, key string) *yaml.Node {
|
||||
for index := 0; index < len(node.Content); index += 2 {
|
||||
if node.Content[index].Value == key {
|
||||
return node.Content[index+1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scalarValues(node *yaml.Node) []string {
|
||||
values := make([]string, 0, len(node.Content))
|
||||
for _, child := range node.Content {
|
||||
values = append(values, child.Value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func requireTypedPolicyError(t *testing.T, err error, rule workflowpolicy.Rule) *workflowpolicy.PolicyError {
|
||||
t.Helper()
|
||||
var policyError *workflowpolicy.PolicyError
|
||||
require.True(t, errors.As(err, &policyError), "expected typed policy error, got %T: %v", err, err)
|
||||
require.True(t, policyError.Has(rule), "diagnostic: %v", err)
|
||||
return policyError
|
||||
}
|
||||
Reference in New Issue
Block a user