Merge branch 'master' into steps

This commit is contained in:
Jorge
2024-02-21 15:31:42 +01:00
committed by GitHub
51 changed files with 32564 additions and 421 deletions

View File

@@ -95,6 +95,8 @@ class OutputsStmt extends Statement instanceof YamlMapping {
this.(YamlMapping).lookup(name).(YamlMapping).lookup("value") = result or
this.(YamlMapping).lookup(name) = result
}
string getAnOutputName() { this.(YamlMapping).maps(any(YamlString s | s.getValue() = result), _) }
}
class InputExpr extends Expression instanceof YamlString {
@@ -147,7 +149,7 @@ class JobStmt extends Statement instanceof Actions::Job {
* out1: ${steps.foo.bar}
* out2: ${steps.foo.baz}
*/
JobOutputStmt getOutputStmt() { result = this.(Actions::Job).lookup("outputs") }
OutputsStmt getOutputsStmt() { result = this.(Actions::Job).lookup("outputs") }
/**
* Reusable workflow jobs may have Uses children
@@ -158,27 +160,9 @@ class JobStmt extends Statement instanceof Actions::Job {
* arg1: value1
*/
JobUsesExpr getUsesExpr() { result.getJobStmt() = this }
}
/**
* Declaration of the outputs for the job.
* eg:
* out1: ${steps.foo.bar}
* out2: ${steps.foo.baz}
*/
class JobOutputStmt extends Statement instanceof YamlMapping {
JobStmt job;
JobOutputStmt() { job.(YamlMapping).lookup("outputs") = this }
YamlMapping asYamlMapping() { result = this }
/**
* Gets a specific value expression
* eg: ${steps.foo.bar}
*/
Expression getOutputExpr(string id) {
this.(YamlMapping).maps(any(YamlScalar s | s.getValue() = id), result)
predicate usesReusableWorkflow() {
this.(YamlMapping).maps(any(YamlString s | s.getValue() = "uses"), _)
}
}
@@ -353,26 +337,60 @@ class ExprAccessExpr extends Expression instanceof YamlString {
string getExpression() { result = expr }
JobStmt getJobStmt() { result.getAChildNode*() = this }
}
/**
* A context access expression.
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
*/
class CtxAccessExpr extends ExprAccessExpr {
CtxAccessExpr() {
expr.regexpMatch([
stepsCtxRegex(), needsCtxRegex(), jobsCtxRegex(), envCtxRegex(), inputsCtxRegex()
])
}
abstract string getFieldName();
abstract Expression getRefExpr();
}
private string stepsCtxRegex() {
result = "\\bsteps\\.([A-Za-z0-9_-]+)\\.outputs\\.([A-Za-z0-9_-]+)\\b"
}
private string needsCtxRegex() {
result = "\\bneeds\\.([A-Za-z0-9_-]+)\\.outputs\\.([A-Za-z0-9_-]+)\\b"
}
private string jobsCtxRegex() {
result = "\\bjobs\\.([A-Za-z0-9_-]+)\\.outputs\\.([A-Za-z0-9_-]+)\\b"
}
private string envCtxRegex() { result = "\\benv\\.([A-Za-z0-9_-]+)\\b" }
private string inputsCtxRegex() {
result = "\\binputs\\.([A-Za-z0-9_-]+)\\b" or
result = "\\bgithub\\.event\\.inputs\\.([A-Za-z0-9_-]+)\\b"
}
/**
* Holds for an ExprAccessExpr accesing the `steps` context.
* Holds for an expression accesing the `steps` context.
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
* e.g. `${{ steps.changed-files.outputs.all_changed_files }}`
*/
class StepOutputAccessExpr extends ExprAccessExpr {
class StepsCtxAccessExpr extends CtxAccessExpr {
string stepId;
string varName;
string fieldName;
StepOutputAccessExpr() {
stepId =
this.getExpression().regexpCapture("steps\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+", 1) and
varName =
this.getExpression().regexpCapture("steps\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)", 1)
StepsCtxAccessExpr() {
expr.regexpMatch(stepsCtxRegex()) and
stepId = expr.regexpCapture(stepsCtxRegex(), 1) and
fieldName = expr.regexpCapture(stepsCtxRegex(), 2)
}
override string getFieldName() { result = fieldName }
override Expression getRefExpr() {
this.getLocation().getFile() = result.getLocation().getFile() and
result.(StepStmt).getId() = stepId
@@ -380,79 +398,112 @@ class StepOutputAccessExpr extends ExprAccessExpr {
}
/**
* Holds for an ExprAccessExpr accesing the `needs` or `job` contexts.
* Holds for an expression accesing the `needs` context.
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
* e.g. `${{ needs.job1.outputs.foo}}` or `${{ jobs.job1.outputs.foo}}` (for reusable workflows)
* e.g. `${{ needs.job1.outputs.foo}}`
*/
class JobOutputAccessExpr extends ExprAccessExpr {
class NeedsCtxAccessExpr extends CtxAccessExpr {
JobStmt job;
string jobId;
string varName;
string fieldName;
JobOutputAccessExpr() {
jobId =
this.getExpression()
.regexpCapture("(needs|jobs)\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+", 2) and
varName =
this.getExpression()
.regexpCapture("(needs|jobs)\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)", 2)
NeedsCtxAccessExpr() {
expr.regexpMatch(needsCtxRegex()) and
jobId = expr.regexpCapture(needsCtxRegex(), 1) and
fieldName = expr.regexpCapture(needsCtxRegex(), 2) and
job.getId() = jobId
}
predicate usesReusableWorkflow() { job.usesReusableWorkflow() }
override string getFieldName() { result = fieldName }
override Expression getRefExpr() {
job.getLocation().getFile() = this.getLocation().getFile() and
(
// regular jobs
job.getOutputsStmt() = result
or
// reusable workflow calling jobs
job.getUsesExpr() = result
)
}
}
/**
* Holds for an expression accesing the `jobs` context.
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
* e.g. `${{ jobs.job1.outputs.foo}}` (within reusable workflows)
*/
class JobsCtxAccessExpr extends CtxAccessExpr {
string jobId;
string fieldName;
JobsCtxAccessExpr() {
expr.regexpMatch(jobsCtxRegex()) and
jobId = expr.regexpCapture(jobsCtxRegex(), 1) and
fieldName = expr.regexpCapture(jobsCtxRegex(), 2)
}
override string getFieldName() { result = fieldName }
override Expression getRefExpr() {
exists(JobStmt job |
job.getId() = jobId and
job.getLocation().getFile() = this.getLocation().getFile() and
(
// 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
)
job.getOutputsStmt() = result
)
}
}
/**
* Holds for an ExprAccessExpr accesing the `inputs` context.
* Holds for an expression the `inputs` context.
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
* e.g. `${{ inputs.foo }}`
*/
class InputAccessExpr extends ExprAccessExpr {
string paramName;
class InputsCtxAccessExpr extends CtxAccessExpr {
string fieldName;
InputAccessExpr() {
paramName = this.getExpression().regexpCapture("inputs\\.([A-Za-z0-9_-]+)", 1)
InputsCtxAccessExpr() {
expr.regexpMatch(inputsCtxRegex()) and
fieldName = expr.regexpCapture(inputsCtxRegex(), 1)
}
override string getFieldName() { result = fieldName }
override Expression getRefExpr() {
exists(ReusableWorkflowStmt w |
w.getLocation().getFile() = this.getLocation().getFile() and
w.getInputsStmt().getInputExpr(paramName) = result
w.getInputsStmt().getInputExpr(fieldName) = result
)
or
exists(CompositeActionStmt a |
a.getLocation().getFile() = this.getLocation().getFile() and
a.getInputsStmt().getInputExpr(paramName) = result
a.getInputsStmt().getInputExpr(fieldName) = result
)
}
}
/**
* Holds for an ExprAccessExpr accesing the `env` context.
* Holds for an expression accesing the `env` context.
* https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
* e.g. `${{ env.foo }}`
*/
class EnvAccessExpr extends ExprAccessExpr {
string varName;
class EnvCtxAccessExpr extends CtxAccessExpr {
string fieldName;
EnvAccessExpr() { varName = this.getExpression().regexpCapture("env\\.([A-Za-z0-9_-]+)", 1) }
EnvCtxAccessExpr() {
expr.regexpMatch(envCtxRegex()) and
fieldName = expr.regexpCapture(envCtxRegex(), 1)
}
override string getFieldName() { result = fieldName }
override Expression getRefExpr() {
exists(JobUsesExpr s | s.getEnvExpr(varName) = result)
exists(JobUsesExpr s | s.getEnvExpr(fieldName) = result)
or
exists(StepUsesExpr s | s.getEnvExpr(varName) = result)
exists(StepUsesExpr s | s.getEnvExpr(fieldName) = result)
or
exists(RunExpr s | s.getEnvExpr(varName) = result)
exists(RunExpr s | s.getEnvExpr(fieldName) = result)
}
}

View File

@@ -7,12 +7,13 @@ module DataFlow {
private import codeql.actions.dataflow.internal.DataFlowImplSpecific
import DataFlowMake<ActionsDataFlow>
import codeql.actions.dataflow.internal.DataFlowPublic
/** debug */
// 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>
}
import DFIC::MakeConsistency<ActionsDataFlow, ActionsTaintTracking, ActionsConsistency>
}
}

View File

@@ -294,8 +294,10 @@ module Actions {
/** Gets the owner and name of the repository where the Action comes from, e.g. `actions/checkout` in `actions/checkout@v2`. */
string getGitHubRepository() {
result =
this.getValue().regexpCapture(usesParser(), 1) + "/" +
this.getValue().regexpCapture(usesParser(), 2)
(
this.getValue().regexpCapture(usesParser(), 1) + "/" +
this.getValue().regexpCapture(usesParser(), 2)
).toLowerCase()
}
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */

View File

@@ -231,7 +231,7 @@ private class JobTree extends StandardPreOrderTree instanceof JobStmt {
rank[i](Expression child, Location l |
(
child = super.getAStepStmt() or
child = super.getOutputStmt() or
child = super.getOutputsStmt() or
child = super.getUsesExpr()
) and
l = child.getLocation()
@@ -243,25 +243,9 @@ private class JobTree extends StandardPreOrderTree instanceof JobStmt {
}
}
private class JobOutputTree extends StandardPreOrderTree instanceof JobOutputStmt {
override ControlFlowTree getChildNode(int i) { result = super.asYamlMapping().getValueNode(i) }
}
private class UsesExprTree extends LeafTree instanceof UsesExpr { }
private class StepUsesTree extends StandardPreOrderTree instanceof StepUsesExpr {
override ControlFlowTree getChildNode(int i) {
result =
rank[i](Expression child, Location l |
(child = super.getArgumentExpr(_) or child = super.getEnvExpr(_)) and
l = child.getLocation()
|
child
order by
l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString()
)
}
}
private class JobUsesTree extends StandardPreOrderTree instanceof JobUsesExpr {
private class UsesTree extends StandardPreOrderTree instanceof UsesExpr {
override ControlFlowTree getChildNode(int i) {
result =
rank[i](Expression child, Location l |

View File

@@ -2,21 +2,31 @@ private import internal.ExternalFlowExtensions as Extensions
import codeql.actions.DataFlow
import actions
/** Holds if a source model exists for the given parameters. */
/**
* MaD sources
* Fields:
* - action: Fully-qualified action name (NWO)
* - version: Either '*' or a specific SHA/Tag
* - output arg: To node (prefixed with either `env.` or `output.`)
* - trigger: Triggering event under which this model introduces tainted data. Use `*` for any event.
*/
predicate sourceModel(string action, string version, string output, string trigger, string kind) {
Extensions::sourceModel(action, version, output, trigger, kind)
}
/** Holds if a sink model exists for the given parameters. */
/**
* MaD summaries
* Fields:
* - action: Fully-qualified action name (NWO)
* - version: Either '*' or a specific SHA/Tag
* - input arg: From node (prefixed with either `env.` or `input.`)
* - output arg: To node (prefixed with either `env.` or `output.`)
* - kind: Either 'Taint' or 'Value'
*/
predicate summaryModel(string action, string version, string input, string output, string kind) {
Extensions::summaryModel(action, version, input, output, kind)
}
/** Holds if a sink model exists for the given parameters. */
predicate sinkModel(string action, string version, string input, string kind) {
Extensions::sinkModel(action, version, input, kind)
}
/**
* MaD sinks
* Fields:
@@ -25,15 +35,67 @@ predicate sinkModel(string action, string version, string input, string kind) {
* - input: sink node (prefixed with either `env.` or `input.`)
* - kind: sink kind
*/
predicate sinkNode(DataFlow::ExprNode sink, string kind) {
predicate sinkModel(string action, string version, string input, string kind) {
Extensions::sinkModel(action, version, input, kind)
}
predicate externallyDefinedSource(DataFlow::Node source, string sourceType, string fieldName) {
exists(UsesExpr uses, string action, string version, string trigger, string kind |
sourceModel(action, version, fieldName, trigger, kind) and
uses.getCallee() = action.toLowerCase() and
(
if version.trim() = "*"
then uses.getVersion() = any(string v)
else uses.getVersion() = version.trim()
) and
(
if fieldName.trim().matches("env.%")
then source.asExpr() = uses.getEnvExpr(fieldName.trim().replaceAll("env.", ""))
else
if fieldName.trim().matches("output.%")
then source.asExpr() = uses
else none()
) and
sourceType = kind
)
}
predicate externallyDefinedStoreStep(
DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c
) {
exists(UsesExpr uses, string action, string version, string input, string output |
summaryModel(action, version, input, output, "taint") and
c = any(DataFlow::FieldContent ct | ct.getName() = output.replaceAll("output.", "")) and
uses.getCallee() = action.toLowerCase() and
(
if version.trim() = "*"
then uses.getVersion() = any(string v)
else uses.getVersion() = version.trim()
) and
(
if input.trim().matches("env.%")
then pred.asExpr() = uses.getEnvExpr(input.trim().replaceAll("env.", ""))
else
if input.trim().matches("input.%")
then pred.asExpr() = uses.getArgumentExpr(input.trim().replaceAll("input.", ""))
else none()
) and
succ.asExpr() = uses
)
}
predicate externallyDefinedSink(DataFlow::ExprNode sink, string kind) {
exists(UsesExpr uses, string action, string version, string input |
(
if input.trim().matches("env.%")
then sink.asExpr() = uses.getEnvExpr(input.trim().replaceAll("input\\.", ""))
else sink.asExpr() = uses.getArgumentExpr(input.trim())
then sink.asExpr() = uses.getEnvExpr(input.trim().replaceAll("env.", ""))
else
if input.trim().matches("input.%")
then sink.asExpr() = uses.getArgumentExpr(input.trim().replaceAll("input.", ""))
else none()
) and
sinkModel(action, version, input, kind) and
uses.getCallee() = action and
uses.getCallee() = action.toLowerCase() and
(
if version.trim() = "*"
then uses.getVersion() = any(string v)

View File

@@ -126,44 +126,18 @@ private class EventSource extends RemoteFlowSource {
}
/**
* MaD sources
* Fields:
* - action: Fully-qualified action name (NWO)
* - version: Either '*' or a specific SHA/Tag
* - output arg: To node (prefixed with either `env.` or `output.`)
* - trigger: Triggering event under which this model introduces tainted data. Use `*` for any event.
* A Source of untrusted data defined in a MaD specification
*/
private class ExternallyDefinedSource extends RemoteFlowSource {
string soutceType;
string sourceType;
ExternallyDefinedSource() {
exists(
UsesExpr uses, string action, string version, string output, string trigger, string kind
|
sourceModel(action, version, output, trigger, kind) and
uses.getCallee() = action and
(
if version.trim() = "*"
then uses.getVersion() = any(string v)
else uses.getVersion() = version.trim()
) and
(
if output.trim().matches("env.%")
then this.asExpr() = uses.getEnvExpr(output.trim().replaceAll("output\\.", ""))
else
// 'output.' is the default qualifier
// TODO: Taint just the specified output
this.asExpr() = uses
) and
soutceType = kind
)
}
ExternallyDefinedSource() { externallyDefinedSource(this, sourceType, _) }
override string getSourceType() { result = soutceType }
override string getSourceType() { result = sourceType }
}
/**
* Composite action input sources
* An input for a Composite Action
*/
private class CompositeActionInputSource extends RemoteFlowSource {
CompositeActionStmt c;

View File

@@ -21,42 +21,6 @@ class AdditionalTaintStep extends Unit {
abstract predicate step(DataFlow::Node node1, DataFlow::Node node2);
}
/**
* MaD summaries
* Fields:
* - action: Fully-qualified action name (NWO)
* - version: Either '*' or a specific SHA/Tag
* - input arg: From node (prefixed with either `env.` or `input.`)
* - output arg: To node (prefixed with either `env.` or `output.`)
* - kind: Either 'Taint' or 'Value'
*/
predicate externallyDefinedSummary(DataFlow::Node pred, DataFlow::Node succ) {
exists(UsesExpr uses, string action, string version, string input |
// `output` not used yet
summaryModel(action, version, input, _, "taint") and
uses.getCallee() = action and
(
if version.trim() = "*"
then uses.getVersion() = any(string v)
else uses.getVersion() = version.trim()
) and
(
if input.trim().matches("env.%")
then pred.asExpr() = uses.getEnvExpr(input.trim().replaceAll("env\\.", ""))
else
// 'input.' is the default qualifier
pred.asExpr() = uses.getArgumentExpr(input.trim().replaceAll("input\\.", ""))
) and
succ.asExpr() = uses
)
}
private class ExternallyDefinedSummary extends AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
externallyDefinedSummary(pred, succ)
}
}
/**
* Holds if a Run step declares an environment variable, uses it in its script and sets an output in its script.
* e.g.
@@ -65,27 +29,22 @@ private class ExternallyDefinedSummary extends AdditionalTaintStep {
* env:
* BODY: ${{ github.event.comment.body }}
* run: |
* INITIAL_URL=$(echo "$BODY" | grep -o 'https://github.com/github/release-assets/assets/[^ >]*')
* echo "Cleaned Initial URL: $INITIAL_URL"
* echo "::set-output name=initial_url::$INITIAL_URL"
* echo "::set-output name=foo::$BODY"
* echo "foo=$(echo $BODY)" >> $GITHUB_OUTPUT
* echo "foo=$(echo $BODY)" >> "$GITHUB_OUTPUT"
*/
private class RunEnvToScriptStep extends AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
runEnvToScriptstep(pred, succ)
}
}
predicate runEnvToScriptstep(DataFlow::Node pred, DataFlow::Node succ) {
exists(RunExpr r, string varName |
predicate runEnvToScriptStoreStep(DataFlow::Node pred, DataFlow::Node succ, DataFlow::ContentSet c) {
exists(RunExpr r, string varName, string output |
c = any(DataFlow::FieldContent ct | ct.getName() = output.replaceAll("output\\.", "")) and
r.getEnvExpr(varName) = pred.asExpr() and
exists(string script, string line |
script = r.getScript() and
line = script.splitAt("\n") and
(
line.regexpMatch(".*::set-output\\s+name.*") or
line.regexpMatch(".*>>\\s*\\$GITHUB_OUTPUT.*")
output = line.regexpCapture(".*::set-output\\s+name=(.*)::.*", 1) or
output = line.regexpCapture(".*echo\\s*\"(.*)=.*\\s*>>\\s*(\")?\\$GITHUB_OUTPUT.*", 1)
) and
script.indexOf("$" + ["", "{", "ENV{"] + varName) > 0
line.indexOf("$" + ["", "{", "ENV{"] + varName) > 0
) and
succ.asExpr() = r
)

View File

@@ -4,6 +4,8 @@ private import codeql.actions.Cfg as Cfg
private import codeql.Locations
private import codeql.actions.controlflow.BasicBlocks
private import DataFlowPublic
private import codeql.actions.dataflow.ExternalFlow
private import codeql.actions.dataflow.FlowSteps
cached
newtype TNode = TExprNode(DataFlowExpr e)
@@ -56,7 +58,7 @@ class DataFlowExpr extends Cfg::Node {
}
/**
* A call corresponds to a Uses steps where a 3rd party action or a reusable workflow gets called
* A call corresponds to a Uses steps where a 3rd party action or a reusable workflow get called
*/
class DataFlowCall instanceof Cfg::Node {
DataFlowCall() { super.getAstNode() instanceof UsesExpr }
@@ -129,25 +131,19 @@ predicate compatibleTypes(DataFlowType t1, DataFlowType t2) { t1 = t2 }
predicate typeStrongerThan(DataFlowType t1, DataFlowType t2) { none() }
private newtype TContent = TNoContent() { none() }
newtype TContent =
TFieldContent(string name) {
// We only use field flow for steps and jobs outputs, not for accessing other context fields such as env or inputs
name = any(StepsCtxAccessExpr a).getFieldName() or
name = any(NeedsCtxAccessExpr a).getFieldName() or
name = any(JobsCtxAccessExpr a).getFieldName()
}
class Content extends TContent {
/** Gets a textual representation of this element. */
string toString() { none() }
}
predicate forceHighPrecision(Content c) { c instanceof FieldContent }
predicate forceHighPrecision(Content c) { none() }
class ContentApprox = ContentSet;
newtype TContentSet = TNoContentSet() { none() }
private newtype TContentApprox = TNoContentApprox() { none() }
class ContentApprox extends TContentApprox {
/** Gets a textual representation of this element. */
string toString() { none() }
}
ContentApprox getContentApprox(Content c) { none() }
ContentApprox getContentApprox(Content c) { result = c }
/**
* Made a string to match the ArgumentPosition type.
@@ -168,12 +164,16 @@ class ArgumentPosition extends string {
predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { ppos = apos }
/**
* Holds if there is a local flow step between a ${{}} expression accesing a step output variable and the step output itself
* e.g. ${{ steps.step1.output.foo }}
* Holds if there is a local flow step between a ${{ steps.xxx.outputs.yyy }} expression accesing a step output field
* and the step output itself. But only for those cases where the step output is defined externally in a MaD Source
* specification. The reason for this is that we don't currently have a way to specify that a source starts with a
* non-empty access path so we cannot write a Source that stores the taint in a Content, we can only do that for steps
* (storeStep). The easiest thing is to add this local flow step that simulates a read step from the source node for a specific
* field name.
*/
predicate stepsCtxLocalStep(Node nodeFrom, Node nodeTo) {
exists(StepStmt astFrom, StepOutputAccessExpr astTo |
(astFrom instanceof UsesExpr or astFrom instanceof RunExpr) and
exists(UsesExpr astFrom, StepsCtxAccessExpr astTo |
externallyDefinedSource(nodeFrom, _, "output." + astTo.getFieldName()) and
astFrom = nodeFrom.asExpr() and
astTo = nodeTo.asExpr() and
astTo.getRefExpr() = astFrom
@@ -181,11 +181,16 @@ predicate stepsCtxLocalStep(Node nodeFrom, Node nodeTo) {
}
/**
* Holds if there is a local flow step between a ${{}} expression accesing a job output variable and the job output itself
* e.g. ${{ needs.job1.output.foo }} or ${{ job.job1.output.foo }}
* Holds if there is a local flow step between a ${{ needs.xxx.outputs.yyy }} expression accesing a job output field
* and the step output itself. But only for those cases where the job (needs) output is defined externally in a MaD Source
* specification. The reason for this is that we don't currently have a way to specify that a source starts with a
* non-empty access path so we cannot write a Source that stores the taint in a Content, we can only do that for steps
* (storeStep). The easiest thing is to add this local flow step that simulates a read step from the source node for a specific
* field name.
*/
predicate jobsCtxLocalStep(Node nodeFrom, Node nodeTo) {
exists(Expression astFrom, JobOutputAccessExpr astTo |
predicate needsCtxLocalStep(Node nodeFrom, Node nodeTo) {
exists(UsesExpr astFrom, NeedsCtxAccessExpr astTo |
externallyDefinedSource(nodeFrom, _, "output." + astTo.getFieldName()) and
astFrom = nodeFrom.asExpr() and
astTo = nodeTo.asExpr() and
astTo.getRefExpr() = astFrom
@@ -197,7 +202,7 @@ predicate jobsCtxLocalStep(Node nodeFrom, Node nodeTo) {
* e.g. ${{ inputs.foo }}
*/
predicate inputsCtxLocalStep(Node nodeFrom, Node nodeTo) {
exists(Expression astFrom, InputAccessExpr astTo |
exists(Expression astFrom, InputsCtxAccessExpr astTo |
astFrom = nodeFrom.asExpr() and
astTo = nodeTo.asExpr() and
astTo.getRefExpr() = astFrom
@@ -209,10 +214,13 @@ predicate inputsCtxLocalStep(Node nodeFrom, Node nodeTo) {
* e.g. ${{ env.foo }}
*/
predicate envCtxLocalStep(Node nodeFrom, Node nodeTo) {
exists(Expression astFrom, EnvAccessExpr astTo |
exists(Expression astFrom, EnvCtxAccessExpr astTo |
astFrom = nodeFrom.asExpr() and
astTo = nodeTo.asExpr() and
astTo.getRefExpr() = astFrom
(
externallyDefinedSource(nodeFrom, _, "env." + astTo.getFieldName()) or
astTo.getRefExpr() = astFrom
)
)
}
@@ -224,7 +232,7 @@ predicate envCtxLocalStep(Node nodeFrom, Node nodeTo) {
pragma[nomagic]
predicate localFlowStep(Node nodeFrom, Node nodeTo) {
stepsCtxLocalStep(nodeFrom, nodeTo) or
jobsCtxLocalStep(nodeFrom, nodeTo) or
needsCtxLocalStep(nodeFrom, nodeTo) or
inputsCtxLocalStep(nodeFrom, nodeTo) or
envCtxLocalStep(nodeFrom, nodeTo)
}
@@ -244,19 +252,53 @@ predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) { localFlowStep(nodeFr
*/
predicate jumpStep(Node nodeFrom, Node nodeTo) { none() }
/**
* Holds if a CtxAccessExpr reads a field from a job (needs/jobs), step (steps) output via a read of `c` (fieldname)
*/
predicate ctxFieldReadStep(Node node1, Node node2, ContentSet c) {
exists(CtxAccessExpr access |
(
access instanceof NeedsCtxAccessExpr or
access instanceof StepsCtxAccessExpr or
access instanceof JobsCtxAccessExpr
) and
c = any(FieldContent ct | ct.getName() = access.getFieldName()) and
node1.asExpr() = access.getRefExpr() and
node2.asExpr() = access
)
}
/**
* Holds if data can flow from `node1` to `node2` via a read of `c`. Thus,
* `node1` references an object with a content `c.getAReadContent()` whose
* value ends up in `node2`.
* Store steps without corresponding reads are pruned aggressively very early, since they can never contribute to a complete path.
*/
predicate readStep(Node node1, ContentSet c, Node node2) { none() }
predicate readStep(Node node1, ContentSet c, Node node2) { ctxFieldReadStep(node1, node2, c) }
/**
* Stores an output expression (node1) into its OutputsStm node (node2)
* using the output variable name as the access path
*/
predicate fieldStoreStep(Node node1, Node node2, ContentSet c) {
exists(OutputsStmt out, string fieldName |
node1.asExpr() = out.getOutputExpr(fieldName) and
node2.asExpr() = out and
c = any(FieldContent ct | ct.getName() = fieldName)
)
}
/**
* Holds if data can flow from `node1` to `node2` via a store into `c`. Thus,
* `node2` references an object with a content `c.getAStoreContent()` that
* contains the value of `node1`.
* Store steps without corresponding reads are pruned aggressively very early, since they can never contribute to a complete path.
*/
predicate storeStep(Node node1, ContentSet c, Node node2) { none() }
predicate storeStep(Node node1, ContentSet c, Node node2) {
fieldStoreStep(node1, node2, c) or
externallyDefinedStoreStep(node1, node2, c) or
runEnvToScriptStoreStep(node1, node2, c)
}
/**
* Holds if values stored inside content `c` are cleared at node `n`. For example,

View File

@@ -66,6 +66,17 @@ class ParameterNode extends ExprNode {
InputExpr getInputExpr() { result = input }
}
/**
* A call to a data flow callable (Uses).
*/
class CallNode extends ExprNode {
private DataFlowCall call;
CallNode() { this.getCfgNode() instanceof DataFlowCall }
string getCallee() { result = this.getCfgNode().(DataFlowCall).getName() }
}
/**
* An argument to a Uses step (call).
*/
@@ -83,18 +94,18 @@ class ArgumentNode extends ExprNode {
* Reusable workflow output nodes
*/
class ReturnNode extends ExprNode {
private OutputExpr output;
private OutputsStmt outputs;
ReturnNode() {
this.asExpr() = output and
output = any(OutputsStmt s).getOutputExpr(_)
this.asExpr() = outputs and
outputs = any(ReusableWorkflowStmt s).getOutputsStmt()
}
ReturnKind getKind() { result = TNormalReturn() }
override string toString() { result = "output " + output.toString() }
override string toString() { result = "output " + outputs.toString() }
override Location getLocation() { result = output.getLocation() }
override Location getLocation() { result = outputs.getLocation() }
}
/** Gets the node corresponding to `e`. */
@@ -106,13 +117,63 @@ Node exprNode(DataFlowExpr e) { result = TExprNode(e) }
* The set may be interpreted differently depending on whether it is
* stored into (`getAStoreContent`) or read from (`getAReadContent`).
*/
class ContentSet extends TContentSet {
/** Gets a textual representation of this element. */
string toString() { none() }
class ContentSet instanceof Content {
/** Gets a content that may be stored into when storing into this set. */
Content getAStoreContent() { none() }
Content getAStoreContent() { result = this }
/** Gets a content that may be read from when reading from this set. */
Content getAReadContent() { none() }
Content getAReadContent() { result = this }
/** Gets a textual representation of this content set. */
string toString() { result = super.toString() }
/**
* Holds if this element is at the specified location.
* The location spans column `startcolumn` of line `startline` to
* column `endcolumn` of line `endline` in file `filepath`.
* For more information, see
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
*/
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
super.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
}
/**
* A reference contained in an object. Examples include instance fields, the
* contents of a collection object, the contents of an array or pointer.
*/
class Content extends TContent {
/** Gets the type of the contained data for the purpose of type pruning. */
DataFlowType getType() { any() }
/** Gets a textual representation of this element. */
abstract string toString();
/**
* Holds if this element is at the specified location.
* The location spans column `startcolumn` of line `startline` to
* column `endcolumn` of line `endline` in file `filepath`.
* For more information, see
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
*/
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
filepath = "" and startline = 0 and startcolumn = 0 and endline = 0 and endcolumn = 0
}
}
/** A field of an object, for example an instance variable. */
class FieldContent extends Content, TFieldContent {
private string name;
FieldContent() { this = TFieldContent(name) }
/** Gets the name of the field. */
string getName() { result = name }
override string toString() { result = name }
}

View File

@@ -0,0 +1,7 @@
extensions:
- addsTo:
pack: githubsecuritylab/actions-all
extensible: sinkModel
data:
- ["","","",""]

View File

@@ -0,0 +1,17 @@
extensions:
- addsTo:
pack: githubsecuritylab/actions-all
extensible: summaryModel
data:
- ["octo-org/this-repo/.github/workflows/workflow.yml", "*", "input.config-path", "output.workflow-output", "taint"]
- ["octo-org/summary-repo/.github/workflows/workflow.yml", "*", "input.config-path", "output.workflow-output", "taint"]
- addsTo:
pack: githubsecuritylab/actions-all
extensible: sourceModel
data:
- ["octo-org/source-repo/.github/workflows/workflow.yml", "*", "output.workflow-output", "*", "Foo"]
- addsTo:
pack: githubsecuritylab/actions-all
extensible: sinkModel
data:
- ["octo-org/sink-repo/.github/workflows/workflow.yml", "*", "input.config-path", "expression-injection"]

View File

@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/actions-all
pack: githubsecuritylab/actions-all
extensible: sourceModel
data:
- ["ahmadnassri/action-changed-files", "*", "output.files", "pull_request", "PR changed files"]

View File

@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/actions-all
pack: githubsecuritylab/actions-all
extensible: sourceModel
data:
- ["dorny/paths-filter", "*", "output.changes", "pull_request", "PR changed files"]

View File

@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/actions-all
pack: githubsecuritylab/actions-all
extensible: summaryModel
data:
- ["frabert/replace-string-action", "*", "input.string", "output.replaced", "taint"]

View File

@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/actions-all
pack: githubsecuritylab/actions-all
extensible: sourceModel
data:
- ["jitterbit/get-changed-files", "*", "output.all", "pull_request", "PR changed files"]
@@ -16,4 +16,4 @@ extensions:
- ["jitterbit/get-changed-files", "*", "output.added_modified", "pull_request", "PR changed files"]
- ["jitterbit/get-changed-files", "*", "output.added_modified", "pull_request_target", "PR changed files"]
- ["jitterbit/get-changed-files", "*", "output.deleted", "pull_request", "PR changed files"]
- ["jitterbit/get-changed-files", "*", "output.deleted", "pull_request_target", "PR changed files"]
- ["jitterbit/get-changed-files", "*", "output.deleted", "pull_request_target", "PR changed files"]

View File

@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/actions-all
pack: githubsecuritylab/actions-all
extensible: summaryModel
data:
- ["mad9000/actions-find-and-replace-string", "*", "input.source", "output.value", "taint"]

View File

@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/actions-all
pack: githubsecuritylab/actions-all
extensible: sourceModel
data:
- ["tj-actions/changed-files", "*", "output.added_files", "pull_request", "PR changed files"]
@@ -36,4 +36,4 @@ extensions:
- ["tj-actions/changed-files", "*", "output.modified_keys", "pull_request", "PR changed files"]
- ["tj-actions/changed-files", "*", "output.modified_keys", "pull_request_target", "PR changed files"]
- ["tj-actions/changed-files", "*", "output.changed_keys", "pull_request", "PR changed files"]
- ["tj-actions/changed-files", "*", "output.changed_keys", "pull_request_target", "PR changed files"]
- ["tj-actions/changed-files", "*", "output.changed_keys", "pull_request_target", "PR changed files"]

View File

@@ -1,6 +1,6 @@
extensions:
- addsTo:
pack: codeql/actions-all
pack: githubsecuritylab/actions-all
extensible: sourceModel
data:
- ["tj-actions/verify-changed-files", "*", "output.changed-files", "pull_request", "PR changed files"]

View File

@@ -1,8 +1,8 @@
---
library: true
warnOnImplicitThis: true
name: codeql/actions-all
version: 0.0.1-dev
name: githubsecuritylab/actions-all
version: 0.0.1
dependencies:
codeql/controlflow: ^0.1.7
codeql/yaml: "*"

View File

@@ -43,14 +43,9 @@ query predicate nonOrphanVarAccesses(ExprAccessExpr va, string var, AstNode pare
query predicate parentNodes(AstNode child, AstNode parent) { child.getParentNode() = parent }
query predicate cfgNodes(Cfg::Node n) {
//any()
n.getAstNode() instanceof OutputsStmt
}
query predicate cfgNodes(Cfg::Node n) { any() }
query predicate dfNodes(DataFlow::Node e) {
e.getLocation().getFile().getBaseName() = "argus_case_study.yml"
}
query predicate dfNodes(DataFlow::Node e) { any() }
query predicate exprNodes(DataFlow::ExprNode e) { any() }
@@ -69,3 +64,7 @@ query predicate sources(string action, string version, string output, string tri
query predicate summaries(string action, string version, string input, string output, string kind) {
summaryModel(action, version, input, output, kind)
}
query predicate calls(DataFlow::CallNode call, string callee) { callee = call.getCallee() }
query predicate needs(DataFlow::ExprNode e) { e.asExpr() instanceof NeedsCtxAccessExpr }

View File

@@ -7,6 +7,7 @@
* @precision high
* @id actions/composite-action-summaries
* @tags actions
* model-generator
* external/cwe/cwe-020
*/
@@ -17,12 +18,10 @@ import codeql.actions.dataflow.ExternalFlow
private module MyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof DataFlow::ParameterNode and
exists(CompositeActionStmt c | c.getInputsStmt().getInputExpr(_) = source.asExpr())
}
predicate isSink(DataFlow::Node sink) {
sink instanceof DataFlow::ReturnNode and
exists(CompositeActionStmt c | c.getOutputsStmt().getOutputExpr(_) = sink.asExpr())
}
}
@@ -32,5 +31,7 @@ module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where MyFlow::flowPath(source, sink)
where
MyFlow::flowPath(source, sink) and
source.getNode().getLocation().getFile() = sink.getNode().getLocation().getFile()
select sink.getNode(), source, sink, "Summary"

View File

@@ -0,0 +1,42 @@
/**
* @name Composite Action Sinks
* @description Actions passing input variables to expression injection sinks.
* @kind path-problem
* @problem.severity warning
* @security-severity 9.3
* @precision high
* @id actions/composite-action-sinks
* @tags actions
* model-generator
* external/cwe/cwe-020
*/
import actions
import codeql.actions.TaintTracking
import codeql.actions.dataflow.FlowSources
import codeql.actions.dataflow.ExternalFlow
private class ExpressionInjectionSink extends DataFlow::Node {
ExpressionInjectionSink() {
exists(RunExpr e | e.getScriptExpr() = this.asExpr()) or
externallyDefinedSink(this, "expression-injection")
}
}
private module MyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(CompositeActionStmt c | c.getInputsStmt().getInputExpr(_) = source.asExpr())
}
predicate isSink(DataFlow::Node sink) { sink instanceof ExpressionInjectionSink }
}
module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where
MyFlow::flowPath(source, sink) and
source.getNode().getLocation().getFile() = sink.getNode().getLocation().getFile()
select sink.getNode(), source, sink, "Sink"

View File

@@ -7,6 +7,7 @@
* @precision high
* @id actions/composite-action-sources
* @tags actions
* model-generator
* external/cwe/cwe-020
*/
@@ -23,9 +24,15 @@ private module MyConfig implements DataFlow::ConfigSig {
}
predicate isSink(DataFlow::Node sink) {
sink instanceof DataFlow::ReturnNode and
exists(CompositeActionStmt c | c.getOutputsStmt().getOutputExpr(_) = sink.asExpr())
}
predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet set) {
allowImplicitRead(node, set)
or
isSink(node) and
set instanceof DataFlow::FieldContent
}
}
module MyFlow = TaintTracking::Global<MyConfig>;
@@ -33,5 +40,7 @@ module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where MyFlow::flowPath(source, sink)
where
MyFlow::flowPath(source, sink) and
source.getNode().getLocation().getFile() = sink.getNode().getLocation().getFile()
select sink.getNode(), source, sink, "Source"

View File

@@ -0,0 +1,42 @@
/**
* @name Reusable Workflow Sinks
* @description Reusable Workflows passing parameters to an expression injection sink.
* @kind path-problem
* @problem.severity warning
* @security-severity 9.3
* @precision high
* @id actions/reusable-wokflow-sinks
* @tags actions
* model-generator
* external/cwe/cwe-020
*/
import actions
import codeql.actions.TaintTracking
import codeql.actions.dataflow.FlowSources
import codeql.actions.dataflow.ExternalFlow
private class ExpressionInjectionSink extends DataFlow::Node {
ExpressionInjectionSink() {
exists(RunExpr e | e.getScriptExpr() = this.asExpr()) or
externallyDefinedSink(this, "expression-injection")
}
}
private module MyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(ReusableWorkflowStmt w | w.getInputsStmt().getInputExpr(_) = source.asExpr())
}
predicate isSink(DataFlow::Node sink) { sink instanceof ExpressionInjectionSink }
}
module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where
MyFlow::flowPath(source, sink) and
source.getNode().getLocation().getFile() = sink.getNode().getLocation().getFile()
select sink.getNode(), source, sink, "Sink"

View File

@@ -0,0 +1,46 @@
/**
* @name Reusable Workflow Sources
* @description Reusable Workflow that pass user-controlled data to their output variables.
* @kind path-problem
* @problem.severity warning
* @security-severity 9.3
* @precision high
* @id actions/reusable-workflow-sources
* @tags actions
* model-generator
* external/cwe/cwe-020
*/
import actions
import codeql.actions.TaintTracking
import codeql.actions.dataflow.FlowSources
import codeql.actions.dataflow.ExternalFlow
private module MyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource and
not source instanceof DataFlow::ParameterNode and
exists(ReusableWorkflowStmt w | w.getAChildNode*() = source.asExpr())
}
predicate isSink(DataFlow::Node sink) {
exists(ReusableWorkflowStmt w | w.getOutputsStmt().getOutputExpr(_) = sink.asExpr())
}
predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet set) {
allowImplicitRead(node, set)
or
isSink(node) and
set instanceof DataFlow::FieldContent
}
}
module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where
MyFlow::flowPath(source, sink) and
source.getNode().getLocation().getFile() = sink.getNode().getLocation().getFile()
select sink.getNode(), source, sink, "Source"

View File

@@ -0,0 +1,37 @@
/**
* @name Reusable Workflows Summaries
* @description Reusable workflow that pass user-controlled data to their output variables.
* @kind path-problem
* @problem.severity warning
* @security-severity 9.3
* @precision high
* @id actions/reusable-workflow-summaries
* @tags actions
* model-generator
* external/cwe/cwe-020
*/
import actions
import codeql.actions.TaintTracking
import codeql.actions.dataflow.FlowSources
import codeql.actions.dataflow.ExternalFlow
private module MyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(ReusableWorkflowStmt w | w.getInputsStmt().getInputExpr(_) = source.asExpr())
}
predicate isSink(DataFlow::Node sink) {
exists(ReusableWorkflowStmt w | w.getOutputsStmt().getOutputExpr(_) = sink.asExpr())
}
}
module MyFlow = TaintTracking::Global<MyConfig>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where
MyFlow::flowPath(source, sink) and
source.getNode().getLocation().getFile() = sink.getNode().getLocation().getFile()
select sink.getNode(), source, sink, "Summary"

View File

@@ -20,7 +20,7 @@ import codeql.actions.dataflow.ExternalFlow
private class ExpressionInjectionSink extends DataFlow::Node {
ExpressionInjectionSink() {
exists(RunExpr e | e.getScriptExpr() = this.asExpr()) or
sinkNode(this, "expression-injection")
externallyDefinedSink(this, "expression-injection")
}
}
@@ -37,5 +37,4 @@ import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where MyFlow::flowPath(source, sink)
select sink.getNode(), source, sink,
"Potential injection from the ${{ " + sink.getNode().asExpr().(ExprAccessExpr).getExpression() +
" }}, which may be controlled by an external user."
"Potential expression injection, which may be controlled by an external user."

View File

@@ -0,0 +1,19 @@
- description: Standard Code Scanning queries for Actions
- queries: .
- include:
kind:
- problem
- path-problem
tags contain:
- security
- maintainability
- include:
kind:
- diagnostic
- exclude:
tags contain:
- experimental
- testing

View File

@@ -0,0 +1,8 @@
- description: Queries to model composite actions
- queries: .
- include:
kind:
- path-problem
tags contain:
- model-generator

View File

@@ -1,6 +1,6 @@
---
library: false
name: codeql/actions-queries
name: githubsecuritylab/actions-queries
version: 0.0.1
groups:
- actions
@@ -9,6 +9,6 @@ suites: codeql-suites
extractor: yaml
defaultSuiteFile: codeql-suites/actions-code-scanning.qls
dependencies:
codeql/actions-all: ${workspace}
githubsecuritylab/actions-all: ${workspace}
warnOnImplicitThis: true
tests: test

View File

@@ -8,17 +8,20 @@ jobs:
uses: octo-org/this-repo/.github/workflows/reusable_workflow.yml@172239021f7ba04fe7327647b213799853a9eb89
with:
config-path: ${{ github.event.pull_request.head.ref }}
secrets: inherit
call2:
uses: ./.github/workflows/reusable_workflow.yml
with:
config-path: ${{ github.event.pull_request.head.ref }}
secrets: inherit
call3:
uses: octo-org/another-repo/.github/workflows/workflow.yml@v1
uses: octo-org/summary-repo/.github/workflows/workflow.yml@v1
with:
config-path: ${{ github.event.pull_request.head.ref }}
call4:
uses: octo-org/source-repo/.github/workflows/workflow.yml@v1
call5:
uses: octo-org/sink-repo/.github/workflows/workflow.yml@v1
with:
config-path: ${{ github.event.pull_request.head.ref }}
secrets: inherit
job1:
runs-on: ubuntu-latest
@@ -36,3 +39,8 @@ jobs:
needs: call3
steps:
- run: echo ${{ needs.call3.outputs.workflow-output }}
job4:
runs-on: ubuntu-latest
needs: call4
steps:
- run: echo ${{ needs.call4.outputs.workflow-output }}

View File

@@ -1,47 +0,0 @@
run-name: Cleanup ${{ github.head_ref }}
on:
pull_request_target:
types: labeled
paths:
- "images/**"
jobs:
clean_ci:
name: Clean CI runs
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: pwsh
run: |
$startDate = Get-Date -UFormat %s
$workflows = @("macos11", "macos12", "ubuntu2004", "ubuntu2204", "windows2019", "windows2022")
while ($true) {
$continue = $false
foreach ($wf in $workflows) {
$skippedCommand = "gh run list --workflow ${wf}.yml --branch ${{ github.event.pull_request.head.ref }} --repo ${{ github.repository }} --status skipped --json databaseId"
$skippedIds = Invoke-Expression -Command $skippedCommand | ConvertFrom-Json | ForEach-Object { $_.databaseId }
$skippedIds | ForEach-Object {
$deleteCommand = "gh run delete --repo ${{ github.repository }} $_"
Invoke-Expression -Command $deleteCommand
}
$pendingCommand = "gh run list --workflow ${wf}.yml --branch ${{ github.event.pull_request.head.ref }} --repo ${{ github.repository }} --status requested --json databaseId --template '{{ . | len }}'"
$pending = Invoke-Expression -Command $pendingCommand
if ($pending -gt 0) {
Write-Host "Pending for ${wf}.yml: $pending run(s)"
$continue = $true
}
}
if ($continue -eq $false) {
Write-Host "All done, exiting"
break
}
$curDate = Get-Date -UFormat %s
if (($curDate - $startDate) -gt 60) {
Write-Host "Reached timeout, exiting"
break
}
Write-Host "Waiting 5 seconds..."
Start-Sleep -Seconds 5

View File

@@ -14,42 +14,24 @@ jobs:
- name: Extract and Clean Initial URL
id: extract-url
env:
BODY: ${{ github.event.comment.body }}
run: |
INITIAL_URL=$(echo "${{ github.event.comment.body }}" | grep -o 'https://github.com/github/release-assets/assets/[^ >]*')
echo "Cleaned Initial URL: $INITIAL_URL"
echo "::set-output name=initial_url::$INITIAL_URL"
echo "::set-output name=initial_url::$BODY"
- name: Get Redirected URL with Debugging
id: curl
env:
INITIAL_URL: ${{ steps.extract-url.outputs.initial_url }}
run: |
REDIRECTED_URL=$(curl -L -o /dev/null -w %{url_effective} -sS "${{ steps.extract-url.outputs.initial_url }}")
echo "Curl Command Executed"
echo "Redirected URL: $REDIRECTED_URL"
echo "::set-output name=redirected_url::$REDIRECTED_URL"
echo "redirected_url=$(echo $INITIAL_URL)" >> $GITHUB_OUTPUT
- name: Trim URL after PNG
id: trim-url
env:
REDIRECTED_URL: ${{ steps.curl.outputs.redirected_url }}
run: |
TRIMMED_URL=$(echo "${{ steps.curl.outputs.redirected_url }}" | sed 's/\(.*\.png\).*/\1/')
echo "Trimmed URL: $TRIMMED_URL"
echo "::set-output name=trimmed_url::$TRIMMED_URL"
- name: Output Final Trimmed URL
run: |
echo "Final Trimmed Image URL: ${{ steps.trim-url.outputs.trimmed_url }}"
echo "trimmed_url=$(echo $REDIRECTED_URL)" >> "$GITHUB_OUTPUT"
- name: Update Comment with New URL
run: |
COMMENT_URL="${{ github.event.comment.url }}"
NEW_COMMENT_BODY="Use this link to include this asset in your changelog: ${{ steps.trim-url.outputs.trimmed_url }}"
ORIGINAL_COMMENT_BODY="${{ github.event.comment.body }}"
UPDATED_COMMENT="${ORIGINAL_COMMENT_BODY} 👀 ${NEW_COMMENT_BODY}"
PAYLOAD=$(jq -n --arg body "$UPDATED_COMMENT" '{"body": $body}')
curl -X PATCH \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"${COMMENT_URL}" \
-d "$PAYLOAD"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,61 +0,0 @@
name: Image URL Processing
on:
issue_comment:
types: [created]
jobs:
process-image-url:
runs-on: ubuntu-latest
if: contains(github.event.comment.body, 'https://github.com/github/release-assets/assets/')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Extract and Clean Initial URL
id: extract-url
env:
BODY: ${{ github.event.comment.body }}
run: |
INITIAL_URL=$(echo "$BODY" | grep -o 'https://github.com/github/release-assets/assets/[^ >]*')
echo "Cleaned Initial URL: $INITIAL_URL"
echo "::set-output name=initial_url::$INITIAL_URL"
- name: Get Redirected URL with Debugging
id: curl
env:
INITIAL_URL: ${{ steps.extract-url.outputs.initial_url }}
run: |
REDIRECTED_URL=$(curl -L -o /dev/null -w %{url_effective} -sS "$INITIAL_URL")
echo "Curl Command Executed"
echo "Redirected URL: $REDIRECTED_URL"
echo "::set-output name=redirected_url::$REDIRECTED_URL"
- name: Trim URL after PNG
id: trim-url
env:
REDIRECTED_URL: ${{ steps.curl.outputs.redirected_url }}
run: |
TRIMMED_URL=$(echo "$REDIRECTED_URL" | sed 's/\(.*\.png\).*/\1/')
echo "Trimmed URL: $TRIMMED_URL"
echo "::set-output name=trimmed_url::$TRIMMED_URL"
- name: Output Final Trimmed URL
run: |
echo "Final Trimmed Image URL: ${{ steps.trim-url.outputs.trimmed_url }}"
- name: Update Comment with New URL
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMENT_URL: ${{ github.event.comment.url }}
ORIGINAL_COMMENT_BODY: ${{ github.event.comment.body }}
run: |
NEW_COMMENT_BODY="Use this link to include this asset in your changelog: ${{ steps.trim-url.outputs.trimmed_url }}"
UPDATED_COMMENT="${ORIGINAL_COMMENT_BODY} 👀 ${NEW_COMMENT_BODY}"
PAYLOAD=$(jq -n --arg body "$UPDATED_COMMENT" '{"body": $body}')
curl -X PATCH \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"${COMMENT_URL}" \
-d "$PAYLOAD"

View File

@@ -1,27 +0,0 @@
name: Image URL Processing
on:
issue_comment:
types: [created]
jobs:
process-image-url:
runs-on: ubuntu-latest
if: contains(github.event.comment.body, 'https://github.com/github/release-assets/assets/')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Extract and Clean Initial URL
id: source
env:
BODY: ${{ github.event.comment.body }}
run: |
INITIAL_URL=$(echo "$BODY" | grep -o 'https://github.com/github/release-assets/assets/[^ >]*')
echo "Cleaned Initial URL: $INITIAL_URL"
echo "::set-output name=initial_url::$INITIAL_URL"
- name: Get Redirected URL with Debugging
id: sink
run: |
echo ${{ steps.source.outputs.initial_url }}

View File

@@ -5,12 +5,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: source
- id: summary
uses: mad9000/actions-find-and-replace-string@3
with:
source: ${{ github.event.head_commit.message }}
find: 'foo'
replace: ''
- id: sink
- id: flow
run: |
echo "${{steps.source.outputs.value}}"
echo "${{steps.summary.outputs.value}}"
- id: no-flow
run: |
echo "${{steps.summary.outputs.foo}}"

View File

@@ -33,4 +33,10 @@ jobs:
echo "$file was changed"
done
- name: List all changed files
id: no-flow
run: |
for file in ${{ steps.source.outputs.all_changed_files_count }}; do
echo "$file was changed"
done

View File

@@ -22,7 +22,9 @@ jobs:
run: |
Write-Output "::set-output name=MSG::$ENV{BODY}"
- id: step2
run: echo "test=${{steps.step1.outputs.MSG}}" >> "$GITHUB_OUTPUT"
env:
MSG: ${{steps.step1.outputs.MSG}}
run: echo "test=$MSG" >> "$GITHUB_OUTPUT"
job2:
runs-on: ubuntu-latest
@@ -32,5 +34,4 @@ jobs:
needs: job1
steps:
- env:
run: echo ${{needs.job1.outputs.job_output}}
- run: echo ${{needs.job1.outputs.job_output}}