Files
codeql/javascript/ql/lib/semmle/javascript/Actions.qll

377 lines
12 KiB
Plaintext

/**
* PENDING DEPRECATION. Models for GitHub Actions workflow files are part of the actions qlpack now.
*
* Libraries for modeling GitHub Actions workflow files written in YAML.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
*/
import javascript
/**
* Libraries for modeling GitHub Actions workflow files written in YAML.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
*/
module Actions {
/** A YAML node in a GitHub Actions workflow or a custom composite action file. */
private class Node extends YamlNode {
Node() {
exists(File f |
f = this.getLocation().getFile() and
(
f.getRelativePath().regexpMatch("(^|.*/)\\.github/workflows/.*\\.ya?ml$")
or
f.getBaseName() = ["action.yml", "action.yaml"]
)
)
}
}
/**
* A custom composite action. This is a mapping at the top level of an Actions YAML action file.
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions.
*/
class CompositeAction extends Node, YamlDocument, YamlMapping {
CompositeAction() {
this.getFile().getBaseName() = ["action.yml", "action.yaml"] and
this.lookup("runs").(YamlMapping).lookup("using").(YamlScalar).getValue() = "composite"
}
/** Gets the `runs` mapping. */
Runs getRuns() { result = this.lookup("runs") }
}
/**
* An `runs` mapping in a custom composite action YAML.
* See https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs
*/
class Runs extends StepsContainer {
CompositeAction action;
Runs() { action.lookup("runs") = this }
/** Gets the action that this `runs` mapping is in. */
CompositeAction getAction() { result = action }
/** Gets the `using` mapping. */
Using getUsing() { result = this.lookup("using") }
}
/**
* The parent class of the class that can contain `steps` mappings. (`Job` or `Runs` currently.)
*/
abstract class StepsContainer extends YamlNode, YamlMapping {
/** Gets the sequence of `steps` within this YAML node. */
YamlSequence getSteps() { result = this.lookup("steps") }
}
/**
* A `using` mapping in a custom composite action YAML.
*/
class Using extends YamlNode, YamlScalar {
Runs runs;
Using() { runs.lookup("using") = this }
/** Gets the `runs` mapping that this `using` mapping is in. */
Runs getRuns() { result = runs }
}
/**
* An Actions workflow. This is a mapping at the top level of an Actions YAML workflow file.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions.
*/
class Workflow extends Node, YamlDocument, YamlMapping {
/** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */
YamlMapping getJobs() { result = this.lookup("jobs") }
/** Gets the 'global' `env` mapping in this workflow. */
WorkflowEnv getEnv() { result = this.lookup("env") }
/** Gets the name of the workflow. */
string getName() { result = this.lookup("name").(YamlString).getValue() }
/** Gets the name of the workflow file. */
string getFileName() { result = this.getFile().getBaseName() }
/** Gets the `on:` in this workflow. */
On getOn() { result = this.lookup("on") }
/** Gets the job within this workflow with the given job ID. */
Job getJob(string jobId) { result.getWorkflow() = this and result.getId() = jobId }
}
/**
* An Actions On trigger within a workflow.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#on.
*/
class On extends YamlNode, YamlMappingLikeNode {
Workflow workflow;
On() { workflow.lookup("on") = this }
/** Gets the workflow that this trigger is in. */
Workflow getWorkflow() { result = workflow }
}
/** A common class for `env` in workflow, job or step. */
abstract class Env extends YamlNode, YamlMapping { }
/** A workflow level `env` mapping. */
class WorkflowEnv extends Env {
Workflow workflow;
WorkflowEnv() { workflow.lookup("env") = this }
/** Gets the workflow this field belongs to. */
Workflow getWorkflow() { result = workflow }
}
/** A job level `env` mapping. */
class JobEnv extends Env {
Job job;
JobEnv() { job.lookup("env") = this }
/** Gets the job this field belongs to. */
Job getJob() { result = job }
}
/** A step level `env` mapping. */
class StepEnv extends Env {
Step step;
StepEnv() { step.lookup("env") = this }
/** Gets the step this field belongs to. */
Step getStep() { result = step }
}
/**
* An Actions job within a workflow.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs.
*/
class Job extends StepsContainer {
string jobId;
Workflow workflow;
Job() { this = workflow.getJobs().lookup(jobId) }
/**
* Gets the ID of this job, as a string.
* This is the job's key within the `jobs` mapping.
*/
string getId() { result = jobId }
/**
* Gets the ID of this job, as a YAML scalar node.
* This is the job's key within the `jobs` mapping.
*/
YamlString getIdNode() { workflow.getJobs().maps(result, this) }
/** Gets the human-readable name of this job, if any, as a string. */
string getName() { result = this.getNameNode().getValue() }
/** Gets the human-readable name of this job, if any, as a YAML scalar node. */
YamlString getNameNode() { result = this.lookup("name") }
/** Gets the step at the given index within this job. */
Step getStep(int index) { result.getJob() = this and result.getIndex() = index }
/** Gets the `env` mapping in this job. */
JobEnv getEnv() { result = this.lookup("env") }
/** Gets the workflow this job belongs to. */
Workflow getWorkflow() { result = workflow }
/** Gets the value of the `if` field in this job, if any. */
JobIf getIf() { result.getJob() = this }
/** Gets the value of the `runs-on` field in this job. */
JobRunson getRunsOn() { result.getJob() = this }
}
/**
* An `if` within a job.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idif.
*/
class JobIf extends YamlNode, YamlScalar {
Job job;
JobIf() { job.lookup("if") = this }
/** Gets the step this field belongs to. */
Job getJob() { result = job }
}
/**
* A `runs-on` within a job.
* See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on.
*/
class JobRunson extends YamlNode, YamlScalar {
Job job;
JobRunson() { job.lookup("runs-on") = this }
/** Gets the step this field belongs to. */
Job getJob() { result = job }
}
/**
* A step within an Actions job.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idsteps.
*/
class Step extends YamlNode, YamlMapping {
int index;
StepsContainer parent;
Step() { this = parent.getSteps().getElement(index) }
/** Gets the 0-based position of this step within the sequence of `steps`. */
int getIndex() { result = index }
/** Gets the `job` this step belongs to, if the step belongs to a `job` in a workflow. Has no result if the step belongs to `runs` in a custom composite action. */
Job getJob() { result = parent }
/** Gets the `runs` this step belongs to, if the step belongs to a `runs` in a custom composite action. Has no result if the step belongs to a `job` in a workflow. */
Runs getRuns() { result = parent }
/** Gets the value of the `uses` field in this step, if any. */
Uses getUses() { result.getStep() = this }
/** Gets the value of the `run` field in this step, if any. */
Run getRun() { result.getStep() = this }
/** Gets the value of the `if` field in this step, if any. */
StepIf getIf() { result.getStep() = this }
/** Gets the value of the `env` field in this step, if any. */
StepEnv getEnv() { result = this.lookup("env") }
/** Gets the ID of this step, if any. */
string getId() { result = this.lookup("id").(YamlString).getValue() }
}
/**
* An `if` within a step.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsif.
*/
class StepIf extends YamlNode, YamlScalar {
Step step;
StepIf() { step.lookup("if") = this }
/** Gets the step this field belongs to. */
Step getStep() { result = step }
}
/**
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
* The capture groups are:
* 1: The owner of the repository where the Action comes from, e.g. `actions` in `actions/checkout@v2`
* 2: The name of the repository where the Action comes from, e.g. `checkout` in `actions/checkout@v2`.
* 3: The version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`.
*/
private string usesParser() { result = "([^/]+)/([^/@]+)@(.+)" }
/**
* A `uses` field within an Actions job step, which references an action as a reusable unit of code.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsuses.
*
* For example:
* ```
* uses: actions/checkout@v2
* ```
*
* Does not handle local repository references, e.g. `.github/actions/action-name`.
*/
class Uses extends YamlNode, YamlScalar {
Step step;
Uses() { step.lookup("uses") = this }
/** Gets the step this field belongs to. */
Step getStep() { result = step }
/** 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)
}
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
string getVersion() { result = this.getValue().regexpCapture(usesParser(), 3) }
}
/**
* A `with` field within an Actions job step, which references an action as a reusable unit of code.
* See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswith.
*
* For example:
* ```
* with:
* arg1: 1
* arg2: abc
* ```
*/
class With extends YamlNode, YamlMapping {
Step step;
With() { step.lookup("with") = this }
/** Gets the step this field belongs to. */
Step getStep() { result = step }
}
/**
* A `ref:` field within an Actions `with:` specific to `actions/checkout` action.
*
* For example:
* ```
* uses: actions/checkout@v2
* with:
* ref: ${{ github.event.pull_request.head.sha }}
* ```
*/
class Ref extends YamlNode, YamlString {
With with;
Ref() { with.lookup("ref") = this }
/** Gets the `with` field this field belongs to. */
With getWith() { result = with }
}
/**
* Holds if `${{ e }}` is a GitHub Actions expression evaluated within this YAML string.
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions.
* Only finds simple expressions like `${{ github.event.comment.body }}`, where the expression contains only alphanumeric characters, underscores, dots, or dashes.
* Does not identify more complicated expressions like `${{ fromJSON(env.time) }}`, or ${{ format('{{Hello {0}!}}', github.event.head_commit.author.name) }}
*/
string getASimpleReferenceExpression(YamlString node) {
// We use `regexpFind` to obtain *all* matches of `${{...}}`,
// not just the last (greedy match) or first (reluctant match).
result =
node.getValue()
.regexpFind("\\$\\{\\{\\s*[A-Za-z0-9_\\[\\]\\*\\(\\)\\.\\-]+\\s*\\}\\}", _, _)
.regexpCapture("\\$\\{\\{\\s*([A-Za-z0-9_\\[\\]\\*\\((\\)\\.\\-]+)\\s*\\}\\}", 1)
}
/** Extracts the 'name' part from env.name */
bindingset[name]
string getEnvName(string name) { result = name.regexpCapture("env\\.([A-Za-z0-9_]+)", 1) }
/**
* A `run` field within an Actions job step, which runs command-line programs using an operating system shell.
* See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
*/
class Run extends YamlNode, YamlString {
Step step;
Run() { step.lookup("run") = this }
/** Gets the step that executes this `run` command. */
Step getStep() { result = step }
}
}