mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-20 18:20: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,197 @@
|
||||
package workflowpolicy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
secretContextPattern = regexp.MustCompile(`(?i)\bsecrets\b`)
|
||||
tokenSourcePattern = regexp.MustCompile(`(?i)\bgithub\s*\.\s*token\b|\bGITHUB_TOKEN\b`)
|
||||
untrustedExpressionPattern = regexp.MustCompile(`\$\{\{[^}]*(?:github\s*\.\s*event\s*\.|github\s*\.\s*head_ref|github\s*\[)[^}]*\}\}`)
|
||||
)
|
||||
|
||||
type checker struct {
|
||||
repository Repository
|
||||
violations []Violation
|
||||
}
|
||||
|
||||
func Verify(data []byte, repository Repository) error {
|
||||
return verify("memory", data, repository)
|
||||
}
|
||||
|
||||
func VerifyFile(path string, repository Repository) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return &ReadError{Path: path, Cause: err}
|
||||
}
|
||||
return verify(path, data, repository)
|
||||
}
|
||||
|
||||
func verify(source string, data []byte, repository Repository) error {
|
||||
root, err := parseWorkflow(source, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policyChecker := checker{repository: repository}
|
||||
policyChecker.checkWorkflow(root)
|
||||
if len(policyChecker.violations) > 0 {
|
||||
return &PolicyError{violations: policyChecker.violations}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *checker) checkWorkflow(root *yaml.Node) {
|
||||
if c.repository != RepositoryAgent && c.repository != RepositoryNezha {
|
||||
c.reject(RuleRepositoryNotAllowed, at("$", root), fmt.Sprintf("current repository %q is not supported", c.repository))
|
||||
}
|
||||
c.checkTriggers(root)
|
||||
c.checkSecretContexts(root)
|
||||
c.checkUntrustedExpressions(root)
|
||||
c.checkForbiddenEnvironment(root)
|
||||
c.checkPermissions(root, "$.permissions", true)
|
||||
concurrency, exists := mappingValue(root, "concurrency")
|
||||
if !exists || !validConcurrency(concurrency) {
|
||||
node := root
|
||||
if exists {
|
||||
node = concurrency
|
||||
}
|
||||
detail := "workflow concurrency requires a nonempty group"
|
||||
if exists {
|
||||
if _, hasGroup := mappingValue(concurrency, "group"); hasGroup {
|
||||
cancel, hasCancel := mappingValue(concurrency, "cancel-in-progress")
|
||||
if hasCancel && (cancel.Kind != yaml.ScalarNode || cancel.Tag != "!!bool") {
|
||||
detail = "workflow concurrency cancel-in-progress must be a boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
c.reject(RuleMissingConcurrency, at("$.concurrency", node), detail)
|
||||
}
|
||||
jobs, exists := mappingValue(root, "jobs")
|
||||
if !exists || jobs.Kind != yaml.MappingNode || len(jobs.Content) == 0 {
|
||||
node := root
|
||||
if exists {
|
||||
node = jobs
|
||||
}
|
||||
c.reject(RuleWorkflowStructure, at("$.jobs", node), "workflow jobs must be a nonempty mapping")
|
||||
return
|
||||
}
|
||||
for _, entry := range mappingEntries(jobs) {
|
||||
if entry[1].Kind != yaml.MappingNode {
|
||||
c.reject(RuleWorkflowStructure, at("$.jobs."+entry[0].Value, entry[1]), "workflow job must be a mapping")
|
||||
continue
|
||||
}
|
||||
c.checkJob(entry[0].Value, entry[1])
|
||||
}
|
||||
c.checkRequiredAggregator(jobs)
|
||||
}
|
||||
|
||||
func (c *checker) checkForbiddenEnvironment(root *yaml.Node) {
|
||||
walkMappings(root, func(mapping *yaml.Node) {
|
||||
environment, exists := mappingValue(mapping, "env")
|
||||
if !exists || environment.Kind != yaml.MappingNode {
|
||||
return
|
||||
}
|
||||
for _, entry := range mappingEntries(environment) {
|
||||
if strings.HasPrefix(strings.ToUpper(entry[0].Value), "GIT_") {
|
||||
c.reject(RuleRepositoryNotLiteral, at("$.env."+entry[0].Value, entry[0]), fmt.Sprintf("Git configuration environment %s is forbidden", entry[0].Value))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func validConcurrency(node *yaml.Node) bool {
|
||||
if value, literal := scalarString(node); literal {
|
||||
return strings.TrimSpace(value) != ""
|
||||
}
|
||||
group, exists := mappingValue(node, "group")
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
value, literal := scalarString(group)
|
||||
if !literal || strings.TrimSpace(value) == "" {
|
||||
return false
|
||||
}
|
||||
cancel, exists := mappingValue(node, "cancel-in-progress")
|
||||
return !exists || (cancel.Kind == yaml.ScalarNode && cancel.Tag == "!!bool")
|
||||
}
|
||||
|
||||
func (c *checker) checkTriggers(root *yaml.Node) {
|
||||
trigger, exists := mappingValue(root, "on")
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
for _, forbidden := range []string{"pull_request_target", "workflow_run"} {
|
||||
if containsScalar(trigger, forbidden) {
|
||||
c.reject(RulePrivilegedTrigger, at("$.on."+forbidden, trigger), fmt.Sprintf("privileged trigger %s is forbidden", forbidden))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *checker) checkSecretContexts(root *yaml.Node) {
|
||||
walkScalars(root, func(node *yaml.Node) {
|
||||
if strings.Contains(node.Value, "${{") && (secretContextPattern.MatchString(node.Value) || tokenSourcePattern.MatchString(node.Value)) {
|
||||
detail := "secrets context is forbidden"
|
||||
if tokenSourcePattern.MatchString(node.Value) {
|
||||
detail = "github.token secret source is forbidden"
|
||||
}
|
||||
c.reject(RuleSecretContext, at("$", node), detail)
|
||||
}
|
||||
})
|
||||
walkMappings(root, func(mapping *yaml.Node) {
|
||||
for _, entry := range mappingEntries(mapping) {
|
||||
if strings.EqualFold(entry[0].Value, "GITHUB_TOKEN") {
|
||||
c.reject(RuleSecretContext, at("$.env.GITHUB_TOKEN", entry[0]), "GITHUB_TOKEN secret source is forbidden")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *checker) checkUntrustedExpressions(root *yaml.Node) {
|
||||
walkScalars(root, func(node *yaml.Node) {
|
||||
if expression := untrustedExpressionPattern.FindString(node.Value); expression != "" {
|
||||
c.reject(RuleUntrustedExpression, at("$", node), fmt.Sprintf("untrusted github event expression %s is forbidden", expression))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *checker) checkPermissions(mapping *yaml.Node, path string, required bool) {
|
||||
permissions, exists := mappingValue(mapping, "permissions")
|
||||
if !exists {
|
||||
if required {
|
||||
c.reject(RuleWritePermission, at(path, mapping), "root permissions must be explicitly read-only")
|
||||
}
|
||||
return
|
||||
}
|
||||
if permissions.Kind == yaml.ScalarNode {
|
||||
if permissions.Value != "read-all" {
|
||||
c.reject(RuleWritePermission, at(path, permissions), fmt.Sprintf("permissions must be read-only, got %q", permissions.Value))
|
||||
}
|
||||
return
|
||||
}
|
||||
if permissions.Kind != yaml.MappingNode {
|
||||
c.reject(RuleWritePermission, at(path, permissions), "permissions must be read-all or a read-only mapping")
|
||||
return
|
||||
}
|
||||
for _, entry := range mappingEntries(permissions) {
|
||||
if entry[1].Kind != yaml.ScalarNode {
|
||||
c.reject(RuleWritePermission, at(path+"."+entry[0].Value, entry[1]), "permission value must be a scalar read or none")
|
||||
continue
|
||||
}
|
||||
value := strings.ToLower(strings.TrimSpace(entry[1].Value))
|
||||
if value != "read" && value != "none" {
|
||||
c.reject(RuleWritePermission, at(path+"."+entry[0].Value, entry[1]), fmt.Sprintf("permission %s must be read or none, got %q", entry[0].Value, entry[1].Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *checker) reject(rule Rule, location violationLocation, detail string) {
|
||||
c.violations = append(c.violations, Violation{
|
||||
Rule: rule, Path: location.path, Line: location.node.Line, Column: location.node.Column,
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user