-
Notifications
You must be signed in to change notification settings - Fork 359
Expand file tree
/
Copy pathexpression_patterns.go
More file actions
191 lines (163 loc) · 7.78 KB
/
expression_patterns.go
File metadata and controls
191 lines (163 loc) · 7.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// This file provides centralized regex patterns for GitHub Actions expression matching.
//
// # Expression Patterns
//
// This file consolidates regular expression patterns used across multiple validation and
// extraction files to provide a single source of truth for expression matching logic.
//
// # Available Pattern Categories
//
// ## Core Expression Patterns
// - ExpressionPattern - Matches GitHub Actions expressions: ${{ ... }}
// - ExpressionPatternDotAll - Matches expressions with dotall mode (multiline)
//
// ## Context Access Patterns
// - NeedsStepsPattern - Matches needs.* and steps.* patterns
// - InputsPattern - Matches github.event.inputs.* patterns
// - WorkflowCallInputsPattern - Matches inputs.* patterns (workflow_call)
// - AWInputsPattern - Matches github.aw.inputs.* patterns
// - EnvPattern - Matches env.* patterns
//
// ## Secret Patterns
// - SecretExpressionPattern - Matches ${{ secrets.SECRET_NAME }} expressions
// - SecretsExpressionPattern - Validates secrets expression syntax
//
// ## Template Patterns
// - InlineExpressionPattern - Matches inline expressions in templates
// - UnsafeContextPattern - Matches potentially unsafe context patterns
// - TemplateIfPattern - Matches {{#if ...}} template conditionals
//
// ## Utility Patterns
// - ComparisonExtractionPattern - Extracts properties from comparison expressions
// - StringLiteralPattern - Matches string literals ('...', "...", `...`)
// - NumberLiteralPattern - Matches numeric literals
// - RangePattern - Matches numeric ranges (e.g., "1-10")
//
// # Design Rationale
//
// Centralizing regex patterns provides several benefits:
// - Single source of truth for expression matching logic
// - Consistent behavior across validation and extraction
// - Easier to maintain and update patterns
// - Better performance through pre-compilation
// - Reduced code duplication across files
//
// # Migration Notes
//
// This file consolidates patterns previously scattered across:
// - expression_validation.go
// - expression_extraction.go
// - secret_extraction.go
// - secrets_validation.go
// - template.go
// - template_injection_validation.go
//
// Files are gradually being migrated to use these centralized patterns.
package workflow
import (
"regexp"
"strings"
"github.com/github/gh-aw/pkg/logger"
)
// hasExpressionMarker reports whether s contains a GitHub Actions expression opening marker.
// This is a permissive check used in scenarios where partial expressions should be treated
// as dynamic values.
func hasExpressionMarker(s string) bool {
return strings.Contains(s, "${{")
}
// containsExpression reports whether s contains a complete non-empty GitHub Actions expression.
// A complete expression has a "${{" marker that appears before a closing "}}" marker
// with at least one character between them.
func containsExpression(s string) bool {
_, afterOpen, found := strings.Cut(s, "${{")
if !found {
return false
}
closeIdx := strings.Index(afterOpen, "}}")
return closeIdx > 0
}
// isExpression reports whether the entire string s is a GitHub Actions expression.
func isExpression(s string) bool {
return strings.HasPrefix(s, "${{") && strings.HasSuffix(s, "}}")
}
var expressionPatternsLog = logger.New("workflow:expression_patterns")
func init() {
expressionPatternsLog.Print("Initializing expression pattern regex compilation")
}
// Core Expression Patterns
var (
// ExpressionPattern matches GitHub Actions expressions: ${{ ... }}
// Uses non-greedy matching to handle nested braces properly
ExpressionPattern = regexp.MustCompile(`\$\{\{(.*?)\}\}`)
// ExpressionPatternDotAll matches expressions with dotall mode enabled
// The (?s) flag enables dotall mode where . matches newlines
ExpressionPatternDotAll = regexp.MustCompile(`(?s)\$\{\{(.*?)\}\}`)
)
// Context Access Patterns
var (
// NeedsStepsPattern matches needs.* and steps.* context patterns
// Example: needs.build.outputs.version, steps.setup.outputs.path
NeedsStepsPattern = regexp.MustCompile(`^(needs|steps)\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$`)
// InputsPattern matches github.event.inputs.* patterns
// Example: github.event.inputs.workflow_id
InputsPattern = regexp.MustCompile(`^github\.event\.inputs\.[a-zA-Z0-9_-]+$`)
// WorkflowCallInputsPattern matches inputs.* patterns for workflow_call
// Example: inputs.branch_name
WorkflowCallInputsPattern = regexp.MustCompile(`^inputs\.[a-zA-Z0-9_-]+$`)
// AWInputsPattern matches github.aw.inputs.* patterns
// Example: github.aw.inputs.custom_param
AWInputsPattern = regexp.MustCompile(`^github\.aw\.inputs\.[a-zA-Z0-9_-]+$`)
// AWInputsExpressionPattern matches full ${{ github.aw.inputs.* }} expressions
// Used for extraction rather than validation
AWInputsExpressionPattern = regexp.MustCompile(`\$\{\{\s*github\.aw\.inputs\.([a-zA-Z0-9_-]+)\s*\}\}`)
// AWImportInputsPattern matches github.aw.import-inputs.* patterns for import-schema form.
// Supports both scalar inputs and one-level deep object sub-keys:
// github.aw.import-inputs.count
// github.aw.import-inputs.config.apiKey
AWImportInputsPattern = regexp.MustCompile(`^github\.aw\.import-inputs\.[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)?$`)
// AWImportInputsExpressionPattern matches full ${{ github.aw.import-inputs.* }} expressions.
// Captures the full dotted path after "import-inputs." (e.g. "count" or "config.apiKey").
// Used for substitution of values provided via the 'with' key in import specifications.
AWImportInputsExpressionPattern = regexp.MustCompile(`\$\{\{\s*github\.aw\.import-inputs\.([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)?)\s*\}\}`)
// EnvPattern matches env.* patterns
// Example: env.NODE_VERSION
EnvPattern = regexp.MustCompile(`^env\.[a-zA-Z0-9_-]+$`)
)
// Secret Patterns
var (
// SecretExpressionPattern matches ${{ secrets.SECRET_NAME }} expressions
// Captures the secret name and supports optional || fallback
SecretExpressionPattern = regexp.MustCompile(`\$\{\{\s*secrets\.([A-Z_][A-Z0-9_]*)\s*(?:\|\|.*?)?\s*\}\}`)
// SecretsExpressionPattern validates complete secrets expression syntax
// Supports chained || fallbacks: ${{ secrets.A || secrets.B }}
SecretsExpressionPattern = regexp.MustCompile(`^\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*(\s*\|\|\s*secrets\.[A-Za-z_][A-Za-z0-9_]*)*\s*\}\}$`)
)
// Template Patterns
var (
// InlineExpressionPattern matches inline ${{ ... }} expressions in templates
InlineExpressionPattern = regexp.MustCompile(`\$\{\{[^}]+\}\}`)
// UnsafeContextPattern matches potentially unsafe context patterns
// These patterns may allow injection attacks in templates
UnsafeContextPattern = regexp.MustCompile(`\$\{\{\s*(github\.event\.|steps\.[^}]+\.outputs\.|inputs\.)[^}]+\}\}`)
// TemplateIfPattern matches {{#if condition }} template conditionals
// Captures the condition expression (which may contain ${{ ... }})
TemplateIfPattern = regexp.MustCompile(`\{\{#if\s+((?:\$\{\{[^\}]*\}\}|[^\}])*)\s*\}\}`)
)
// Comparison and Literal Patterns
var (
// ComparisonExtractionPattern extracts property accesses from comparison expressions
// Matches patterns like "github.workflow == 'value'" and extracts "github.workflow"
ComparisonExtractionPattern = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*)\s*(?:==|!=|<|>|<=|>=)\s*`)
// OrPattern matches logical OR expressions
// Example: value1 || value2
OrPattern = regexp.MustCompile(`^(.+?)\s*\|\|\s*(.+)$`)
// StringLiteralPattern matches string literals in single quotes, double quotes, or backticks
// Example: 'hello', "world", `template`
StringLiteralPattern = regexp.MustCompile(`^'[^']*'$|^"[^"]*"$|^` + "`[^`]*`$")
// NumberLiteralPattern matches numeric literals (integers and decimals)
// Example: 42, -3.14, 0.5
NumberLiteralPattern = regexp.MustCompile(`^-?\d+(\.\d+)?$`)
// RangePattern matches numeric range patterns
// Example: 1-10, 100-200
RangePattern = regexp.MustCompile(`^\d+-\d+$`)
)