mirror of
https://github.com/github/codeql.git
synced 2026-05-05 05:35:13 +02:00
dataflow through reusable workflows
This commit is contained in:
@@ -39,23 +39,21 @@ class WorkflowStmt extends Statement instanceof Actions::Workflow {
|
||||
}
|
||||
|
||||
class ReusableWorkflowStmt extends WorkflowStmt {
|
||||
YamlMapping parameters;
|
||||
YamlValue workflow_call;
|
||||
|
||||
ReusableWorkflowStmt() {
|
||||
exists(Actions::On on |
|
||||
on.getWorkflow() = this and
|
||||
on.getNode("workflow_call").(YamlMapping).lookup("inputs") = parameters
|
||||
)
|
||||
this.(Actions::Workflow).getOn().getNode("workflow_call") = workflow_call
|
||||
}
|
||||
|
||||
ParamsStmt getParams() { result = parameters }
|
||||
InputsStmt getInputs() { result = workflow_call.(YamlMapping).lookup("inputs") }
|
||||
|
||||
OutputsStmt getOutputs() { result = workflow_call.(YamlMapping).lookup("outputs") }
|
||||
|
||||
// TODO: implemnt callable name
|
||||
string getName() { result = this.getLocation().getFile().getRelativePath() }
|
||||
}
|
||||
|
||||
class ParamsStmt extends Statement instanceof YamlMapping {
|
||||
ParamsStmt() {
|
||||
class InputsStmt extends Statement instanceof YamlMapping {
|
||||
InputsStmt() {
|
||||
exists(Actions::On on | on.getNode("workflow_call").(YamlMapping).lookup("inputs") = this)
|
||||
}
|
||||
|
||||
@@ -72,12 +70,38 @@ class ParamsStmt extends Statement instanceof YamlMapping {
|
||||
* token:
|
||||
* required: true
|
||||
*/
|
||||
ParamExpr getParamExpr(string name) {
|
||||
this.(YamlMapping).maps(any(YamlScalar s | s.getValue() = name), result)
|
||||
InputExpr getInputExpr(string name) {
|
||||
result.(YamlString).getValue() = name and
|
||||
this.(YamlMapping).maps(result, _)
|
||||
}
|
||||
}
|
||||
|
||||
class ParamExpr extends Expression instanceof YamlValue { }
|
||||
class InputExpr extends Expression instanceof YamlString { }
|
||||
|
||||
class OutputsStmt extends Statement instanceof YamlMapping {
|
||||
OutputsStmt() {
|
||||
exists(Actions::On on | on.getNode("workflow_call").(YamlMapping).lookup("outputs") = this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific parameter expression (YamlMapping) by name.
|
||||
* eg:
|
||||
* on:
|
||||
* workflow_call:
|
||||
* outputs:
|
||||
* firstword:
|
||||
* description: "The first output string"
|
||||
* value: ${{ jobs.example_job.outputs.output1 }}
|
||||
* secondword:
|
||||
* description: "The second output string"
|
||||
* value: ${{ jobs.example_job.outputs.output2 }}
|
||||
*/
|
||||
OutputExpr getOutputExpr(string name) {
|
||||
this.(YamlMapping).lookup(name).(YamlMapping).lookup("value") = result
|
||||
}
|
||||
}
|
||||
|
||||
class OutputExpr extends Expression instanceof YamlString { }
|
||||
|
||||
/**
|
||||
* A Job is a collection of steps that run in an execution environment.
|
||||
@@ -117,8 +141,13 @@ class JobStmt extends Statement instanceof Actions::Job {
|
||||
|
||||
/**
|
||||
* Reusable workflow jobs may have Uses children
|
||||
* eg:
|
||||
* call-job:
|
||||
* uses: ./.github/workflows/reusable_workflow.yml
|
||||
* with:
|
||||
* arg1: value1
|
||||
*/
|
||||
JobUsesExpr getUsesExpr() { result = this.(Actions::Job).lookup("uses") }
|
||||
JobUsesExpr getUsesExpr() { result.getJob() = this }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,8 +181,11 @@ class StepStmt extends Statement instanceof Actions::Step {
|
||||
JobStmt getJob() { result = super.getJob() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class representing a call to a 3rd party action or reusable workflow.
|
||||
*/
|
||||
abstract class UsesExpr extends Expression {
|
||||
abstract string getTarget();
|
||||
abstract string getCallee();
|
||||
|
||||
abstract string getVersion();
|
||||
|
||||
@@ -168,7 +200,7 @@ class StepUsesExpr extends StepStmt, UsesExpr {
|
||||
|
||||
StepUsesExpr() { uses.getStep() = this }
|
||||
|
||||
override string getTarget() { result = uses.getGitHubRepository() }
|
||||
override string getCallee() { result = uses.getGitHubRepository() }
|
||||
|
||||
override string getVersion() { result = uses.getVersion() }
|
||||
|
||||
@@ -183,12 +215,12 @@ class StepUsesExpr extends StepStmt, UsesExpr {
|
||||
/**
|
||||
* A Uses step represents a call to an action that is defined in a GitHub repository.
|
||||
*/
|
||||
class JobUsesExpr extends UsesExpr instanceof YamlScalar {
|
||||
JobStmt job;
|
||||
class JobUsesExpr extends UsesExpr instanceof YamlMapping {
|
||||
JobUsesExpr() {
|
||||
this instanceof JobStmt and this.maps(any(YamlString s | s.getValue() = "uses"), _)
|
||||
}
|
||||
|
||||
JobUsesExpr() { job.(YamlMapping).lookup("uses") = this }
|
||||
|
||||
JobStmt getJob() { result = job }
|
||||
JobStmt getJob() { result = this }
|
||||
|
||||
/**
|
||||
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
|
||||
@@ -200,31 +232,31 @@ class JobUsesExpr extends UsesExpr instanceof YamlScalar {
|
||||
|
||||
private string pathUsesParser() { result = "\\./(.+)" }
|
||||
|
||||
override string getTarget() {
|
||||
exists(string name |
|
||||
this.(YamlScalar).getValue() = name and
|
||||
if name.matches("./%")
|
||||
then result = name.regexpCapture(this.pathUsesParser(), 1)
|
||||
override string getCallee() {
|
||||
exists(YamlString name |
|
||||
this.(YamlMapping).lookup("uses") = name and
|
||||
if name.getValue().matches("./%")
|
||||
then result = name.getValue().regexpCapture(this.pathUsesParser(), 1)
|
||||
else
|
||||
result =
|
||||
name.regexpCapture(this.repoUsesParser(), 1) + "/" +
|
||||
name.regexpCapture(this.repoUsesParser(), 2) + "/" +
|
||||
name.regexpCapture(this.repoUsesParser(), 3)
|
||||
name.getValue().regexpCapture(this.repoUsesParser(), 1) + "/" +
|
||||
name.getValue().regexpCapture(this.repoUsesParser(), 2) + "/" +
|
||||
name.getValue().regexpCapture(this.repoUsesParser(), 3)
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
|
||||
override string getVersion() {
|
||||
exists(string name |
|
||||
this.(YamlScalar).getValue() = name and
|
||||
if not name.matches("\\.%")
|
||||
then result = this.(YamlScalar).getValue().regexpCapture(this.repoUsesParser(), 4)
|
||||
exists(YamlString name |
|
||||
this.(YamlMapping).lookup("uses") = name and
|
||||
if not name.getValue().matches("\\.%")
|
||||
then result = name.getValue().regexpCapture(this.repoUsesParser(), 4)
|
||||
else none()
|
||||
)
|
||||
}
|
||||
|
||||
override Expression getArgument(string key) {
|
||||
job.(YamlMapping).lookup("with").(YamlMapping).lookup(key) = result
|
||||
this.(YamlMapping).lookup("with").(YamlMapping).lookup(key) = result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +319,7 @@ class StepOutputAccessExpr extends ExprAccessExpr {
|
||||
/**
|
||||
* A ExprAccessExpr where the expression evaluated is a job output read.
|
||||
* eg: `${{ needs.job1.outputs.foo}}`
|
||||
* eg: `${{ jobs.job1.outputs.foo}}` (for reusable workflows)
|
||||
*/
|
||||
class JobOutputAccessExpr extends ExprAccessExpr {
|
||||
string jobId;
|
||||
@@ -294,9 +327,11 @@ class JobOutputAccessExpr extends ExprAccessExpr {
|
||||
|
||||
JobOutputAccessExpr() {
|
||||
jobId =
|
||||
this.getExpression().regexpCapture("needs\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+", 1) and
|
||||
this.getExpression()
|
||||
.regexpCapture("(needs|jobs)\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+", 2) and
|
||||
varName =
|
||||
this.getExpression().regexpCapture("needs\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)", 1)
|
||||
this.getExpression()
|
||||
.regexpCapture("(needs|jobs)\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)", 2)
|
||||
}
|
||||
|
||||
string getVarName() { result = varName }
|
||||
@@ -305,7 +340,35 @@ class JobOutputAccessExpr extends ExprAccessExpr {
|
||||
exists(JobStmt job |
|
||||
job.getId() = jobId and
|
||||
job.getLocation().getFile() = this.getLocation().getFile() and
|
||||
job.getOutputStmt().getOutputExpr(varName) = result
|
||||
(
|
||||
// A Job can have multiple outputs, so we need to check both
|
||||
// jobs.<job_id>.outputs.<output_name>
|
||||
job.getOutputStmt().getOutputExpr(varName) = result
|
||||
or
|
||||
// jobs.<job_id>.uses (variables returned from the reusable workflow
|
||||
job.getUsesExpr() = result
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A ExprAccessExpr where the expression evaluated is a reusable workflow input read.
|
||||
* eg: `${{ inputs.foo}}`
|
||||
*/
|
||||
class ReusableWorkflowInputAccessExpr extends ExprAccessExpr {
|
||||
string paramName;
|
||||
|
||||
ReusableWorkflowInputAccessExpr() {
|
||||
paramName = this.getExpression().regexpCapture("inputs\\.([A-Za-z0-9_-]+)", 1)
|
||||
}
|
||||
|
||||
string getParamName() { result = paramName }
|
||||
|
||||
Expression getInputExpr() {
|
||||
exists(ReusableWorkflowStmt w |
|
||||
w.getLocation().getFile() = this.getLocation().getFile() and
|
||||
w.getInputs().getInputExpr(paramName) = result
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
3
ql/lib/codeql/actions/Consistency.ql
Normal file
3
ql/lib/codeql/actions/Consistency.ql
Normal file
@@ -0,0 +1,3 @@
|
||||
import DataFlow::DataFlow::Consistency
|
||||
|
||||
|
||||
@@ -7,4 +7,12 @@ module DataFlow {
|
||||
private import codeql.actions.dataflow.internal.DataFlowImplSpecific
|
||||
import DataFlowMake<ActionsDataFlow>
|
||||
import codeql.actions.dataflow.internal.DataFlowPublic
|
||||
|
||||
/** debug */
|
||||
private import codeql.actions.dataflow.internal.TaintTrackingImplSpecific
|
||||
import codeql.dataflow.internal.DataFlowImplConsistency as DFIC
|
||||
module ActionsConsistency implements DFIC::InputSig<ActionsDataFlow> { }
|
||||
module Consistency {
|
||||
import DFIC::MakeConsistency<ActionsDataFlow, ActionsTaintTracking, ActionsConsistency>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +87,9 @@ module Completion {
|
||||
module CfgScope {
|
||||
abstract class CfgScope extends AstNode { }
|
||||
|
||||
private class ReusableWorkflowScope extends CfgScope instanceof ReusableWorkflowStmt { }
|
||||
class ReusableWorkflowScope extends CfgScope instanceof ReusableWorkflowStmt { }
|
||||
|
||||
private class JobScope extends CfgScope instanceof JobStmt { }
|
||||
class JobScope extends CfgScope instanceof JobStmt { }
|
||||
}
|
||||
|
||||
private module Implementation implements CfgShared::InputSig<Location> {
|
||||
@@ -123,7 +123,7 @@ private module Implementation implements CfgShared::InputSig<Location> {
|
||||
int maxSplits() { result = 0 }
|
||||
|
||||
predicate scopeFirst(CfgScope scope, AstNode e) {
|
||||
first(scope.(ReusableWorkflowStmt).getParams(), e) or
|
||||
first(scope.(ReusableWorkflowStmt).getInputs(), e) or
|
||||
first(scope.(JobStmt), e)
|
||||
}
|
||||
|
||||
@@ -148,14 +148,11 @@ private import Completion
|
||||
private import CfgScope
|
||||
|
||||
private class ReusableWorkflowTree extends StandardPreOrderTree instanceof ReusableWorkflowStmt {
|
||||
override ControlFlowTree getChildNode(int i) { result = super.getParams() and i = 0 }
|
||||
}
|
||||
|
||||
private class ReusableWorkflowParamsTree extends StandardPreOrderTree instanceof ParamsStmt {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](Expression child, Location l |
|
||||
child = super.getParamExpr(_) and l = child.getLocation()
|
||||
(child = super.getInputs() or child = super.getOutputs()) and
|
||||
l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
@@ -164,7 +161,35 @@ private class ReusableWorkflowParamsTree extends StandardPreOrderTree instanceof
|
||||
}
|
||||
}
|
||||
|
||||
private class ParamExprTree extends LeafTree instanceof ParamExpr { }
|
||||
private class ReusableWorkflowInputsTree extends StandardPreOrderTree instanceof InputsStmt {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](Expression child, Location l |
|
||||
child = super.getInputExpr(_) and l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class InputExprTree extends LeafTree instanceof InputExpr { }
|
||||
|
||||
private class ReusableWorkflowOutputsTree extends StandardPreOrderTree instanceof OutputsStmt {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
result =
|
||||
rank[i](Expression child, Location l |
|
||||
child = super.getOutputExpr(_) and l = child.getLocation()
|
||||
|
|
||||
child
|
||||
order by
|
||||
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class OutputExprTree extends LeafTree instanceof OutputExpr { }
|
||||
|
||||
private class JobTree extends StandardPreOrderTree instanceof JobStmt {
|
||||
override ControlFlowTree getChildNode(int i) {
|
||||
|
||||
@@ -127,7 +127,7 @@ private class EventSource extends RemoteFlowSource {
|
||||
private class ChangedFilesSource extends RemoteFlowSource {
|
||||
ChangedFilesSource() {
|
||||
exists(UsesExpr uses |
|
||||
uses.getTarget() = "tj-actions/changed-files" and
|
||||
uses.getCallee() = "tj-actions/changed-files" and
|
||||
uses.getVersion() = ["v10", "v20", "v30", "v40"] and
|
||||
uses = this.asExpr()
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ class AdditionalTaintStep extends Unit {
|
||||
private class ActionsFindAndReplaceStringStep extends AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(UsesExpr u |
|
||||
u.getTarget() = "mad9000/actions-find-and-replace-string" and
|
||||
u.getCallee() = "mad9000/actions-find-and-replace-string" and
|
||||
pred.asExpr() = u.getArgument(["source", "replace"]) and
|
||||
succ.asExpr() = u
|
||||
)
|
||||
|
||||
@@ -6,38 +6,7 @@ private import codeql.actions.controlflow.BasicBlocks
|
||||
private import DataFlowPublic
|
||||
|
||||
cached
|
||||
newtype TNode =
|
||||
TExprNode(DataFlowExpr e) or
|
||||
TParameterNode(ParamExpr p) { p = any(ReusableWorkflowStmt w).getParams().getParamExpr(_) } or
|
||||
TReturningNode(Cfg::Node n) { n.getAstNode() = any(JobStmt j).getOutputStmt().getOutputExpr(_) }
|
||||
|
||||
/**
|
||||
* Reusable workflow input nodes
|
||||
*/
|
||||
class ParameterNode extends Node, TParameterNode {
|
||||
private ParamExpr parameter;
|
||||
|
||||
ParameterNode() { this = TParameterNode(parameter) }
|
||||
|
||||
predicate isParameterOf(DataFlowCallable c, ParameterPosition pos) {
|
||||
parameter = c.(ReusableWorkflowStmt).getParams().getParamExpr(pos)
|
||||
}
|
||||
|
||||
override string toString() { result = parameter.toString() }
|
||||
|
||||
override Location getLocation() { result = parameter.getLocation() }
|
||||
|
||||
ParamExpr getParameter() { result = parameter }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable workflow output nodes
|
||||
*/
|
||||
class ReturnNode extends Node {
|
||||
ReturnNode() { none() }
|
||||
|
||||
ReturnKind getKind() { none() }
|
||||
}
|
||||
newtype TNode = TExprNode(DataFlowExpr e)
|
||||
|
||||
class OutNode extends ExprNode {
|
||||
private DataFlowCall call;
|
||||
@@ -76,6 +45,8 @@ predicate isArgumentNode(ArgumentNode arg, DataFlowCall call, ArgumentPosition p
|
||||
|
||||
DataFlowCallable nodeGetEnclosingCallable(Node node) {
|
||||
node = TExprNode(any(DataFlowExpr e | result = e.getScope()))
|
||||
// node = TReturningNode(any(Cfg::Node n | result = n.getScope()))
|
||||
// node = TParameterNode(any(InputExpr p | p = result.(ReusableWorkflowStmt).getInputs().getInputExpr(_)))
|
||||
}
|
||||
|
||||
DataFlowType getNodeType(Node node) { any() }
|
||||
@@ -97,21 +68,27 @@ class DataFlowCall instanceof Cfg::Node {
|
||||
|
||||
Location getLocation() { result = super.getLocation() }
|
||||
|
||||
string getName() { result = super.getAstNode().(UsesExpr).getTarget() }
|
||||
string getName() { result = super.getAstNode().(UsesExpr).getCallee() }
|
||||
|
||||
DataFlowCallable getEnclosingCallable() { result = super.getScope() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Cfg scope that can be called
|
||||
* ReusableWorkflowStmt
|
||||
*/
|
||||
class DataFlowCallable instanceof ReusableWorkflowStmt {
|
||||
class DataFlowCallable instanceof Cfg::CfgScope {
|
||||
string toString() { result = super.toString() }
|
||||
|
||||
Location getLocation() { result = super.getLocation() }
|
||||
|
||||
string getName() { result = super.getName() }
|
||||
string getName() {
|
||||
if this instanceof ReusableWorkflowStmt
|
||||
then result = this.(ReusableWorkflowStmt).getName()
|
||||
else
|
||||
if this instanceof JobStmt
|
||||
then result = this.(JobStmt).getId()
|
||||
else none()
|
||||
}
|
||||
}
|
||||
|
||||
newtype TReturnKind = TNormalReturn()
|
||||
@@ -188,7 +165,7 @@ ContentApprox getContentApprox(Content c) { none() }
|
||||
* Made a string to match the ArgumentPosition type
|
||||
*/
|
||||
class ParameterPosition extends string {
|
||||
ParameterPosition() { exists(any(ReusableWorkflowStmt w).getParams().getParamExpr(this)) }
|
||||
ParameterPosition() { exists(any(ReusableWorkflowStmt w).getInputs().getInputExpr(this)) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,20 +208,25 @@ predicate jobOutputDefToUse(Node nodeFrom, Node nodeTo) {
|
||||
)
|
||||
}
|
||||
|
||||
predicate reusableWorkflowInputDefToUse(Node nodeFrom, Node nodeTo) {
|
||||
// nodeTo is a ReusableWorkflowInputAccessExpr and nodeFrom is the ReusableWorkflowStmt corresponding parameter expression
|
||||
exists(Expression astFrom, ReusableWorkflowInputAccessExpr astTo |
|
||||
astFrom = nodeFrom.asExpr() and
|
||||
astTo = nodeTo.asExpr() and
|
||||
astTo.getInputExpr() = astFrom
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a local flow step from `nodeFrom` to `nodeTo`.
|
||||
* For Actions, we dont need SSA nodes since it should be already in SSA form
|
||||
* Local flow steps are always between two nodes in the same Cfg scope (job definition).
|
||||
*/
|
||||
pragma[nomagic]
|
||||
predicate localFlowStep(Node nodeFrom, Node nodeTo) {
|
||||
stepUsesOutputDefToUse(nodeFrom, nodeTo) or
|
||||
runOutputDefToUse(nodeFrom, nodeTo) or
|
||||
jobOutputDefToUse(nodeFrom, nodeTo)
|
||||
}
|
||||
predicate localFlowStep(Node nodeFrom, Node nodeTo) { none() }
|
||||
|
||||
/**
|
||||
* a simple local flow step
|
||||
* a simple local flow step that should always preserve the call context (same callable)
|
||||
*/
|
||||
predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) { localFlowStep(nodeFrom, nodeTo) }
|
||||
|
||||
@@ -252,8 +234,16 @@ predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) { localFlowStep(nodeFr
|
||||
* Holds if data can flow from `node1` to `node2` through a non-local step
|
||||
* that does not follow a call edge. For example, a step through a global
|
||||
* variable.
|
||||
* We throw away the call context and let us jump to any location
|
||||
* AKA teleport steps
|
||||
* local steps are preferible since they are more predictable and easier to control
|
||||
*/
|
||||
predicate jumpStep(Node node1, Node node2) { none() }
|
||||
predicate jumpStep(Node nodeFrom, Node nodeTo) {
|
||||
stepUsesOutputDefToUse(nodeFrom, nodeTo) or
|
||||
runOutputDefToUse(nodeFrom, nodeTo) or
|
||||
jobOutputDefToUse(nodeFrom, nodeTo) or
|
||||
reusableWorkflowInputDefToUse(nodeFrom, nodeTo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if data can flow from `node1` to `node2` via a read of `c`. Thus,
|
||||
|
||||
@@ -44,6 +44,28 @@ class ExprNode extends Node, TExprNode {
|
||||
override AstNode asExpr() { result = expr.getAstNode() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable workflow input nodes
|
||||
*/
|
||||
class ParameterNode extends ExprNode {
|
||||
private InputExpr parameter;
|
||||
|
||||
ParameterNode() {
|
||||
this.asExpr() = parameter and
|
||||
parameter = any(ReusableWorkflowStmt w).getInputs().getInputExpr(_)
|
||||
}
|
||||
|
||||
predicate isParameterOf(DataFlowCallable c, ParameterPosition pos) {
|
||||
parameter = c.(ReusableWorkflowStmt).getInputs().getInputExpr(pos)
|
||||
}
|
||||
|
||||
override string toString() { result = parameter.toString() }
|
||||
|
||||
override Location getLocation() { result = parameter.getLocation() }
|
||||
|
||||
InputExpr getInputExpr() { result = parameter }
|
||||
}
|
||||
|
||||
/**
|
||||
* An argument to a Uses step (call)
|
||||
*/
|
||||
@@ -51,12 +73,30 @@ class ArgumentNode extends ExprNode {
|
||||
ArgumentNode() { this.getCfgNode().getAstNode() = any(UsesExpr e).getArgument(_) }
|
||||
|
||||
predicate argumentOf(DataFlowCall call, ArgumentPosition pos) {
|
||||
this.getCfgNode() = call.(Cfg::Node).getAPredecessor+() and
|
||||
this.getCfgNode() = call.(Cfg::Node).getASuccessor+() and
|
||||
call.(Cfg::Node).getAstNode() =
|
||||
any(UsesExpr e | e.getArgument(pos) = this.getCfgNode().getAstNode())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable workflow output nodes
|
||||
*/
|
||||
class ReturnNode extends ExprNode {
|
||||
private Cfg::Node node;
|
||||
|
||||
ReturnNode() {
|
||||
this.getCfgNode() = node and
|
||||
node.getAstNode() = any(ReusableWorkflowStmt w).getOutputs().getOutputExpr(_)
|
||||
}
|
||||
|
||||
ReturnKind getKind() { result = TNormalReturn() }
|
||||
|
||||
override string toString() { result = "return " + node.toString() }
|
||||
|
||||
override Location getLocation() { result = node.getLocation() }
|
||||
}
|
||||
|
||||
/** Gets the node corresponding to `e`. */
|
||||
Node exprNode(DataFlowExpr e) { result = TExprNode(e) }
|
||||
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
on: push
|
||||
name: Call a reusable workflow and use its outputs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call-workflow-1-in-local-repo:
|
||||
call1:
|
||||
uses: octo-org/this-repo/.github/workflows/reusable_workflow.yml@172239021f7ba04fe7327647b213799853a9eb89
|
||||
with:
|
||||
config-path: ${{ github.event.pull_request.head.ref }}
|
||||
secrets: inherit
|
||||
call-workflow-2-in-local-repo:
|
||||
call2:
|
||||
uses: ./.github/workflows/reusable_workflow.yml
|
||||
with:
|
||||
config-path: ${{ github.event.pull_request.head.ref }}
|
||||
secrets: inherit
|
||||
call-workflow-in-another-repo:
|
||||
call3:
|
||||
uses: octo-org/another-repo/.github/workflows/workflow.yml@v1
|
||||
with:
|
||||
config-path: ${{ github.event.pull_request.head.ref }}
|
||||
secrets: inherit
|
||||
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
needs: call1
|
||||
steps:
|
||||
- run: echo ${{ needs.call1.outputs.workflow-output }}
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: call2
|
||||
steps:
|
||||
- run: echo ${{ needs.call2.outputs.workflow-output1 }}
|
||||
- run: echo ${{ needs.call2.outputs.workflow-output2 }}
|
||||
job3:
|
||||
runs-on: ubuntu-latest
|
||||
needs: call3
|
||||
steps:
|
||||
- run: echo ${{ needs.call3.outputs.workflow-output }}
|
||||
|
||||
@@ -6,13 +6,28 @@ on:
|
||||
config-path:
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
workflow-output1:
|
||||
value: ${{ jobs.job1.outputs.job-output1 }}
|
||||
workflow-output2:
|
||||
value: ${{ jobs.job1.outputs.job-output2 }}
|
||||
secrets:
|
||||
token:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
job-output1: ${{ steps.step1.outputs.step-output}}
|
||||
job-output2: ${{ steps.step2.outputs.all_changed_files}}
|
||||
steps:
|
||||
- id: sink
|
||||
run: echo ${{ inputs.config-path }}
|
||||
- id: step1
|
||||
env:
|
||||
CONFIG_PATH: ${{ inputs.config-path }}
|
||||
run: |
|
||||
echo ${{ inputs.config-path }}
|
||||
echo "::set-output name=step-output:: $CONFIG_PATH"
|
||||
- name: Get changed files
|
||||
id: step2
|
||||
uses: tj-actions/changed-files@v40
|
||||
|
||||
@@ -48,7 +48,7 @@ query predicate parentNodes(AstNode child, AstNode parent) { child.getParentNode
|
||||
|
||||
query predicate cfgNodes(Cfg::Node n) {
|
||||
//any()
|
||||
n.getAstNode() instanceof JobUsesExpr
|
||||
n.getAstNode() instanceof OutputsStmt
|
||||
}
|
||||
|
||||
query predicate dfNodes(DataFlow::Node e) {
|
||||
@@ -66,3 +66,5 @@ query predicate usesIds(StepUsesExpr s, string a) { s.getId() = a }
|
||||
query predicate varIds(StepOutputAccessExpr s, string a) { s.getStepId() = a }
|
||||
|
||||
query predicate nodeLocations(DataFlow::Node n, Location l) { n.getLocation() = l }
|
||||
|
||||
query predicate scopes(Cfg::CfgScope c) { any() }
|
||||
|
||||
@@ -24,6 +24,7 @@ private module MyConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof ExpressionInjectionSink }
|
||||
//predicate isSink(DataFlow::Node sink) { any() }
|
||||
//predicate neverSkip(DataFlow::Node node) { any() }
|
||||
}
|
||||
|
||||
|
||||
33
ql/src/test/partial.ql
Normal file
33
ql/src/test/partial.ql
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @name Forward Partial Dataflow
|
||||
* @description Forward Partial Dataflow
|
||||
* @kind path-problem
|
||||
* @precision low
|
||||
* @problem.severity error
|
||||
* @id actions/test-dataflow
|
||||
*/
|
||||
|
||||
import actions
|
||||
import codeql.actions.TaintTracking
|
||||
import codeql.actions.dataflow.FlowSources
|
||||
import PartialFlow::PartialPathGraph
|
||||
|
||||
private module MyConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) {
|
||||
source instanceof RemoteFlowSource and
|
||||
source.getLocation().getFile().getBaseName() = "calling_workflow.yml"
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { none() }
|
||||
}
|
||||
|
||||
private module MyFlow = TaintTracking::Global<MyConfig>; // or DataFlow::Global<..>
|
||||
|
||||
int explorationLimit() { result = 10 }
|
||||
|
||||
private module PartialFlow = MyFlow::FlowExplorationFwd<explorationLimit/0>;
|
||||
|
||||
from PartialFlow::PartialPathNode source, PartialFlow::PartialPathNode sink
|
||||
where PartialFlow::partialFlow(source, sink, _)
|
||||
select sink.getNode(), source, sink, "This node receives taint from $@.", source.getNode(),
|
||||
"this source"
|
||||
Reference in New Issue
Block a user