mirror of
https://github.com/github/codeql.git
synced 2026-05-04 13:15:21 +02:00
Merge branch 'master' into steps
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`. */
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
7
ql/lib/ext/PLACEHOLDER.model.yml
Normal file
7
ql/lib/ext/PLACEHOLDER.model.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: githubsecuritylab/actions-all
|
||||
extensible: sinkModel
|
||||
data:
|
||||
- ["","","",""]
|
||||
|
||||
17
ql/lib/ext/TEST-RW-MODELS.model.yml
Normal file
17
ql/lib/ext/TEST-RW-MODELS.model.yml
Normal 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"]
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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: "*"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
|
||||
42
ql/src/Security/CWE-020/CompositeActionsSinks.ql
Normal file
42
ql/src/Security/CWE-020/CompositeActionsSinks.ql
Normal 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"
|
||||
@@ -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"
|
||||
|
||||
42
ql/src/Security/CWE-020/ReusableWorkflowsSinks.ql
Normal file
42
ql/src/Security/CWE-020/ReusableWorkflowsSinks.ql
Normal 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"
|
||||
46
ql/src/Security/CWE-020/ReusableWorkflowsSources.ql
Normal file
46
ql/src/Security/CWE-020/ReusableWorkflowsSources.ql
Normal 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"
|
||||
37
ql/src/Security/CWE-020/ReusableWorkflowsSummaries.ql
Normal file
37
ql/src/Security/CWE-020/ReusableWorkflowsSummaries.ql
Normal 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"
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
8
ql/src/codeql-suites/actions-summaries-queries.qls
Normal file
8
ql/src/codeql-suites/actions-summaries-queries.qls
Normal file
@@ -0,0 +1,8 @@
|
||||
- description: Queries to model composite actions
|
||||
- queries: .
|
||||
|
||||
- include:
|
||||
kind:
|
||||
- path-problem
|
||||
tags contain:
|
||||
- model-generator
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
47
ql/src/test/.github/workflows/ci-cleanup.yml
vendored
47
ql/src/test/.github/workflows/ci-cleanup.yml
vendored
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
@@ -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 }}
|
||||
9
ql/src/test/.github/workflows/simple1.yml
vendored
9
ql/src/test/.github/workflows/simple1.yml
vendored
@@ -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}}"
|
||||
|
||||
6
ql/src/test/.github/workflows/simple2.yml
vendored
6
ql/src/test/.github/workflows/simple2.yml
vendored
@@ -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
|
||||
|
||||
|
||||
7
ql/src/test/.github/workflows/test.yml
vendored
7
ql/src/test/.github/workflows/test.yml
vendored
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user