mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50: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,207 @@
|
||||
package workflowpolicy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
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*(?:;|$)`)
|
||||
workspaceCommandPattern = regexp.MustCompile(`(?mi)(?:\$\{\{\s*github\.workspace\s*\}\}|\$GITHUB_WORKSPACE|\$\{GITHUB_WORKSPACE\})(?:/|\\)|(?:^|[;&|]\s*)(?:sudo\s+)?(?:\.\.?/|[A-Za-z0-9_.-]+/)[^\s;&|]+`)
|
||||
gitRepositoryCommand = regexp.MustCompile(`(?m)(?:^|[;&|]\s*|\s)(?:(?:sudo|command|env)\s+)?(?:/usr/bin/)?git\b[^\n]*(?:clone|ls-remote)\b`)
|
||||
gitConfigurationPattern = regexp.MustCompile(`(?mi)(?:^|[;&|]\s*)(?:sudo\s+)?git(?:\s+-c\s+url\.[^\s]+\.insteadOf=\S+|\s+config\b)`)
|
||||
)
|
||||
|
||||
func (c *checker) checkJob(name string, job *yaml.Node) {
|
||||
path := "$.jobs." + name
|
||||
timeout, exists := mappingValue(job, "timeout-minutes")
|
||||
if !exists || !positiveInteger(timeout) {
|
||||
node := job
|
||||
if exists {
|
||||
node = timeout
|
||||
}
|
||||
c.reject(RuleMissingJobTimeout, at(path+".timeout-minutes", node), "job timeout-minutes must be a positive literal")
|
||||
}
|
||||
c.checkRunner(path, job)
|
||||
if container, exists := mappingValue(job, "container"); exists {
|
||||
c.reject(RuleContainerizedExecution, at(path+".container", container), "job containers are forbidden")
|
||||
}
|
||||
if services, exists := mappingValue(job, "services"); exists {
|
||||
c.reject(RuleContainerizedExecution, at(path+".services", services), "service containers are forbidden")
|
||||
}
|
||||
c.checkPermissions(job, path+".permissions", false)
|
||||
c.checkContinueOnError(job, path)
|
||||
if reusableWorkflow, exists := mappingValue(job, "uses"); exists {
|
||||
c.reject(RuleReusableExecutable, at(path+".uses", reusableWorkflow), "job-level reusable workflows are forbidden")
|
||||
return
|
||||
}
|
||||
steps, exists := mappingValue(job, "steps")
|
||||
if !exists {
|
||||
c.reject(RuleWorkflowStructure, at(path+".steps", job), "workflow jobs must define steps")
|
||||
return
|
||||
}
|
||||
if steps.Kind != yaml.SequenceNode {
|
||||
c.reject(RuleWorkflowStructure, at(path+".steps", steps), "workflow steps must be a sequence")
|
||||
return
|
||||
}
|
||||
c.checkSteps(path, steps)
|
||||
}
|
||||
|
||||
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 {
|
||||
c.reject(RuleWorkflowStructure, at(path, step), "workflow step must be a mapping")
|
||||
redactionReady = false
|
||||
continue
|
||||
}
|
||||
uses, hasUses := mappingValue(step, "uses")
|
||||
run, hasRun := mappingValue(step, "run")
|
||||
if !hasUses && !hasRun {
|
||||
c.reject(RuleWorkflowStructure, at(path, step), "workflow step must define a nonempty uses or run")
|
||||
redactionReady = false
|
||||
continue
|
||||
}
|
||||
if hasUses && (uses.Kind != yaml.ScalarNode || uses.Tag != "!!str" || strings.TrimSpace(uses.Value) == "") {
|
||||
c.reject(RuleWorkflowStructure, at(path+".uses", uses), "step uses must be a string action reference")
|
||||
}
|
||||
if hasRun && (run.Kind != yaml.ScalarNode || run.Tag != "!!str" || strings.TrimSpace(run.Value) == "") {
|
||||
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
|
||||
}
|
||||
if hasUses {
|
||||
c.checkUses(path, step, stepCheckState{redactionComplete: redactionReady, validatedResolvers: validatedResolvers})
|
||||
redactionReady = false
|
||||
continue
|
||||
}
|
||||
redactionReady = c.isRedactionStep(step)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *checker) checkContinueOnError(mapping *yaml.Node, path string) {
|
||||
value, exists := mappingValue(mapping, "continue-on-error")
|
||||
if exists && !explicitFalse(value) {
|
||||
c.reject(RuleContinueOnError, at(path+".continue-on-error", value), "continue-on-error must not enable failure suppression")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *checker) checkRun(path string, run *yaml.Node, validatedResolver bool) {
|
||||
command, exists := scalarString(run)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
if dockerCommandPattern.MatchString(command) {
|
||||
c.reject(RuleContainerizedExecution, at(path, run), "docker execution is forbidden")
|
||||
}
|
||||
if swallowedFailurePattern.MatchString(command) {
|
||||
c.reject(RuleSwallowedFailure, at(path, run), "shell failure is swallowed by || true or another ignored fallback, exit 0, or disabled errexit")
|
||||
}
|
||||
if workspaceCommandPattern.MatchString(command) {
|
||||
c.reject(RuleReusableExecutable, at(path, run), "executing a binary from the GitHub workspace is forbidden")
|
||||
}
|
||||
if gitHubEnvironmentPattern.MatchString(command) {
|
||||
c.reject(RuleRepositoryNotLiteral, at(path, run), "writing GIT_* configuration through GITHUB_ENV is forbidden")
|
||||
}
|
||||
if gitConfigurationPattern.MatchString(command) {
|
||||
c.reject(RuleRepositoryNotLiteral, at(path, run), "Git configuration mutation is forbidden")
|
||||
}
|
||||
if gitRepositoryCommand.MatchString(command) && !validatedResolver {
|
||||
rule := RuleRepositoryNotLiteral
|
||||
detail := fmt.Sprintf("git repository operation %q is allowed only in the validated resolver", strings.TrimSpace(command))
|
||||
if !strings.Contains(command, "$") {
|
||||
rule = RuleRepositoryNotAllowed
|
||||
detail = fmt.Sprintf("repository operation %q is forbidden outside the validated resolver", strings.TrimSpace(command))
|
||||
}
|
||||
c.reject(rule, at(path, run), detail)
|
||||
}
|
||||
}
|
||||
|
||||
type stepCheckState struct {
|
||||
redactionComplete bool
|
||||
validatedResolvers map[string]Repository
|
||||
}
|
||||
|
||||
func (c *checker) checkUses(path string, step *yaml.Node, state stepCheckState) {
|
||||
uses, exists := mappingValue(step, "uses")
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
action, literal := scalarString(uses)
|
||||
if !literal {
|
||||
return
|
||||
}
|
||||
if strings.Contains(action, "${{") {
|
||||
c.reject(RuleRepositoryNotLiteral, at(path+".uses", uses), "action reference must be literal")
|
||||
return
|
||||
}
|
||||
lowerAction := strings.ToLower(action)
|
||||
if strings.HasPrefix(lowerAction, "docker://") {
|
||||
c.reject(RuleContainerizedExecution, at(path+".uses", uses), "Docker actions are forbidden")
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(lowerAction, "./") {
|
||||
c.reject(RuleReusableExecutable, at(path+".uses", uses), "local action reuse from the workspace is forbidden")
|
||||
return
|
||||
}
|
||||
actionRepository, _, found := strings.Cut(lowerAction, "@")
|
||||
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 == "" {
|
||||
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 {
|
||||
case "actions/checkout":
|
||||
c.checkCheckout(path, step, state.validatedResolvers)
|
||||
case "actions/setup-go":
|
||||
c.checkRequiredCacheDisabled(path, step)
|
||||
case "actions/upload-artifact":
|
||||
c.checkArtifactUpload(path, step, state.redactionComplete)
|
||||
}
|
||||
c.checkCacheInputs(path, step)
|
||||
}
|
||||
|
||||
func approvedAction(action string) (string, string, bool) {
|
||||
repository, ref, found := strings.Cut(strings.ToLower(action), "@")
|
||||
if !found {
|
||||
return repository, "", false
|
||||
}
|
||||
approvedRefs := map[string]string{
|
||||
"actions/checkout": "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
|
||||
"actions/setup-go": "924ae3a1cded613372ab5595356fb5720e22ba16",
|
||||
"actions/upload-artifact": "b7c566a772e6b6bfb58ed0dc250532a479d7789f",
|
||||
}
|
||||
approvedRef, approved := approvedRefs[repository]
|
||||
if !approved {
|
||||
return "", ref, false
|
||||
}
|
||||
return repository, ref, ref == approvedRef
|
||||
}
|
||||
Reference in New Issue
Block a user