1
1
private import codeql.actions.ast.internal.Yaml
2
2
private import codeql.Locations
3
+ private import codeql.actions.Ast:: Utils as Utils
3
4
4
5
/**
5
6
* Gets the length of each line in the StringValue .
@@ -18,24 +19,19 @@ int partialLineLengthSum(string text, int i) {
18
19
result = sum ( int j , int length | j in [ 0 .. i ] and length = lineLength ( text , j ) | length )
19
20
}
20
21
21
- /**
22
- * Holds if `${{ e }}` is a GitHub Actions expression evaluated within this YAML string.
23
- * See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
24
- * Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
25
- * Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
26
- */
27
- string getASimpleReferenceExpression ( YamlString s , int offset ) {
22
+ string getADelimitedExpression ( YamlString s , int offset ) {
28
23
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
29
24
// not just the last (greedy match) or first (reluctant match).
30
25
result =
31
26
s .getValue ( )
32
- .regexpFind ( "\\$\\{\\{\\s*[A-Za-z0-9_\\[\\]\\*\\(\\)\\.\\-]+\\s*\\}\\}" , _, offset )
33
- .regexpCapture ( "(\\$\\{\\{\\s*[A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+\\s*\\}\\})" , 1 )
27
+ .regexpFind ( "\\$\\{\\{(?:[^}]|}(?!}))*\\}\\}" , _, offset )
28
+ .regexpCapture ( "(\\$\\{\\{(?:[^}]|}(?!}))*\\}\\})" , 1 )
29
+ .trim ( )
34
30
}
35
31
36
32
private newtype TAstNode =
37
33
TExpressionNode ( YamlNode key , YamlScalar value , string raw , int exprOffset ) {
38
- raw = getASimpleReferenceExpression ( value , exprOffset ) and
34
+ raw = getADelimitedExpression ( value , exprOffset ) and
39
35
exists ( YamlMapping m |
40
36
(
41
37
exists ( int i | value = m .getValueNode ( i ) and key = m .getKeyNode ( i ) )
@@ -45,6 +41,14 @@ private newtype TAstNode =
45
41
)
46
42
)
47
43
)
44
+ or
45
+ // `if`'s conditions do not need to be delimted with ${{}}
46
+ exists ( YamlMapping m |
47
+ m .maps ( key , value ) and
48
+ key .( YamlScalar ) .getValue ( ) = [ "if" ] and
49
+ value .getValue ( ) = raw and
50
+ exprOffset = 1
51
+ )
48
52
} or
49
53
TCompositeAction ( YamlMapping n ) {
50
54
n instanceof YamlDocument and
@@ -123,7 +127,7 @@ class ScalarValueImpl extends AstNodeImpl, TScalarValueNode {
123
127
124
128
override Location getLocation ( ) { result = value .getLocation ( ) }
125
129
126
- override YamlNode getNode ( ) { result = value }
130
+ override YamlScalar getNode ( ) { result = value }
127
131
}
128
132
129
133
class ExpressionImpl extends AstNodeImpl , TExpressionNode {
@@ -135,15 +139,16 @@ class ExpressionImpl extends AstNodeImpl, TExpressionNode {
135
139
136
140
ExpressionImpl ( ) {
137
141
this = TExpressionNode ( key , value , rawExpression , exprOffset - 1 ) and
138
- expression =
139
- rawExpression .regexpCapture ( "\\$\\{\\{\\s*([A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+)\\s*\\}\\}" , 1 )
142
+ if rawExpression .trim ( ) .regexpMatch ( "\\$\\{\\{.*\\}\\}" )
143
+ then expression = rawExpression .trim ( ) .regexpCapture ( "\\$\\{\\{\\s*(.*)\\s*\\}\\}" , 1 ) .trim ( )
144
+ else expression = rawExpression .trim ( )
140
145
}
141
146
142
147
override string toString ( ) { result = expression }
143
148
144
149
override AstNodeImpl getAChildNode ( ) { none ( ) }
145
150
146
- override AstNodeImpl getParentNode ( ) { result .getNode ( ) = value }
151
+ override ScalarValueImpl getParentNode ( ) { result .getNode ( ) = value }
147
152
148
153
override string getAPrimaryQlClass ( ) { result = "ExpressionNode" }
149
154
@@ -638,6 +643,9 @@ class IfImpl extends AstNodeImpl, TIfNode {
638
643
639
644
/** Gets the condition that must be satisfied for this job to run. */
640
645
string getCondition ( ) { result = n .( YamlScalar ) .getValue ( ) }
646
+
647
+ /** Gets the condition that must be satisfied for this job to run. */
648
+ ExpressionImpl getConditionExpr ( ) { result .getParentNode ( ) .getNode ( ) = n }
641
649
}
642
650
643
651
class EnvImpl extends AstNodeImpl , TEnvNode {
@@ -776,11 +784,29 @@ class RunImpl extends StepImpl {
776
784
}
777
785
}
778
786
787
+ /**
788
+ * Holds if `${{ e }}` is a GitHub Actions expression evaluated within this YAML string.
789
+ * See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
790
+ * Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
791
+ * Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
792
+ */
793
+ bindingset [ s]
794
+ string getASimpleReferenceExpression ( string s , int offset ) {
795
+ // We use `regexpFind` to obtain *all* matches of `${{...}}`,
796
+ // not just the last (greedy match) or first (reluctant match).
797
+ result =
798
+ s .trim ( )
799
+ .regexpFind ( "[A-Za-z0-9'\"_\\[\\]\\*\\(\\)\\.\\-]+" , _, offset )
800
+ .regexpCapture ( "([A-Za-z0-9'\"_\\[\\]\\*\\(\\)\\.\\-]+)" , 1 )
801
+ }
802
+
779
803
/**
780
804
* A ${{}} expression accessing a context variable such as steps, needs, jobs, env, inputs, or matrix.
781
805
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
782
806
*/
783
- abstract class ContextExpressionImpl extends ExpressionImpl {
807
+ abstract class SimpleReferenceExpressionImpl extends ExpressionImpl {
808
+ SimpleReferenceExpressionImpl ( ) { exists ( getASimpleReferenceExpression ( expression , _) ) }
809
+
784
810
abstract string getFieldName ( ) ;
785
811
786
812
abstract AstNodeImpl getTarget ( ) ;
@@ -816,44 +842,49 @@ private string wrapRegexp(string regex) {
816
842
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
817
843
* e.g. `${{ steps.changed-files.outputs.all_changed_files }}`
818
844
*/
819
- class StepsExpressionImpl extends ContextExpressionImpl {
845
+ class StepsExpressionImpl extends SimpleReferenceExpressionImpl {
820
846
string stepId ;
821
847
string fieldName ;
822
848
823
849
StepsExpressionImpl ( ) {
824
- expression .regexpMatch ( stepsCtxRegex ( ) ) and
825
- stepId = expression .regexpCapture ( stepsCtxRegex ( ) , 1 ) and
826
- fieldName = expression .regexpCapture ( stepsCtxRegex ( ) , 2 )
850
+ Utils :: normalizeExpr ( expression ) .regexpMatch ( stepsCtxRegex ( ) ) and
851
+ stepId = Utils :: normalizeExpr ( expression ) .regexpCapture ( stepsCtxRegex ( ) , 1 ) and
852
+ fieldName = Utils :: normalizeExpr ( expression ) .regexpCapture ( stepsCtxRegex ( ) , 2 )
827
853
}
828
854
829
855
override string getFieldName ( ) { result = fieldName }
830
856
831
857
override AstNodeImpl getTarget ( ) {
832
- this .getLocation ( ) . getFile ( ) = result .getLocation ( ) . getFile ( ) and
858
+ this .getEnclosingJob ( ) = result .getEnclosingJob ( ) and
833
859
result .( StepImpl ) .getId ( ) = stepId
834
860
}
861
+
862
+ string getStepId ( ) { result = stepId }
835
863
}
836
864
837
865
/**
838
866
* Holds for an expression accesing the `needs` context.
839
867
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
840
868
* e.g. `${{ needs.job1.outputs.foo}}`
841
869
*/
842
- class NeedsExpressionImpl extends ContextExpressionImpl {
870
+ class NeedsExpressionImpl extends SimpleReferenceExpressionImpl {
843
871
JobImpl neededJob ;
844
872
string fieldName ;
845
873
846
874
NeedsExpressionImpl ( ) {
847
- expression .regexpMatch ( needsCtxRegex ( ) ) and
848
- fieldName = expression .regexpCapture ( needsCtxRegex ( ) , 2 ) and
849
- neededJob .getId ( ) = expression .regexpCapture ( needsCtxRegex ( ) , 1 ) and
875
+ Utils :: normalizeExpr ( expression ) .regexpMatch ( needsCtxRegex ( ) ) and
876
+ fieldName = Utils :: normalizeExpr ( expression ) .regexpCapture ( needsCtxRegex ( ) , 2 ) and
877
+ neededJob .getId ( ) = Utils :: normalizeExpr ( expression ) .regexpCapture ( needsCtxRegex ( ) , 1 ) and
850
878
neededJob .getLocation ( ) .getFile ( ) = this .getLocation ( ) .getFile ( )
851
879
}
852
880
853
881
override string getFieldName ( ) { result = fieldName }
854
882
855
883
override AstNodeImpl getTarget ( ) {
856
- this .getEnclosingJob ( ) .getANeededJob ( ) = neededJob and
884
+ (
885
+ this .getEnclosingJob ( ) .getANeededJob ( ) = neededJob or
886
+ this .getEnclosingJob ( ) = neededJob
887
+ ) and
857
888
(
858
889
// regular jobs
859
890
neededJob .getOutputs ( ) = result
@@ -869,14 +900,14 @@ class NeedsExpressionImpl extends ContextExpressionImpl {
869
900
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
870
901
* e.g. `${{ jobs.job1.outputs.foo}}` (within reusable workflows)
871
902
*/
872
- class JobsExpressionImpl extends ContextExpressionImpl {
903
+ class JobsExpressionImpl extends SimpleReferenceExpressionImpl {
873
904
string jobId ;
874
905
string fieldName ;
875
906
876
907
JobsExpressionImpl ( ) {
877
- expression .regexpMatch ( jobsCtxRegex ( ) ) and
878
- jobId = expression .regexpCapture ( jobsCtxRegex ( ) , 1 ) and
879
- fieldName = expression .regexpCapture ( jobsCtxRegex ( ) , 2 )
908
+ Utils :: normalizeExpr ( expression ) .regexpMatch ( jobsCtxRegex ( ) ) and
909
+ jobId = Utils :: normalizeExpr ( expression ) .regexpCapture ( jobsCtxRegex ( ) , 1 ) and
910
+ fieldName = Utils :: normalizeExpr ( expression ) .regexpCapture ( jobsCtxRegex ( ) , 2 )
880
911
}
881
912
882
913
override string getFieldName ( ) { result = fieldName }
@@ -895,12 +926,12 @@ class JobsExpressionImpl extends ContextExpressionImpl {
895
926
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
896
927
* e.g. `${{ inputs.foo }}`
897
928
*/
898
- class InputsExpressionImpl extends ContextExpressionImpl {
929
+ class InputsExpressionImpl extends SimpleReferenceExpressionImpl {
899
930
string fieldName ;
900
931
901
932
InputsExpressionImpl ( ) {
902
- expression .regexpMatch ( inputsCtxRegex ( ) ) and
903
- fieldName = expression .regexpCapture ( inputsCtxRegex ( ) , 1 )
933
+ Utils :: normalizeExpr ( expression ) .regexpMatch ( inputsCtxRegex ( ) ) and
934
+ fieldName = Utils :: normalizeExpr ( expression ) .regexpCapture ( inputsCtxRegex ( ) , 1 )
904
935
}
905
936
906
937
override string getFieldName ( ) { result = fieldName }
@@ -920,12 +951,12 @@ class InputsExpressionImpl extends ContextExpressionImpl {
920
951
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
921
952
* e.g. `${{ env.foo }}`
922
953
*/
923
- class EnvExpressionImpl extends ContextExpressionImpl {
954
+ class EnvExpressionImpl extends SimpleReferenceExpressionImpl {
924
955
string fieldName ;
925
956
926
957
EnvExpressionImpl ( ) {
927
- expression .regexpMatch ( envCtxRegex ( ) ) and
928
- fieldName = expression .regexpCapture ( envCtxRegex ( ) , 1 )
958
+ Utils :: normalizeExpr ( expression ) .regexpMatch ( envCtxRegex ( ) ) and
959
+ fieldName = Utils :: normalizeExpr ( expression ) .regexpCapture ( envCtxRegex ( ) , 1 )
929
960
}
930
961
931
962
override string getFieldName ( ) { result = fieldName }
@@ -943,12 +974,12 @@ class EnvExpressionImpl extends ContextExpressionImpl {
943
974
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
944
975
* e.g. `${{ matrix.foo }}`
945
976
*/
946
- class MatrixExpressionImpl extends ContextExpressionImpl {
977
+ class MatrixExpressionImpl extends SimpleReferenceExpressionImpl {
947
978
string fieldName ;
948
979
949
980
MatrixExpressionImpl ( ) {
950
- expression .regexpMatch ( matrixCtxRegex ( ) ) and
951
- fieldName = expression .regexpCapture ( matrixCtxRegex ( ) , 1 )
981
+ Utils :: normalizeExpr ( expression ) .regexpMatch ( matrixCtxRegex ( ) ) and
982
+ fieldName = Utils :: normalizeExpr ( expression ) .regexpCapture ( matrixCtxRegex ( ) , 1 )
952
983
}
953
984
954
985
override string getFieldName ( ) { result = fieldName }
0 commit comments