Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions actions/extractor/tools/autobuild-impl.ps1
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# Note: We're adding the `reusable_workflows` subdirectories to proactively
# record workflows that were called cross-repo, check them out locally,
# and enable an interprocedural analysis across the workflow files.
# These workflows follow the convention `.github/reusable_workflows/<nwo>/*.ya?ml`
# Include downloaded external reusable workflows to enable interprocedural analysis.
# These workflows are stored below a generated UUID directory at the repository root.
$DefaultPathFilters = @(
'exclude:**/*',
'include:.github/workflows/*.yml',
'include:.github/workflows/*.yaml',
'include:.github/reusable_workflows/**/*.yml',
'include:.github/reusable_workflows/**/*.yaml',
'include:9466014afba34ef28239871ceabf4132/**/*.yml',
'include:9466014afba34ef28239871ceabf4132/**/*.yaml',
'include:**/action.yml',
'include:**/action.yaml'
)
Expand Down
10 changes: 4 additions & 6 deletions actions/extractor/tools/autobuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

set -eu

# Note: We're adding the `reusable_workflows` subdirectories to proactively
# record workflows that were called cross-repo, check them out locally,
# and enable an interprocedural analysis across the workflow files.
# These workflows follow the convention `.github/reusable_workflows/<nwo>/*.ya?ml`
# Include downloaded external reusable workflows to enable interprocedural analysis.
# These workflows are stored below a generated UUID directory at the repository root.
DEFAULT_PATH_FILTERS=$(cat << END
exclude:**/*
include:.github/workflows/*.yml
include:.github/workflows/*.yaml
include:.github/reusable_workflows/**/*.yml
include:.github/reusable_workflows/**/*.yaml
include:9466014afba34ef28239871ceabf4132/**/*.yml
include:9466014afba34ef28239871ceabf4132/**/*.yaml
include:**/action.yml
include:**/action.yaml
END
Expand Down
4 changes: 2 additions & 2 deletions actions/extractor/tools/baseline-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"paths": [
".github/workflows/*.yml",
".github/workflows/*.yaml",
".github/reusable_workflows/**/*.yml",
".github/reusable_workflows/**/*.yaml",
"9466014afba34ef28239871ceabf4132/**/*.yml",
"9466014afba34ef28239871ceabf4132/**/*.yaml",
"**/action.yml",
"**/action.yaml"
]
Expand Down
7 changes: 1 addition & 6 deletions actions/ql/lib/codeql/actions/Helper.qll
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,9 @@ string getRepoRoot() {
w.getLocation()
.getFile()
.getRelativePath()
.prefix(w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") + 1) and
// exclude workflow_enum reusable workflows directory root
not result.indexOf(".github/workflows/external/") > -1 and
not result.indexOf(".github/actions/external/") > -1
.prefix(w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") + 1)
or
not w.getLocation().getFile().getRelativePath().indexOf("/.github/workflows") > 0 and
not w.getLocation().getFile().getRelativePath().indexOf(".github/workflows/external/") > -1 and
not w.getLocation().getFile().getRelativePath().indexOf(".github/actions/external/") > -1 and
result = ""
)
}
Expand Down
173 changes: 162 additions & 11 deletions actions/ql/lib/codeql/actions/ast/internal/Ast.qll
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,14 @@ class ExpressionImpl extends AstNodeImpl, TExpressionNode {
}
}

bindingset[owner, repo, action_path]
private string externalCompositeActionName(string owner, string repo, string action_path) {
action_path.trim() = "" and result = owner.trim() + "/" + repo.trim()
or
not action_path.trim() = "" and
result = owner.trim() + "/" + repo.trim() + "/" + action_path.trim()
}

class CompositeActionImpl extends AstNodeImpl, TCompositeAction {
YamlMapping n;

Expand Down Expand Up @@ -415,7 +423,33 @@ class CompositeActionImpl extends AstNodeImpl, TCompositeAction {
)
}

/**
* Holds if this composite action is described by an external composite action model with the
* given repository, action, revision, and local path details.
*/
predicate hasExternalCompositeActionModel(
string owner, string repo, string action_path, string requested_ref, string resolved_commit_sha,
string local_path
) {
externalCompositeActionDataModel(owner, repo, action_path, requested_ref, resolved_commit_sha,
local_path) and
local_path.trim() = this.getLocation().getFile().getRelativePath()
}

/** Holds if this composite action was downloaded from an external repository. */
predicate isExternalCompositeAction() {
this.hasExternalCompositeActionModel(_, _, _, _, _, _)
or
this.getLocation().getFile().getRelativePath().matches("9466014afba34ef28239871ceabf4132/%")
}

string getResolvedPath() {
exists(string owner, string repo, string action_path, string requested_ref |
this.hasExternalCompositeActionModel(owner, repo, action_path, requested_ref, _, _) and
result = externalCompositeActionName(owner, repo, action_path) + "@" + requested_ref.trim()
)
or
not this.isExternalCompositeAction() and
result =
["", "./"] +
this.getLocation()
Expand All @@ -424,7 +458,6 @@ class CompositeActionImpl extends AstNodeImpl, TCompositeAction {
.replaceAll(getRepoRoot(), "")
.replaceAll("/action.yml", "")
.replaceAll("/action.yaml", "")
.replaceAll(".github/actions/external/", "")
}

private predicate hasExplicitSecretAccess() {
Expand Down Expand Up @@ -542,14 +575,36 @@ class ReusableWorkflowImpl extends AstNodeImpl, WorkflowImpl {
)
}

/**
* Holds if this reusable workflow is described by an external reusable workflow model with the
* given repository, workflow, revision, and local path details.
*/
predicate hasExternalReusableWorkflowModel(
string owner, string repo, string workflow_path, string requested_ref,
string resolved_commit_sha, string local_path
) {
externalReusableWorkflowDataModel(owner, repo, workflow_path, requested_ref,
resolved_commit_sha, local_path) and
local_path.trim() = this.getLocation().getFile().getRelativePath()
}

/** Holds if this reusable workflow was downloaded from an external repository. */
predicate isExternalReusableWorkflow() {
this.hasExternalReusableWorkflowModel(_, _, _, _, _, _)
or
this.getLocation().getFile().getRelativePath().matches("9466014afba34ef28239871ceabf4132/%") // root folder for external workflows and composite actions
}

string getResolvedPath() {
exists(string owner, string repo, string workflow_path, string requested_ref |
this.hasExternalReusableWorkflowModel(owner, repo, workflow_path, requested_ref, _, _) and
result =
owner.trim() + "/" + repo.trim() + "/" + workflow_path.trim() + "@" + requested_ref.trim()
)
or
not this.isExternalReusableWorkflow() and
result =
["", "./"] +
this.getLocation()
.getFile()
.getRelativePath()
.replaceAll(getRepoRoot(), "")
.replaceAll(".github/workflows/external/", "")
["", "./"] + this.getLocation().getFile().getRelativePath().replaceAll(getRepoRoot(), "")
}
}

Expand Down Expand Up @@ -1336,6 +1391,9 @@ class EnvImpl extends AstNodeImpl, TEnvNode {
abstract class UsesImpl extends AstNodeImpl {
abstract string getCallee();

/** Gets the canonical name used to resolve this `uses` element to its callable target. */
abstract string getCallableName();

abstract ScalarValueImpl getCalleeNode();

abstract string getVersion();
Expand Down Expand Up @@ -1374,6 +1432,63 @@ class UsesStepImpl extends StepImpl, UsesImpl {
else result = u.getValue()
}

private predicate isWorkspaceLocalCall() { u.getValue().matches(["./%", ".github/%"]) }

private predicate isSelfCall() { u.getValue().matches("$/%") }

private predicate isLocalCall() { this.isWorkspaceLocalCall() or this.isSelfCall() }

private predicate hasModeledExternalCallee() {
exists(string owner, string repo, string action_path, string requested_ref |
externalCompositeActionDataModel(owner, repo, action_path, requested_ref, _, _) and
this.getCallee() = externalCompositeActionName(owner, repo, action_path) and
this.getVersion() = requested_ref.trim()
)
}

private predicate hasExternalEnclosingCompositeAction() {
exists(CompositeActionImpl action |
action = this.getEnclosingCompositeAction() and action.isExternalCompositeAction()
)
}

private predicate hasModeledExternalEnclosingCompositeAction() {
exists(CompositeActionImpl action |
action = this.getEnclosingCompositeAction() and
action.hasExternalCompositeActionModel(_, _, _, _, _, _)
)
}

private string getSelfCallableName() {
exists(CompositeActionImpl action, string owner, string repo, string requested_ref |
action = this.getEnclosingCompositeAction() and
action.hasExternalCompositeActionModel(owner, repo, _, requested_ref, _, _) and
result =
externalCompositeActionName(owner, repo, this.getCallee().suffix(2)) + "@" +
requested_ref.trim()
)
or
not this.hasExternalEnclosingCompositeAction() and
result = this.getCallee().suffix(2)
}

override string getCallableName() {
this.isWorkspaceLocalCall() and
(
this.hasModeledExternalEnclosingCompositeAction()
or
not this.hasExternalEnclosingCompositeAction()
) and
result = this.getCallee()
or
this.isSelfCall() and
result = this.getSelfCallableName()
or
not this.isLocalCall() and
this.hasModeledExternalCallee() and
result = this.getCallee() + "@" + this.getVersion()
}

override ScalarValueImpl getCalleeNode() { result.getNode() = u }

/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
Expand All @@ -1388,27 +1503,63 @@ class UsesStepImpl extends StepImpl, UsesImpl {
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
* local repo: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
* local repo: ./.github/workflows/workflow-2.yml
* local repo: $/.github/workflows/workflow-2.yml
* remote repo: octo-org/another-repo/.github/workflows/workflow.yml@v1
*/
private string repoUsesParser() { result = "([^/]+)/([^/]+)/([^@]+)@(.+)" }

private string pathUsesParser() { result = "\\./(.+)" }

class ExternalJobImpl extends JobImpl, UsesImpl {
YamlScalar u;

ExternalJobImpl() { n.lookup("uses") = u }

override string getCallee() {
if u.getValue().matches("./%")
then result = u.getValue().regexpCapture(pathUsesParser(), 1)
if u.getValue().matches(["./%", "$/%"])
then result = u.getValue().suffix(2)
else
result =
u.getValue().regexpCapture(repoUsesParser(), 1) + "/" +
u.getValue().regexpCapture(repoUsesParser(), 2) + "/" +
u.getValue().regexpCapture(repoUsesParser(), 3)
}

private predicate isLocalCall() { u.getValue().matches(["./%", "$/%"]) }

private predicate hasExternalEnclosingWorkflow() {
exists(ReusableWorkflowImpl enclosing_workflow |
enclosing_workflow = this.getEnclosingWorkflow() and
enclosing_workflow.isExternalReusableWorkflow()
)
}

private predicate hasModeledExternalCallee() {
exists(string owner, string repo, string workflow_path, string requested_ref |
externalReusableWorkflowDataModel(owner, repo, workflow_path, requested_ref, _, _) and
this.getCallee() = owner.trim() + "/" + repo.trim() + "/" + workflow_path.trim() and
this.getVersion() = requested_ref.trim()
)
}

override string getCallableName() {
this.isLocalCall() and
exists(
ReusableWorkflowImpl enclosing_workflow, string owner, string repo, string requested_ref
|
enclosing_workflow = this.getEnclosingWorkflow() and
enclosing_workflow.hasExternalReusableWorkflowModel(owner, repo, _, requested_ref, _, _) and
result =
owner.trim() + "/" + repo.trim() + "/" + this.getCallee() + "@" + requested_ref.trim()
)
or
this.isLocalCall() and
not this.hasExternalEnclosingWorkflow() and
result = this.getCallee()
or
not this.isLocalCall() and
this.hasModeledExternalCallee() and
result = this.getCallee() + "@" + this.getVersion()
}

override ScalarValueImpl getCalleeNode() { result.getNode() = u }

/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
Expand Down
36 changes: 36 additions & 0 deletions actions/ql/lib/codeql/actions/config/Config.qll
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,42 @@ predicate workflowDataModel(
Extensions::workflowDataModel(path, trigger, job, secrets_source, permissions, runner)
}

/**
* MaD models for downloaded external reusable workflows
* Fields:
* - owner: Repository owner
* - repo: Repository name
* - workflow_path: Workflow path within the repository
* - requested_ref: Ref used by the caller
* - resolved_commit_sha: Commit to which the ref resolved
* - local_path: Downloaded workflow path within the analyzed repository
*/
predicate externalReusableWorkflowDataModel(
string owner, string repo, string workflow_path, string requested_ref, string resolved_commit_sha,
string local_path
) {
Extensions::externalReusableWorkflowDataModel(owner, repo, workflow_path, requested_ref,
resolved_commit_sha, local_path)
}

/**
* MaD models for downloaded external composite actions
* Fields:
* - owner: Repository owner
* - repo: Repository name
* - action_path: Action path within the repository
* - requested_ref: Ref used by the caller
* - resolved_commit_sha: Commit to which the ref resolved
* - local_path: Downloaded action metadata path within the analyzed repository
*/
predicate externalCompositeActionDataModel(
string owner, string repo, string action_path, string requested_ref, string resolved_commit_sha,
string local_path
) {
Extensions::externalCompositeActionDataModel(owner, repo, action_path, requested_ref,
resolved_commit_sha, local_path)
}

/**
* MaD models for repository details
* Fields:
Expand Down
16 changes: 16 additions & 0 deletions actions/ql/lib/codeql/actions/config/ConfigExtensions.qll
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ extensible predicate workflowDataModel(
string path, string trigger, string job, string secrets_source, string permissions, string runner
);

/**
* Holds if an external reusable workflow was downloaded for the given reference.
*/
extensible predicate externalReusableWorkflowDataModel(
string owner, string repo, string workflow_path, string requested_ref, string resolved_commit_sha,
string local_path
);

/**
* Holds if an external composite action was downloaded for the given reference.
*/
extensible predicate externalCompositeActionDataModel(
string owner, string repo, string action_path, string requested_ref, string resolved_commit_sha,
string local_path
);

/**
* Holds if repository data model exists for the given parameters.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class DataFlowCall instanceof Cfg::Node {
/** Gets a textual representation of this element. */
string toString() { result = super.toString() }

string getName() { result = super.getAstNode().(Uses).getCallee() }
string getName() { result = super.getAstNode().(UsesImpl).getCallableName() }

DataFlowCallable getEnclosingCallable() { result = super.getScope() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Event getRelevantCachePoisoningEventForSink(DataFlow::Node sink) {
// the workflow caller runs in the context of the default branch
result.getName() = "workflow_call" and
exists(ExternalJob caller |
caller.getCallee() = job.getLocation().getFile().getRelativePath() and
job.getEnclosingWorkflow().(ReusableWorkflow).getACaller() = caller and
runsOnDefaultBranch(caller.getATriggerEvent())
)
)
Expand Down
Loading
Loading