diff --git a/actions/ql/consistency-queries/CfgConsistency.ql b/actions/ql/consistency-queries/CfgConsistency.ql new file mode 100644 index 000000000000..76d0384ce010 --- /dev/null +++ b/actions/ql/consistency-queries/CfgConsistency.ql @@ -0,0 +1,2 @@ +import codeql.actions.Cfg +import ControlFlow::Consistency diff --git a/actions/ql/consistency-queries/qlpack.yml b/actions/ql/consistency-queries/qlpack.yml new file mode 100644 index 000000000000..41594962c859 --- /dev/null +++ b/actions/ql/consistency-queries/qlpack.yml @@ -0,0 +1,5 @@ +name: codeql/actions-consistency-queries +groups: [actions, test, consistency-queries] +dependencies: + codeql/actions-all: ${workspace} +warnOnImplicitThis: true diff --git a/actions/ql/lib/change-notes/2026-09-29-shared-cfg.md b/actions/ql/lib/change-notes/2026-09-29-shared-cfg.md new file mode 100644 index 000000000000..dfab6367a8ce --- /dev/null +++ b/actions/ql/lib/change-notes/2026-09-29-shared-cfg.md @@ -0,0 +1,4 @@ +--- +category: breaking +--- +* The GitHub Actions control flow graph (CFG) now uses the shared CFG library. The CFG includes explicit before and after nodes and uses the shared entry and exit node representations. Existing code that relies on specific CFG nodes, edges, textual representations, or basic block boundaries may need to be updated. The legacy `Completion`, `NormalCompletion`, `SimpleCompletion`, `BooleanCompletion`, and `ReturnCompletion` classes have been removed because completions are no longer part of the Actions CFG API. Code that inspected completions should inspect CFG edge labels such as `DirectSuccessor`, `BooleanSuccessor`, and `ReturnSuccessor` instead. diff --git a/actions/ql/lib/codeql/actions/Cfg.qll b/actions/ql/lib/codeql/actions/Cfg.qll index 8ccc8de1d445..695dc55bd1ee 100644 --- a/actions/ql/lib/codeql/actions/Cfg.qll +++ b/actions/ql/lib/codeql/actions/Cfg.qll @@ -1,6 +1,3 @@ /** Provides classes representing the control flow graph. */ -private import codeql.actions.controlflow.internal.Cfg as CfgInternal -import CfgInternal::Completion -import CfgInternal::CfgScope -import CfgInternal::CfgImpl +import codeql.actions.controlflow.internal.Cfg::CfgImpl diff --git a/actions/ql/lib/codeql/actions/controlflow/BasicBlocks.qll b/actions/ql/lib/codeql/actions/controlflow/BasicBlocks.qll index 2dcfd81a47dc..5ca4f19eff62 100644 --- a/actions/ql/lib/codeql/actions/controlflow/BasicBlocks.qll +++ b/actions/ql/lib/codeql/actions/controlflow/BasicBlocks.qll @@ -1,379 +1,66 @@ /** Provides classes representing basic blocks. */ -private import codeql.actions.Cfg -private import codeql.actions.Ast -private import codeql.Locations +private import codeql.actions.Cfg as Cfg /** * A basic block, that is, a maximal straight-line sequence of control flow nodes * without branches or joins. */ -class BasicBlock extends TBasicBlockStart { - /** Gets the scope of this basic block. */ - final CfgScope getScope() { result = this.getFirstNode().getScope() } - +class BasicBlock extends Cfg::BasicBlock { /** Gets an immediate successor of this basic block, if any. */ - BasicBlock getASuccessor() { result = this.getASuccessor(_) } + BasicBlock getASuccessor() { result = super.getASuccessor() } /** Gets an immediate successor of this basic block of a given type, if any. */ - BasicBlock getASuccessor(SuccessorType t) { - result.getFirstNode() = this.getLastNode().getASuccessor(t) - } + BasicBlock getASuccessor(Cfg::SuccessorType t) { result = super.getASuccessor(t) } /** Gets an immediate predecessor of this basic block, if any. */ - BasicBlock getAPredecessor() { result.getASuccessor() = this } + BasicBlock getAPredecessor() { result = super.getAPredecessor() } /** Gets an immediate predecessor of this basic block of a given type, if any. */ - BasicBlock getAPredecessor(SuccessorType t) { result.getASuccessor(t) = this } + BasicBlock getAPredecessor(Cfg::SuccessorType t) { result = super.getAPredecessor(t) } /** Gets the control flow node at a specific (zero-indexed) position in this basic block. */ - Node getNode(int pos) { bbIndex(this.getFirstNode(), result, pos) } + Cfg::Node getNode(int pos) { result = super.getNode(pos) } /** Gets a control flow node in this basic block. */ - Node getANode() { result = this.getNode(_) } + Cfg::Node getANode() { result = super.getANode() } /** Gets the first control flow node in this basic block. */ - Node getFirstNode() { this = TBasicBlockStart(result) } + Cfg::Node getFirstNode() { result = super.getFirstNode() } /** Gets the last control flow node in this basic block. */ - Node getLastNode() { result = this.getNode(this.length() - 1) } - - /** Gets the length of this basic block. */ - int length() { result = strictcount(this.getANode()) } - - /** - * Holds if this basic block immediately dominates basic block `bb`. - * - * That is, all paths reaching basic block `bb` from some entry point - * basic block must go through this basic block (which is an immediate - * predecessor of `bb`). - * - * Example: - * - * ```rb - * def m b - * if b - * return 0 - * end - * return 1 - * end - * ``` - * - * The basic block starting on line 2 immediately dominates the - * basic block on line 5 (all paths from the entry point of `m` - * to `return 1` must go through the `if` block). - */ - predicate immediatelyDominates(BasicBlock bb) { bbIDominates(this, bb) } - - /** - * Holds if this basic block strictly dominates basic block `bb`. - * - * That is, all paths reaching basic block `bb` from some entry point - * basic block must go through this basic block (which must be different - * from `bb`). - * - * Example: - * - * ```rb - * def m b - * if b - * return 0 - * end - * return 1 - * end - * ``` - * - * The basic block starting on line 2 strictly dominates the - * basic block on line 5 (all paths from the entry point of `m` - * to `return 1` must go through the `if` block). - */ - predicate strictlyDominates(BasicBlock bb) { bbIDominates+(this, bb) } - - /** - * Holds if this basic block dominates basic block `bb`. - * - * That is, all paths reaching basic block `bb` from some entry point - * basic block must go through this basic block. - * - * Example: - * - * ```rb - * def m b - * if b - * return 0 - * end - * return 1 - * end - * ``` - * - * The basic block starting on line 2 dominates the basic - * basic block on line 5 (all paths from the entry point of `m` - * to `return 1` must go through the `if` block). - */ - predicate dominates(BasicBlock bb) { - bb = this or - this.strictlyDominates(bb) - } - - /** - * Holds if `df` is in the dominance frontier of this basic block. - * That is, this basic block dominates a predecessor of `df`, but - * does not dominate `df` itself. - * - * Example: - * - * ```rb - * def m x - * if x < 0 - * x = -x - * if x > 10 - * x = x - 1 - * end - * end - * puts x - * end - * ``` - * - * The basic block on line 8 is in the dominance frontier - * of the basic block starting on line 3 because that block - * dominates the basic block on line 4, which is a predecessor of - * `puts x`. Also, the basic block starting on line 3 does not - * dominate the basic block on line 8. - */ - predicate inDominanceFrontier(BasicBlock df) { - this.dominatesPredecessor(df) and - not this.strictlyDominates(df) - } + Cfg::Node getLastNode() { result = super.getLastNode() } - /** - * Holds if this basic block dominates a predecessor of `df`. - */ - private predicate dominatesPredecessor(BasicBlock df) { this.dominates(df.getAPredecessor()) } + predicate immediatelyDominates(BasicBlock bb) { super.immediatelyDominates(bb) } - /** - * Gets the basic block that immediately dominates this basic block, if any. - * - * That is, all paths reaching this basic block from some entry point - * basic block must go through the result, which is an immediate basic block - * predecessor of this basic block. - * - * Example: - * - * ```rb - * def m b - * if b - * return 0 - * end - * return 1 - * end - * ``` - * - * The basic block starting on line 2 is an immediate dominator of - * the basic block on line 5 (all paths from the entry point of `m` - * to `return 1` must go through the `if` block, and the `if` block - * is an immediate predecessor of `return 1`). - */ - BasicBlock getImmediateDominator() { bbIDominates(result, this) } + predicate strictlyDominates(BasicBlock bb) { super.strictlyDominates(bb) } - /** - * Holds if this basic block strictly post-dominates basic block `bb`. - * - * That is, all paths reaching a normal exit point basic block from basic - * block `bb` must go through this basic block (which must be different - * from `bb`). - * - * Example: - * - * ```rb - * def m b - * if b - * puts "b" - * end - * puts "m" - * end - * ``` - * - * The basic block on line 5 strictly post-dominates the basic block on - * line 3 (all paths to the exit point of `m` from `puts "b"` must go - * through `puts "m"`). - */ - predicate strictlyPostDominates(BasicBlock bb) { bbIPostDominates+(this, bb) } + predicate dominates(BasicBlock bb) { super.dominates(bb) } - /** - * Holds if this basic block post-dominates basic block `bb`. - * - * That is, all paths reaching a normal exit point basic block from basic - * block `bb` must go through this basic block. - * - * Example: - * - * ```rb - * def m b - * if b - * puts "b" - * end - * puts "m" - * end - * ``` - * - * The basic block on line 5 post-dominates the basic block on line 3 - * (all paths to the exit point of `m` from `puts "b"` must go through - * `puts "m"`). - */ - predicate postDominates(BasicBlock bb) { - this.strictlyPostDominates(bb) or - this = bb - } + predicate inDominanceFrontier(BasicBlock df) { super.inDominanceFrontier(df) } - /** Holds if this basic block is in a loop in the control flow graph. */ - predicate inLoop() { this.getASuccessor+() = this } + BasicBlock getImmediateDominator() { result = super.getImmediateDominator() } - /** Gets a textual representation of this basic block. */ - string toString() { result = this.getFirstNode().toString() } + predicate strictlyPostDominates(BasicBlock bb) { super.strictlyPostDominates(bb) } - /** Gets the location of this basic block. */ - Location getLocation() { result = this.getFirstNode().getLocation() } + predicate postDominates(BasicBlock bb) { super.postDominates(bb) } } -cached -private module Cached { - /** Internal representation of basic blocks. */ - cached - newtype TBasicBlock = TBasicBlockStart(Node cfn) { startsBB(cfn) } - - /** Holds if `cfn` starts a new basic block. */ - private predicate startsBB(Node cfn) { - not exists(cfn.getAPredecessor()) and exists(cfn.getASuccessor()) - or - cfn.isJoin() - or - cfn.getAPredecessor().isBranch() - or - /* - * In cases such as - * - * ```rb - * if x or y - * foo - * else - * bar - * ``` - * - * we have a CFG that looks like - * - * x --false--> [false] x or y --false--> bar - * \ | - * --true--> y --false-- - * \ - * --true--> [true] x or y --true--> foo - * - * and we want to ensure that both `foo` and `bar` start a new basic block, - * in order to get a `ConditionalBlock` out of the disjunction. - */ - - exists(cfn.getAPredecessor(any(BooleanSuccessor s))) - } - - /** - * Holds if `succ` is a control flow successor of `pred` within - * the same basic block. - */ - private predicate intraBBSucc(Node pred, Node succ) { - succ = pred.getASuccessor() and - not startsBB(succ) - } - - /** - * Holds if `cfn` is the `i`th node in basic block `bb`. - * - * In other words, `i` is the shortest distance from a node `bbStart` - * that starts a basic block to `cfn` along the `intraBBSucc` relation. - */ - cached - predicate bbIndex(Node bbStart, Node cfn, int i) = - shortestDistances(startsBB/1, intraBBSucc/2)(bbStart, cfn, i) - - /** - * Holds if the first node of basic block `succ` is a control flow - * successor of the last node of basic block `pred`. - */ - private predicate succBB(BasicBlock pred, BasicBlock succ) { succ = pred.getASuccessor() } - - /** Holds if `dom` is an immediate dominator of `bb`. */ - cached - predicate bbIDominates(BasicBlock dom, BasicBlock bb) = - idominance(entryBB/1, succBB/2)(_, dom, bb) - - /** Holds if `pred` is a basic block predecessor of `succ`. */ - private predicate predBB(BasicBlock succ, BasicBlock pred) { succBB(pred, succ) } - - /** Holds if `bb` is an exit basic block that represents normal exit. */ - private predicate normalExitBB(BasicBlock bb) { bb.getANode().(AnnotatedExitNode).isNormal() } - - /** Holds if `dom` is an immediate post-dominator of `bb`. */ - cached - predicate bbIPostDominates(BasicBlock dom, BasicBlock bb) = - idominance(normalExitBB/1, predBB/2)(_, dom, bb) - - /** - * Gets the `i`th predecessor of join block `jb`, with respect to some - * arbitrary order. - */ - cached - JoinBlockPredecessor getJoinBlockPredecessor(JoinBlock jb, int i) { - none() - /* - * result = - * rank[i + 1](JoinBlockPredecessor jbp | - * jbp = jb.getAPredecessor() - * | - * jbp order by JoinBlockPredecessors::getId(jbp), JoinBlockPredecessors::getSplitString(jbp) - * ) - */ - - } - - cached - predicate immediatelyControls(ConditionBlock cb, BasicBlock succ, BooleanSuccessor s) { - succ = cb.getASuccessor(s) and - forall(BasicBlock pred | pred = succ.getAPredecessor() and pred != cb | succ.dominates(pred)) - } - - cached - predicate controls(ConditionBlock cb, BasicBlock controlled, BooleanSuccessor s) { - exists(BasicBlock succ | cb.immediatelyControls(succ, s) | succ.dominates(controlled)) - } -} - -private import Cached - -/** Holds if `bb` is an entry basic block. */ -private predicate entryBB(BasicBlock bb) { bb.getFirstNode() instanceof EntryNode } - /** * An entry basic block, that is, a basic block whose first node is * an entry node. */ -class EntryBasicBlock extends BasicBlock { - EntryBasicBlock() { entryBB(this) } -} +class EntryBasicBlock extends BasicBlock, Cfg::EntryBasicBlock { } /** - * An annotated exit basic block, that is, a basic block whose last node is - * an annotated exit node. + * An annotated exit basic block, that is, a basic block that contains an + * annotated exit node. */ class AnnotatedExitBasicBlock extends BasicBlock { - private boolean normal; - - AnnotatedExitBasicBlock() { - exists(AnnotatedExitNode n | - n = this.getANode() and - if n.isNormal() then normal = true else normal = false - ) - } + AnnotatedExitBasicBlock() { this.getANode() instanceof Cfg::AnnotatedExitNode } - /** Holds if this block represent a normal exit. */ - final predicate isNormal() { normal = true } + /** Holds if this block represents a normal exit. */ + final predicate isNormal() { this.getANode() instanceof Cfg::NormalExitNode } } /** @@ -381,39 +68,18 @@ class AnnotatedExitBasicBlock extends BasicBlock { * an exit node. */ class ExitBasicBlock extends BasicBlock { - ExitBasicBlock() { this.getLastNode() instanceof ExitNode } + ExitBasicBlock() { this.getLastNode() instanceof Cfg::ExitNode } } -/* - * private module JoinBlockPredecessors { - * private predicate id(AstNode x, AstNode y) { x = y } - * - * private predicate idOf(AstNode x, int y) = equivalenceRelation(id/2)(x, y) - * - * int getId(JoinBlockPredecessor jbp) { - * idOf(Ast::toTreeSitter(jbp.getFirstNode().(AstCfgNode).getAstNode()), result) - * or - * idOf(Ast::toTreeSitter(jbp.(EntryBasicBlock).getScope()), result) - * } - * - * string getSplitString(JoinBlockPredecessor jbp) { - * result = jbp.getFirstNode().(AstCfgNode).getSplitsString() - * or - * not exists(jbp.getFirstNode().(AstCfgNode).getSplitsString()) and - * result = "" - * } - * } - */ - /** A basic block with more than one predecessor. */ class JoinBlock extends BasicBlock { - JoinBlock() { this.getFirstNode().isJoin() } + JoinBlock() { strictcount(this.getFirstNode().getAPredecessor()) > 1 } /** * Gets the `i`th predecessor of this join block, with respect to some * arbitrary order. */ - JoinBlockPredecessor getJoinBlockPredecessor(int i) { result = getJoinBlockPredecessor(this, i) } + JoinBlockPredecessor getJoinBlockPredecessor(int i) { none() } } /** A basic block that is an immediate predecessor of a join block. */ @@ -423,22 +89,24 @@ class JoinBlockPredecessor extends BasicBlock { /** A basic block that terminates in a condition, splitting the subsequent control flow. */ class ConditionBlock extends BasicBlock { - ConditionBlock() { this.getLastNode().isCondition() } + ConditionBlock() { + exists(this.getLastNode().getASuccessor(any(Cfg::BooleanSuccessor successor))) + } /** * Holds if basic block `succ` is immediately controlled by this basic - * block with conditional value `s`. That is, `succ` is an immediate - * successor of this block, and `succ` can only be reached from - * the callable entry point by going via the `s` edge out of this basic block. + * block with conditional value `s`. */ - predicate immediatelyControls(BasicBlock succ, BooleanSuccessor s) { - immediatelyControls(this, succ, s) + predicate immediatelyControls(BasicBlock succ, Cfg::BooleanSuccessor s) { + succ = this.getASuccessor(s) and + forall(BasicBlock pred | pred = succ.getAPredecessor() and pred != this | succ.dominates(pred)) } /** * Holds if basic block `controlled` is controlled by this basic block with - * conditional value `s`. That is, `controlled` can only be reached from - * the callable entry point by going via the `s` edge out of this basic block. + * conditional value `s`. */ - predicate controls(BasicBlock controlled, BooleanSuccessor s) { controls(this, controlled, s) } + predicate controls(BasicBlock controlled, Cfg::BooleanSuccessor s) { + exists(BasicBlock succ | this.immediatelyControls(succ, s) and succ.dominates(controlled)) + } } diff --git a/actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll b/actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll index 38ce9e7e03db..0adefae2762b 100644 --- a/actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll +++ b/actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll @@ -1,288 +1,489 @@ private import codeql.actions.Ast -private import codeql.controlflow.Cfg as CfgShared +private import codeql.controlflow.ControlFlowGraph as CfgShared private import codeql.Locations +private import codeql.util.Void -module Completion { - import codeql.controlflow.SuccessorType +private class ActionsAstNode = AstNode; - private newtype TCompletion = - TSimpleCompletion() or - TBooleanCompletion(boolean b) { b in [false, true] } or - TReturnCompletion() - - abstract class Completion extends TCompletion { - abstract string toString(); +module CfgImpl { + private predicate isDeclaredEnvExpr(AstNode parent, AstNode child) { + exists(Workflow workflow | parent = workflow and child = workflow.getEnv().getAnEnvVarExpr()) + or + exists(Job job | parent = job and child = job.getEnv().getAnEnvVarExpr()) + or + exists(Step step | parent = step and child = step.getEnv().getAnEnvVarExpr()) + } - predicate isValidForSpecific(AstNode e) { none() } + private predicate isCfgChild(AstNode parent, AstNode child) { + isDeclaredEnvExpr(parent, child) + or + exists(CompositeAction action | + parent = action and + (child = action.getAnInput() or child = action.getOutputs() or child = action.getRuns()) + ) + or + exists(ReusableWorkflow workflow | + parent = workflow and + ( + child = workflow.getAnInput() or + child = workflow.getOutputs() or + child = workflow.getStrategy() or + child = workflow.getAJob() + ) + ) + or + exists(Workflow workflow | + parent = workflow and + not workflow instanceof ReusableWorkflow and + (child = workflow.getStrategy() or child = workflow.getAJob()) + ) + or + exists(Runs runs | parent = runs and child = runs.getStep(_)) + or + exists(Outputs outputs | parent = outputs and child = outputs.getAnOutputExpr()) + or + exists(Strategy strategy | parent = strategy and child = strategy.getAMatrixVarExpr()) + or + exists(LocalJob job | + parent = job and + (child = job.getAStep() or child = job.getOutputs() or child = job.getStrategy()) + ) + or + exists(ExternalJob job | + parent = job and + ( + child = job.getArgumentExpr(_) or + child = job.getOutputs() or + child = job.getStrategy() + ) + ) + or + exists(UsesStep uses | parent = uses and child = uses.getArgumentExpr(_)) + or + exists(Run run | + parent = run and + (child = run.getAnScriptExpr() or child = run.getScript()) + ) + } - predicate isValidFor(AstNode e) { this.isValidForSpecific(e) } + private AstNode getCfgChild(AstNode parent, int index) { + result = + rank[index](AstNode child, Location l | + isCfgChild(parent, child) and l = child.getLocation() + | + child + order by + l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() + ) + } - abstract SuccessorType getAMatchingSuccessorType(); + private AstNode getLastCfgAstNode(AstNode node) { + not exists(getCfgChild(node, _)) and result = node + or + exists(AstNode child, int index | + child = getCfgChild(node, index) and + not exists(int later | later > index and exists(getCfgChild(node, later))) and + result = getLastCfgAstNode(child) + ) } - abstract class NormalCompletion extends Completion { } + private module CfgAst implements CfgShared::AstSig { + class AstNode = ActionsAstNode; + + AstNode getChild(AstNode node, int index) { result = getCfgChild(node, index) } + + class Callable extends AstNode { + Callable() { + this instanceof CompositeAction + or + this instanceof Workflow and + not exists(CompositeAction action | action.getLocation() = this.getLocation()) + } + } + + AstNode callableGetBody(Callable callable) { result = callable } + + /** + * Gets the unique callable containing `node`. + * + * An Actions AST node may have multiple parent paths, but they converge on + * the same root. + */ + Callable getEnclosingCallable(AstNode node) { + result = node + or + not node instanceof Callable and + result = getEnclosingCallable(node.getParentNode()) + } - class SimpleCompletion extends NormalCompletion, TSimpleCompletion { - override string toString() { result = "SimpleCompletion" } + class Parameter extends AstNode { + Parameter() { none() } - override predicate isValidFor(AstNode e) { not any(Completion c).isValidForSpecific(e) } + AstNode getPattern() { none() } - override DirectSuccessor getAMatchingSuccessorType() { any() } - } + Expr getDefaultValue() { none() } + } - class BooleanCompletion extends NormalCompletion, TBooleanCompletion { - boolean value; + Parameter callableGetParameter(Callable callable, int index) { none() } - BooleanCompletion() { this = TBooleanCompletion(value) } + class Stmt extends AstNode { + Stmt() { none() } + } - override string toString() { result = "BooleanCompletion(" + value + ")" } + class LabeledStmt extends Stmt { + LabeledStmt() { none() } - override predicate isValidForSpecific(AstNode e) { none() } + Stmt getStmt() { none() } + } - override BooleanSuccessor getAMatchingSuccessorType() { result.getValue() = value } + class Expr extends AstNode { + Expr() { none() } + } - final boolean getValue() { result = value } - } + class BlockStmt extends Stmt { + BlockStmt() { none() } - class ReturnCompletion extends Completion, TReturnCompletion { - override string toString() { result = "ReturnCompletion" } + Stmt getStmt(int index) { none() } - override predicate isValidForSpecific(AstNode e) { none() } + Stmt getLastStmt() { none() } + } - override ReturnSuccessor getAMatchingSuccessorType() { any() } - } -} + class ExprStmt extends Stmt { + ExprStmt() { none() } -module CfgScope { - abstract class CfgScope extends AstNode { } + Expr getExpr() { none() } + } - class WorkflowScope extends CfgScope instanceof Workflow { } + class IfStmt extends Stmt { + IfStmt() { none() } - class CompositeActionScope extends CfgScope instanceof CompositeAction { } -} + Expr getCondition() { none() } -private module Implementation implements CfgShared::InputSig { - import codeql.actions.Ast - import Completion - import CfgScope + Stmt getThen() { none() } - predicate completionIsNormal(Completion c) { not c instanceof ReturnCompletion } + Stmt getElse() { none() } + } - // Not using CFG splitting, so the following are just dummy types. - private newtype TUnit = Unit() + class LoopStmt extends Stmt { + LoopStmt() { none() } - additional class SplitKindBase = TUnit; + Stmt getBody() { none() } + } - additional class Split extends TUnit { - abstract string toString(); - } + class WhileStmt extends LoopStmt { + WhileStmt() { none() } - predicate completionIsSimple(Completion c) { c instanceof SimpleCompletion } + Expr getCondition() { none() } + } - predicate completionIsValidFor(Completion c, AstNode e) { c.isValidFor(e) } + class DoStmt extends LoopStmt { + DoStmt() { none() } - CfgScope getCfgScope(AstNode e) { - exists(AstNode p | p = e.getParentNode() | - result = p - or - not p instanceof CfgScope and result = getCfgScope(p) - ) - } + Expr getCondition() { none() } + } - additional int maxSplits() { result = 0 } + class UntilStmt extends LoopStmt { + UntilStmt() { none() } - predicate scopeFirst(CfgScope scope, AstNode e) { - first(scope.(Workflow), e) or - first(scope.(CompositeAction), e) - } + Expr getCondition() { none() } + } - predicate scopeLast(CfgScope scope, AstNode e, Completion c) { - last(scope.(Workflow), e, c) or - last(scope.(CompositeAction), e, c) - } + class ForStmt extends LoopStmt { + ForStmt() { none() } - SuccessorType getAMatchingSuccessorType(Completion c) { result = c.getAMatchingSuccessorType() } + AstNode getInit(int index) { none() } - int idOfAstNode(AstNode node) { none() } + Expr getCondition() { none() } - int idOfCfgScope(CfgScope scope) { none() } -} + AstNode getUpdate(int index) { none() } + } -module CfgImpl = CfgShared::Make; + class ForEachStmt extends LoopStmt { + ForEachStmt() { none() } -private import CfgImpl -private import Completion -private import CfgScope + Expr getVariable() { none() } -private class CompositeActionTree extends StandardPreOrderTree instanceof CompositeAction { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](AstNode child, Location l | - ( - child = this.(CompositeAction).getAnInput() or - child = this.(CompositeAction).getOutputs() or - child = this.(CompositeAction).getRuns() - ) and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) - } -} + Expr getCollection() { none() } + } -private class RunsTree extends StandardPreOrderTree instanceof Runs { - override ControlFlowTree getChildNode(int i) { result = super.getStep(i) } -} + class BreakStmt extends Stmt { + BreakStmt() { none() } + } -private class WorkflowTree extends StandardPreOrderTree instanceof Workflow { - override ControlFlowTree getChildNode(int i) { - if this instanceof ReusableWorkflow - then - result = - rank[i](AstNode child, Location l | - ( - child = this.(ReusableWorkflow).getAnInput() or - child = this.(ReusableWorkflow).getOutputs() or - child = this.(ReusableWorkflow).getStrategy() or - child = this.(ReusableWorkflow).getAJob() - ) and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) - else - result = - rank[i](AstNode child, Location l | - ( - child = super.getStrategy() or - child = super.getAJob() - ) and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) - } -} + class ContinueStmt extends Stmt { + ContinueStmt() { none() } + } -private class OutputsTree extends StandardPreOrderTree instanceof Outputs { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](AstNode child, Location l | - child = super.getAnOutputExpr() and l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) - } -} + class GotoStmt extends Stmt { + GotoStmt() { none() } + } -private class StrategyTree extends StandardPreOrderTree instanceof Strategy { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](AstNode child, Location l | - child = super.getAMatrixVarExpr() and l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) - } -} + class ReturnStmt extends Stmt { + ReturnStmt() { none() } -private class JobTree extends StandardPreOrderTree instanceof LocalJob { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](AstNode child, Location l | - ( - child = super.getAStep() or - child = super.getOutputs() or - child = super.getStrategy() - ) and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) + Expr getExpr() { none() } + } + + class Throw extends AstNode { + Throw() { none() } + + Expr getExpr() { none() } + } + + class TryStmt extends Stmt { + TryStmt() { none() } + + AstNode getBody(int index) { none() } + + CatchClause getCatch(int index) { none() } + + Stmt getFinally() { none() } + } + + class CatchClause extends AstNode { + CatchClause() { none() } + + AstNode getPattern() { none() } + + AstNode getVariable() { none() } + + Expr getCondition() { none() } + + Stmt getBody() { none() } + } + + class Switch extends AstNode { + Switch() { none() } + + Expr getExpr() { none() } + + Case getCase(int index) { none() } + + Stmt getStmt(int index) { none() } + } + + class Case extends AstNode { + Case() { none() } + + AstNode getPattern(int index) { none() } + + Expr getGuard() { none() } + + AstNode getBody() { none() } + } + + class DefaultCase extends Case { + DefaultCase() { none() } + } + + class ConditionalExpr extends Expr { + ConditionalExpr() { none() } + + Expr getCondition() { none() } + + Expr getThen() { none() } + + Expr getElse() { none() } + } + + class BinaryExpr extends Expr { + BinaryExpr() { none() } + + Expr getLeftOperand() { none() } + + Expr getRightOperand() { none() } + } + + class LogicalAndExpr extends BinaryExpr { + LogicalAndExpr() { none() } + } + + class LogicalOrExpr extends BinaryExpr { + LogicalOrExpr() { none() } + } + + class NullCoalescingExpr extends BinaryExpr { + NullCoalescingExpr() { none() } + } + + class UnaryExpr extends Expr { + UnaryExpr() { none() } + + Expr getOperand() { none() } + } + + class LogicalNotExpr extends UnaryExpr { + LogicalNotExpr() { none() } + } + + class Assignment extends BinaryExpr { + Assignment() { none() } + } + + class AssignExpr extends Assignment { + AssignExpr() { none() } + } + + class CompoundAssignment extends Assignment { + CompoundAssignment() { none() } + } + + class AssignLogicalAndExpr extends CompoundAssignment { + AssignLogicalAndExpr() { none() } + } + + class AssignLogicalOrExpr extends CompoundAssignment { + AssignLogicalOrExpr() { none() } + } + + class AssignNullCoalescingExpr extends CompoundAssignment { + AssignNullCoalescingExpr() { none() } + } + + class BooleanLiteral extends Expr { + BooleanLiteral() { none() } + + boolean getValue() { none() } + } + + class PatternMatchExpr extends Expr { + PatternMatchExpr() { none() } + + Expr getExpr() { none() } + + AstNode getPattern() { none() } + } } -} -private class ExternalJobTree extends StandardPreOrderTree instanceof ExternalJob { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](AstNode child, Location l | - ( - child = super.getArgumentExpr(_) or - child = super.getInScopeEnvVarExpr(_) or - child = super.getOutputs() or - child = super.getStrategy() - ) and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) + private module Cfg0 = CfgShared::Make0; + + private module Input1 implements Cfg0::InputSig1 { + predicate cfgCachedStageRef() { CfgCachedStage::ref() } + + class Label = Void; + + class CallableContext = Void; } -} -private class UsesTree extends StandardPreOrderTree instanceof UsesStep { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](AstNode child, Location l | - (child = super.getArgumentExpr(_) or child = super.getInScopeEnvVarExpr(_)) and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) + private module Cfg1 = Cfg0::Make1; + + private module Input2 implements Cfg1::InputSig2 { + predicate beginAbruptCompletion( + AstNode ast, PreControlFlowNode node, AbruptCompletion completion, boolean always + ) { + none() + } + + predicate endAbruptCompletion(AstNode ast, PreControlFlowNode node, AbruptCompletion completion) { + none() + } + + predicate step(PreControlFlowNode predecessor, PreControlFlowNode successor) { none() } } -} -private class RunTree extends StandardPreOrderTree instanceof Run { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](AstNode child, Location l | - ( - child = super.getInScopeEnvVarExpr(_) or - child = super.getAnScriptExpr() or - child = super.getScript() - ) and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) + private module Cfg2 = Cfg1::Make2; + + private import Cfg0 + private import Cfg1 + private import Cfg2 + import Public + import ControlFlow + + class CfgScope = CfgAst::Callable; + + /** A CFG scope for a workflow. */ + class WorkflowScope extends CfgScope instanceof Workflow { } + + /** A CFG scope for a composite action. */ + class CompositeActionScope extends CfgScope instanceof CompositeAction { } + + /** + * A control flow node. + * + * Only nodes that can be reached from an entry point are included in the CFG. + */ + class Node extends ControlFlowNode { + /** Gets the CFG scope containing this node. */ + CfgScope getScope() { result = this.getEnclosingCallable() } + + Node getASuccessor(SuccessorType type) { result = super.getASuccessor(type) } + + Node getASuccessor() { result = super.getASuccessor() } + + /** Gets an immediate predecessor connected by an edge of type `type`, if any. */ + Node getAPredecessor(SuccessorType type) { result.getASuccessor(type) = this } + + Node getAPredecessor() { result = super.getAPredecessor() } + + /** Holds if this node has a conditional successor. */ + predicate isCondition() { exists(this.getASuccessor(any(ConditionalSuccessor successor))) } + + /** Holds if this node has more than one predecessor. */ + predicate isJoin() { strictcount(this.getAPredecessor()) > 1 } + + /** Holds if this node has more than one successor. */ + predicate isBranch() { strictcount(this.getASuccessor()) > 1 } } -} -private class ScalarValueTree extends StandardPreOrderTree instanceof ScalarValue { - override ControlFlowTree getChildNode(int i) { - result = - rank[i](Expression child, Location l | - child = super.getAChildNode() and - l = child.getLocation() - | - child - order by - l.getStartLine(), l.getStartColumn(), l.getEndColumn(), l.getEndLine(), child.toString() - ) + /** The control flow node at the entry point of a scope. */ + class EntryNode extends Node, ControlFlow::EntryNode { } + + /** A control flow node indicating normal or exceptional termination of a scope. */ + class AnnotatedExitNode extends Node, ControlFlow::AnnotatedExitNode { + /** Holds if this node represents a normal exit. */ + predicate isNormal() { this instanceof NormalExitNode } } -} -private class UsesLeaf extends LeafTree instanceof Uses { } + /** A control flow node indicating normal termination of a scope. */ + class NormalExitNode extends AnnotatedExitNode, ControlFlow::NormalExitNode { } + + /** A control flow node indicating exceptional termination of a scope. */ + class ExceptionalExitNode extends AnnotatedExitNode, ControlFlow::ExceptionalExitNode { } -private class InputTree extends LeafTree instanceof Input { } + /** A control flow node indicating the termination of a scope. */ + class ExitNode extends Node, ControlFlow::ExitNode { } -private class ScalarValueLeaf extends LeafTree instanceof ScalarValue { } + /** The empty split type retained for compatibility with the legacy Actions CFG. */ + class Split = Void; -private class ExpressionLeaf extends LeafTree instanceof Expression { } + /** + * A node that uniquely represents an AST node. + * + * Unreachable AST nodes do not have an `AstCfgNode`. + */ + class AstCfgNode extends Node { + AstCfgNode() { this.injects(_) } + + AstNode getAstNode() { this.injects(result) } + + /** Gets a comma-separated list of splits in this node, if any. */ + string getSplitsString() { none() } + + /** Gets a split for this control flow node, if any. */ + Split getASplit() { none() } + } + + /** + * If needed, call this predicate to force a stage dependency on the cached CFG stage. + */ + cached + predicate forceCachingInSameStage() { CfgCachedStage::ref() } + + /** Gets the first AST node executed within `node`. */ + cached + AstNode getAControlFlowEntryNode(AstNode node) { + result = node and + exists(Node cfgNode | cfgNode.injects(node)) + } + + /** Gets a potential last AST node executed within `node`. */ + cached + AstNode getAControlFlowExitNode(AstNode node) { + exists(Node cfgNode | cfgNode.injects(node)) and + result = getLastCfgAstNode(node) + } + + /** Gets the CFG scope of `node`. */ + cached + CfgScope getNodeCfgScope(Node node) { result = node.getScope() } +} diff --git a/actions/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll b/actions/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll index cf95292588c3..084acb587768 100644 --- a/actions/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll +++ b/actions/ql/lib/codeql/actions/dataflow/internal/DataFlowPrivate.qll @@ -59,13 +59,16 @@ predicate nodeIsHidden(Node node) { none() } class DataFlowExpr extends Cfg::Node { DataFlowExpr() { - this.getAstNode() instanceof Job or - this.getAstNode() instanceof Expression or - this.getAstNode() instanceof Uses or - this.getAstNode() instanceof Run or - this.getAstNode() instanceof Outputs or - this.getAstNode() instanceof Input or - this.getAstNode() instanceof ScalarValue + this.injects(this.getAstNode()) and + ( + this.getAstNode() instanceof Job or + this.getAstNode() instanceof Expression or + this.getAstNode() instanceof Uses or + this.getAstNode() instanceof Run or + this.getAstNode() instanceof Outputs or + this.getAstNode() instanceof Input or + this.getAstNode() instanceof ScalarValue + ) } } @@ -73,7 +76,10 @@ class DataFlowExpr extends Cfg::Node { * A call corresponds to a Uses steps where a composite action or a reusable workflow get called */ class DataFlowCall instanceof Cfg::Node { - DataFlowCall() { super.getAstNode() instanceof Uses } + DataFlowCall() { + this.injects(this.getAstNode()) and + super.getAstNode() instanceof Uses + } /** Gets a textual representation of this element. */ string toString() { result = super.toString() } diff --git a/actions/ql/test/library-tests/basic/test.expected b/actions/ql/test/library-tests/basic/test.expected index 7c1c560c87e1..947b249f3791 100644 --- a/actions/ql/test/library-tests/basic/test.expected +++ b/actions/ql/test/library-tests/basic/test.expected @@ -931,254 +931,369 @@ parentNodes | .github/workflows/test.yml:40:14:40:52 | echo ${{needs.job1.outputs.job_output}} | .github/workflows/test.yml:39:9:40:53 | Run Step: sink | | .github/workflows/test.yml:40:20:40:53 | needs.job1.outputs.job_output | .github/workflows/test.yml:40:14:40:52 | echo ${{needs.job1.outputs.job_output}} | cfgNodes -| .github/workflows/commands.yml:1:1:39:30 | enter on: push | -| .github/workflows/commands.yml:1:1:39:30 | exit on: push | -| .github/workflows/commands.yml:1:1:39:30 | exit on: push (normal) | +| .github/workflows/commands.yml:1:1:39:30 | After on: push | +| .github/workflows/commands.yml:1:1:39:30 | Entry | +| .github/workflows/commands.yml:1:1:39:30 | Exit | +| .github/workflows/commands.yml:1:1:39:30 | Normal Exit | | .github/workflows/commands.yml:1:1:39:30 | on: push | +| .github/workflows/commands.yml:9:5:31:2 | After Job: local_commands | | .github/workflows/commands.yml:9:5:31:2 | Job: local_commands | +| .github/workflows/commands.yml:15:9:18:6 | After Run Step | | .github/workflows/commands.yml:15:9:18:6 | Run Step | | .github/workflows/commands.yml:16:14:17:30 | command1 ; command2\n | +| .github/workflows/commands.yml:18:9:20:6 | After Run Step | | .github/workflows/commands.yml:18:9:20:6 | Run Step | | .github/workflows/commands.yml:18:14:19:30 | command3 \| command4\n | +| .github/workflows/commands.yml:20:9:22:6 | After Run Step | | .github/workflows/commands.yml:20:9:22:6 | Run Step | | .github/workflows/commands.yml:20:14:21:33 | command5 "$(command6)"\n | +| .github/workflows/commands.yml:22:9:24:6 | After Run Step | | .github/workflows/commands.yml:22:9:24:6 | Run Step | | .github/workflows/commands.yml:22:14:23:31 | command7 && command8\n | +| .github/workflows/commands.yml:24:9:26:6 | After Run Step | | .github/workflows/commands.yml:24:9:26:6 | Run Step | | .github/workflows/commands.yml:24:14:25:32 | command9 \|\| command10\n | +| .github/workflows/commands.yml:26:9:28:6 | After Run Step | | .github/workflows/commands.yml:26:9:28:6 | Run Step | | .github/workflows/commands.yml:26:14:27:34 | command11 "`command12`"\n | +| .github/workflows/commands.yml:28:9:31:2 | After Run Step | | .github/workflows/commands.yml:28:9:31:2 | Run Step | | .github/workflows/commands.yml:28:14:29:50 | command13 "`command14` $(date \| wc -l)"\n | +| .github/workflows/commands.yml:32:5:39:30 | After Job: local_commands2 | | .github/workflows/commands.yml:32:5:39:30 | Job: local_commands2 | +| .github/workflows/commands.yml:34:9:37:6 | After Run Step | | .github/workflows/commands.yml:34:9:37:6 | Run Step | | .github/workflows/commands.yml:35:14:36:30 | command1 ; command2\n | +| .github/workflows/commands.yml:37:9:39:30 | After Run Step | | .github/workflows/commands.yml:37:9:39:30 | Run Step | | .github/workflows/commands.yml:38:14:39:30 | command3 \| command4\n | -| .github/workflows/controlcheck.yml:1:1:16:25 | enter on: | -| .github/workflows/controlcheck.yml:1:1:16:25 | exit on: | -| .github/workflows/controlcheck.yml:1:1:16:25 | exit on: (normal) | +| .github/workflows/controlcheck.yml:1:1:16:25 | After on: | +| .github/workflows/controlcheck.yml:1:1:16:25 | Entry | +| .github/workflows/controlcheck.yml:1:1:16:25 | Exit | +| .github/workflows/controlcheck.yml:1:1:16:25 | Normal Exit | | .github/workflows/controlcheck.yml:1:1:16:25 | on: | +| .github/workflows/controlcheck.yml:6:5:11:2 | After Job: test1 | | .github/workflows/controlcheck.yml:6:5:11:2 | Job: test1 | +| .github/workflows/controlcheck.yml:9:9:11:2 | After Run Step | | .github/workflows/controlcheck.yml:9:9:11:2 | Run Step | | .github/workflows/controlcheck.yml:10:14:10:25 | echo "test1" | +| .github/workflows/controlcheck.yml:12:5:16:25 | After Job: test2 | | .github/workflows/controlcheck.yml:12:5:16:25 | Job: test2 | +| .github/workflows/controlcheck.yml:15:9:16:25 | After Run Step | | .github/workflows/controlcheck.yml:15:9:16:25 | Run Step | | .github/workflows/controlcheck.yml:16:14:16:25 | echo "test2" | -| .github/workflows/expression_nodes.yml:1:1:21:47 | enter on: issue_comment | -| .github/workflows/expression_nodes.yml:1:1:21:47 | exit on: issue_comment | -| .github/workflows/expression_nodes.yml:1:1:21:47 | exit on: issue_comment (normal) | +| .github/workflows/expression_nodes.yml:1:1:21:47 | After on: issue_comment | +| .github/workflows/expression_nodes.yml:1:1:21:47 | Entry | +| .github/workflows/expression_nodes.yml:1:1:21:47 | Exit | +| .github/workflows/expression_nodes.yml:1:1:21:47 | Normal Exit | | .github/workflows/expression_nodes.yml:1:1:21:47 | on: issue_comment | +| .github/workflows/expression_nodes.yml:5:5:21:47 | After Job: echo-chamber | | .github/workflows/expression_nodes.yml:5:5:21:47 | Job: echo-chamber | +| .github/workflows/expression_nodes.yml:7:9:8:6 | After Run Step | | .github/workflows/expression_nodes.yml:7:9:8:6 | Run Step | | .github/workflows/expression_nodes.yml:7:14:7:58 | LINE 1echo '${{ github.event.comment.body }}' | | .github/workflows/expression_nodes.yml:7:27:7:58 | github.event.comment.body | +| .github/workflows/expression_nodes.yml:8:9:10:6 | After Run Step | | .github/workflows/expression_nodes.yml:8:9:10:6 | Run Step | | .github/workflows/expression_nodes.yml:8:14:9:57 | LINE 1 echo '${{ github.event.comment.body }}'\n | | .github/workflows/expression_nodes.yml:9:25:9:56 | github.event.comment.body | +| .github/workflows/expression_nodes.yml:10:9:13:6 | After Run Step | | .github/workflows/expression_nodes.yml:10:9:13:6 | Run Step | | .github/workflows/expression_nodes.yml:10:14:12:53 | LINE 1 echo '${{ github.event.comment.body }}'\nLINE 2 echo '${{github.event.issue.body}}'\n | | .github/workflows/expression_nodes.yml:11:25:11:56 | github.event.comment.body | | .github/workflows/expression_nodes.yml:12:24:12:51 | github.event.issue.body | +| .github/workflows/expression_nodes.yml:13:9:16:6 | After Run Step | | .github/workflows/expression_nodes.yml:13:9:16:6 | Run Step | | .github/workflows/expression_nodes.yml:13:14:15:46 | LINE 1 echo '${{ github.event.comment.body }}' echo '${{github.event.issue.body}}'\n | | .github/workflows/expression_nodes.yml:14:9:15:46 | github.event.comment.body | | .github/workflows/expression_nodes.yml:14:9:15:46 | github.event.issue.body | +| .github/workflows/expression_nodes.yml:16:9:20:6 | After Run Step | | .github/workflows/expression_nodes.yml:16:9:20:6 | Run Step | | .github/workflows/expression_nodes.yml:16:14:19:57 | LINE 1 echo '${{ github.event.comment.body }}'\nLINE 2 echo '${{github.event.issue.body}}'\nLINE 3 echo '${{ github.event.comment.body }}'\n | | .github/workflows/expression_nodes.yml:17:25:17:56 | github.event.comment.body | | .github/workflows/expression_nodes.yml:18:24:18:51 | github.event.issue.body | | .github/workflows/expression_nodes.yml:19:24:19:55 | github.event.comment.body | +| .github/workflows/expression_nodes.yml:20:9:21:47 | After Run Step | | .github/workflows/expression_nodes.yml:20:9:21:47 | Run Step | | .github/workflows/expression_nodes.yml:20:14:21:46 | LINE 1 echo '${{ github.event.comment.body }}' echo '${{github.event.issue.body}}' | | .github/workflows/expression_nodes.yml:20:14:21:46 | github.event.comment.body | | .github/workflows/expression_nodes.yml:20:14:21:46 | github.event.issue.body | -| .github/workflows/many_strings.yml:1:1:18:1211 | enter on: | -| .github/workflows/many_strings.yml:1:1:18:1211 | exit on: | -| .github/workflows/many_strings.yml:1:1:18:1211 | exit on: (normal) | +| .github/workflows/many_strings.yml:1:1:18:1211 | After on: | +| .github/workflows/many_strings.yml:1:1:18:1211 | Entry | +| .github/workflows/many_strings.yml:1:1:18:1211 | Exit | +| .github/workflows/many_strings.yml:1:1:18:1211 | Normal Exit | | .github/workflows/many_strings.yml:1:1:18:1211 | on: | +| .github/workflows/many_strings.yml:9:5:18:1211 | After Job: Test | | .github/workflows/many_strings.yml:9:5:18:1211 | Job: Test | +| .github/workflows/many_strings.yml:11:9:18:1211 | After Run Step | | .github/workflows/many_strings.yml:11:9:18:1211 | Run Step | | .github/workflows/many_strings.yml:11:14:18:1211 | # Avoid choking on large chunks of data containing quotes\necho '["string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10", "string11", "string12", "string13", "string14", "string15", "string16", "string17", "string18", "string19", "string20", "string21", "string22", "string23", "string24", "string25", "string26", "string27", "string28", "string29", "string30", "string31", "string32", "string33", "string34", "string35", "string36", "string37", "string38", "string39", "string40", "string41", "string42", "string43", "string44", "string45", "string46", "string47", "string48", "string49", "string50", "string51", "string52", "string53", "string54", "string55", "string56", "string57", "string58", "string59", "string60", "string61", "string62", "string63", "string64", "string65", "string66", "string67", "string68", "string69", "string70", "string71", "string72", "string73", "string74", "string75", "string76", "string77", "string78", "string79", "string80", "string81", "string82", "string83", "string84", "string85", "string86", "string87", "string88", "string89", "string90", "string91", "string92", "string93", "string94", "string95", "string96", "string97", "string98", "string99", "string100"]'\necho "['string1', 'string2', 'string3', 'string4', 'string5', 'string6', 'string7', 'string8', 'string9', 'string10', 'string11', 'string12', 'string13', 'string14', 'string15', 'string16', 'string17', 'string18', 'string19', 'string20', 'string21', 'string22', 'string23', 'string24', 'string25', 'string26', 'string27', 'string28', 'string29', 'string30', 'string31', 'string32', 'string33', 'string34', 'string35', 'string36', 'string37', 'string38', 'string39', 'string40', 'string41', 'string42', 'string43', 'string44', 'string45', 'string46', 'string47', 'string48', 'string49', 'string50', 'string51', 'string52', 'string53', 'string54', 'string55', 'string56', 'string57', 'string58', 'string59', 'string60', 'string61', 'string62', 'string63', 'string64', 'string65', 'string66', 'string67', 'string68', 'string69', 'string70', 'string71', 'string72', 'string73', 'string74', 'string75', 'string76', 'string77', 'string78', 'string79', 'string80', 'string81', 'string82', 'string83', 'string84', 'string85', 'string86', 'string87', 'string88', 'string89', 'string90', 'string91', 'string92', 'string93', 'string94', 'string95', 'string96', 'string97', 'string98', 'string99', 'string100']"\n\n# Same as above but where each line has an unbalanced internal quote near the end\necho '["string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10", "string11", "string12", "string13", "string14", "string15", "string16", "string17", "string18", "string19", "string20", "string21", "string22", "string23", "string24", "string25", "string26", "string27", "string28", "string29", "string30", "string31", "string32", "string33", "string34", "string35", "string36", "string37", "string38", "string39", "string40", "string41", "string42", "string43", "string44", "string45", "string46", "string47", "string48", "string49", "string50", "string51", "string52", "string53", "string54", "string55", "string56", "string57", "string58", "string59", "string60", "string61", "string62", "string63", "string64", "string65", "string66", "string67", "string68", "string69", "string70", "string71", "string72", "string73", "string74", "string75", "string76", "string77", "string78", "string79", "string80", "string81", "string82", "string83", "string84", "string85", "string86", "string87", "string88", "string89", "string90", "string91", "string92", "string93", "string94", "string95", "string96", "string97", "string98", "string99", "string100"]"'\necho "['string1', 'string2', 'string3', 'string4', 'string5', 'string6', 'string7', 'string8', 'string9', 'string10', 'string11', 'string12', 'string13', 'string14', 'string15', 'string16', 'string17', 'string18', 'string19', 'string20', 'string21', 'string22', 'string23', 'string24', 'string25', 'string26', 'string27', 'string28', 'string29', 'string30', 'string31', 'string32', 'string33', 'string34', 'string35', 'string36', 'string37', 'string38', 'string39', 'string40', 'string41', 'string42', 'string43', 'string44', 'string45', 'string46', 'string47', 'string48', 'string49', 'string50', 'string51', 'string52', 'string53', 'string54', 'string55', 'string56', 'string57', 'string58', 'string59', 'string60', 'string61', 'string62', 'string63', 'string64', 'string65', 'string66', 'string67', 'string68', 'string69', 'string70', 'string71', 'string72', 'string73', 'string74', 'string75', 'string76', 'string77', 'string78', 'string79', 'string80', 'string81', 'string82', 'string83', 'string84', 'string85', 'string86', 'string87', 'string88', 'string89', 'string90', 'string91', 'string92', 'string93', 'string94', 'string95', 'string96', 'string97', 'string98', 'string99', 'string100']'"\n | -| .github/workflows/multiline2.yml:1:1:89:35 | enter on: | -| .github/workflows/multiline2.yml:1:1:89:35 | exit on: | -| .github/workflows/multiline2.yml:1:1:89:35 | exit on: (normal) | +| .github/workflows/multiline2.yml:1:1:89:35 | After on: | +| .github/workflows/multiline2.yml:1:1:89:35 | Entry | +| .github/workflows/multiline2.yml:1:1:89:35 | Exit | +| .github/workflows/multiline2.yml:1:1:89:35 | Normal Exit | | .github/workflows/multiline2.yml:1:1:89:35 | on: | +| .github/workflows/multiline2.yml:9:5:89:35 | After Job: Test | | .github/workflows/multiline2.yml:9:5:89:35 | Job: Test | +| .github/workflows/multiline2.yml:11:9:15:6 | After Run Step | | .github/workflows/multiline2.yml:11:9:15:6 | Run Step | | .github/workflows/multiline2.yml:11:14:14:54 | echo "changelog< event.json\n ${{ toJson(github.event) }}\nEOF\n | | .github/workflows/multiline2.yml:32:13:32:39 | toJson(github.event) | +| .github/workflows/multiline2.yml:34:9:40:6 | After Run Step | | .github/workflows/multiline2.yml:34:9:40:6 | Run Step | | .github/workflows/multiline2.yml:35:14:39:14 | cat \| tee -a $GITHUB_ENV << EOL\n${ISSUE_BODY}\nFOO\nEOL\n | +| .github/workflows/multiline2.yml:40:9:46:6 | After Run Step | | .github/workflows/multiline2.yml:40:9:46:6 | Run Step | | .github/workflows/multiline2.yml:41:14:45:14 | cat > issue.txt << EOL\n${ISSUE_BODY}\nFOO\nEOL\n | +| .github/workflows/multiline2.yml:46:9:52:6 | After Run Step | | .github/workflows/multiline2.yml:46:9:52:6 | Run Step | | .github/workflows/multiline2.yml:47:14:51:14 | cat << EOL \| tee -a $GITHUB_ENV\n${ISSUE_BODY}\nFOO\nEOL\n | +| .github/workflows/multiline2.yml:52:9:58:6 | After Run Step | | .github/workflows/multiline2.yml:52:9:58:6 | Run Step | | .github/workflows/multiline2.yml:53:14:57:14 | cat < file.txt\nHello\nWorld\nEOF\n | +| .github/workflows/multiline2.yml:58:9:63:6 | After Run Step | | .github/workflows/multiline2.yml:58:9:63:6 | Run Step | | .github/workflows/multiline2.yml:59:14:62:14 | cat <<-EOF \| tee -a "$GITHUB_ENV"\necho "FOO=$TITLE"\nEOF\n | +| .github/workflows/multiline2.yml:63:9:66:6 | After Run Step | | .github/workflows/multiline2.yml:63:9:66:6 | Run Step | | .github/workflows/multiline2.yml:64:14:65:142 | echo REPO_NAME=$(cat issue.txt \| sed 's/\\\\r/\\\\n/g' \| grep -ioE '\\\\s*[a-z0-9_-]+/[a-z0-9_-]+\\\\s*$' \| tr -d ' ') \| tee -a $GITHUB_ENV\n | +| .github/workflows/multiline2.yml:66:9:71:6 | After Run Step | | .github/workflows/multiline2.yml:66:9:71:6 | Run Step | | .github/workflows/multiline2.yml:67:14:70:42 | echo "PR_TITLE<> $GITHUB_OUTPUT\necho -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT\necho "CHANGELOGEOF" >> $GITHUB_OUTPUT\n | +| .github/workflows/multiline.yml:15:9:20:6 | After Run Step | | .github/workflows/multiline.yml:15:9:20:6 | Run Step | | .github/workflows/multiline.yml:15:14:19:40 | EOF=$(dd if=/dev/urandom bs=15 count=1 status=none \| base64)\necho "status<<$EOF" >> $GITHUB_OUTPUT\necho "$(cat status.output.json)" >> $GITHUB_OUTPUT\necho "$EOF" >> $GITHUB_OUTPUT\n | +| .github/workflows/multiline.yml:20:9:24:6 | After Run Step | | .github/workflows/multiline.yml:20:9:24:6 | Run Step | | .github/workflows/multiline.yml:20:14:23:40 | echo "response<<$EOF" >> $GITHUB_OUTPUT\necho $output >> $GITHUB_OUTPUT\necho "$EOF" >> $GITHUB_OUTPUT\n | +| .github/workflows/multiline.yml:24:9:30:6 | After Run Step | | .github/workflows/multiline.yml:24:9:30:6 | Run Step | | .github/workflows/multiline.yml:24:14:29:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | +| .github/workflows/multiline.yml:30:9:34:6 | After Run Step | | .github/workflows/multiline.yml:30:9:34:6 | Run Step | | .github/workflows/multiline.yml:30:14:33:14 | cat <<-"EOF" > event.json\n ${{ toJson(github.event) }}\nEOF\n | | .github/workflows/multiline.yml:32:13:32:39 | toJson(github.event) | +| .github/workflows/multiline.yml:34:9:40:6 | After Run Step | | .github/workflows/multiline.yml:34:9:40:6 | Run Step | | .github/workflows/multiline.yml:35:14:39:14 | cat >> $GITHUB_ENV << EOL\n${ISSUE_BODY}\nFOO\nEOL\n | +| .github/workflows/multiline.yml:40:9:46:6 | After Run Step | | .github/workflows/multiline.yml:40:9:46:6 | Run Step | | .github/workflows/multiline.yml:41:14:45:14 | cat > issue.txt << EOL\n${ISSUE_BODY}\nFOO\nEOL\n | +| .github/workflows/multiline.yml:46:9:52:6 | After Run Step | | .github/workflows/multiline.yml:46:9:52:6 | Run Step | | .github/workflows/multiline.yml:47:14:51:14 | cat << EOL >> $GITHUB_ENV\n${ISSUE_BODY}\nFOO\nEOL\n | +| .github/workflows/multiline.yml:52:9:58:6 | After Run Step | | .github/workflows/multiline.yml:52:9:58:6 | Run Step | | .github/workflows/multiline.yml:53:14:57:14 | cat < file.txt\nHello\nWorld\nEOF\n | +| .github/workflows/multiline.yml:58:9:63:6 | After Run Step | | .github/workflows/multiline.yml:58:9:63:6 | Run Step | | .github/workflows/multiline.yml:59:14:62:14 | cat <<-EOF >> "$GITHUB_ENV"\necho "FOO=$TITLE"\nEOF\n | +| .github/workflows/multiline.yml:63:9:66:6 | After Run Step | | .github/workflows/multiline.yml:63:9:66:6 | Run Step | | .github/workflows/multiline.yml:64:14:65:136 | echo REPO_NAME=$(cat issue.txt \| sed 's/\\\\r/\\\\n/g' \| grep -ioE '\\\\s*[a-z0-9_-]+/[a-z0-9_-]+\\\\s*$' \| tr -d ' ') >> $GITHUB_ENV\n | +| .github/workflows/multiline.yml:66:9:71:6 | After Run Step | | .github/workflows/multiline.yml:66:9:71:6 | Run Step | | .github/workflows/multiline.yml:67:14:70:36 | echo "PR_TITLE<> $GITHUB_ENV\necho "$TITLE" >> $GITHUB_ENV\necho "EOF" >> $GITHUB_ENV\n | +| .github/workflows/multiline.yml:71:9:78:6 | After Run Step | | .github/workflows/multiline.yml:71:9:78:6 | Run Step | | .github/workflows/multiline.yml:72:14:77:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | +| .github/workflows/multiline.yml:78:9:85:6 | After Run Step | | .github/workflows/multiline.yml:78:9:85:6 | Run Step | | .github/workflows/multiline.yml:79:14:84:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | +| .github/workflows/multiline.yml:85:9:89:29 | After Run Step | | .github/workflows/multiline.yml:85:9:89:29 | Run Step | | .github/workflows/multiline.yml:86:14:89:29 | {\n echo 'JSON_RESPONSE<> "$GITHUB_ENV"\n | -| .github/workflows/poisonable_steps.yml:1:1:46:111 | enter on: push | -| .github/workflows/poisonable_steps.yml:1:1:46:111 | exit on: push | -| .github/workflows/poisonable_steps.yml:1:1:46:111 | exit on: push (normal) | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | After on: push | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | Entry | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | Exit | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | Normal Exit | | .github/workflows/poisonable_steps.yml:1:1:46:111 | on: push | +| .github/workflows/poisonable_steps.yml:5:5:46:111 | After Job: local_commands | | .github/workflows/poisonable_steps.yml:5:5:46:111 | Job: local_commands | +| .github/workflows/poisonable_steps.yml:7:9:8:6 | After Run Step | | .github/workflows/poisonable_steps.yml:7:9:8:6 | Run Step | | .github/workflows/poisonable_steps.yml:7:14:7:30 | venv/bin/activate | +| .github/workflows/poisonable_steps.yml:8:9:13:6 | After Uses Step | | .github/workflows/poisonable_steps.yml:8:9:13:6 | Uses Step | | .github/workflows/poisonable_steps.yml:11:53:11:75 | github.workspace | +| .github/workflows/poisonable_steps.yml:13:9:14:6 | After Run Step | | .github/workflows/poisonable_steps.yml:13:9:14:6 | Run Step | | .github/workflows/poisonable_steps.yml:13:14:13:32 | . venv/bin/activate | +| .github/workflows/poisonable_steps.yml:14:9:15:6 | After Run Step | | .github/workflows/poisonable_steps.yml:14:9:15:6 | Run Step | | .github/workflows/poisonable_steps.yml:14:14:14:42 | echo foo; . venv/bin/activate | +| .github/workflows/poisonable_steps.yml:15:9:16:6 | After Run Step | | .github/workflows/poisonable_steps.yml:15:9:16:6 | Run Step | | .github/workflows/poisonable_steps.yml:15:14:15:41 | echo foo;. venv/bin/activate | +| .github/workflows/poisonable_steps.yml:16:9:17:6 | After Run Step | | .github/workflows/poisonable_steps.yml:16:9:17:6 | Run Step | | .github/workflows/poisonable_steps.yml:16:14:16:42 | echo foo \|. venv/bin/activate | +| .github/workflows/poisonable_steps.yml:17:9:18:6 | After Run Step | | .github/workflows/poisonable_steps.yml:17:9:18:6 | Run Step | | .github/workflows/poisonable_steps.yml:17:14:17:32 | ./venv/bin/activate | +| .github/workflows/poisonable_steps.yml:18:9:19:6 | After Run Step | | .github/workflows/poisonable_steps.yml:18:9:19:6 | Run Step | | .github/workflows/poisonable_steps.yml:18:14:18:36 | sh venv/bin/activate.sh | +| .github/workflows/poisonable_steps.yml:19:9:20:6 | After Run Step | | .github/workflows/poisonable_steps.yml:19:9:20:6 | Run Step | | .github/workflows/poisonable_steps.yml:19:14:19:44 | echo $(sh venv/bin/activate.sh) | +| .github/workflows/poisonable_steps.yml:20:9:21:6 | After Run Step | | .github/workflows/poisonable_steps.yml:20:9:21:6 | Run Step | | .github/workflows/poisonable_steps.yml:20:14:20:56 | echo foo; sh venv/bin/activate.sh; echo bar | +| .github/workflows/poisonable_steps.yml:21:9:22:6 | After Run Step | | .github/workflows/poisonable_steps.yml:21:9:22:6 | Run Step | | .github/workflows/poisonable_steps.yml:21:14:21:56 | echo foo \| sh venv/bin/activate.sh > output | +| .github/workflows/poisonable_steps.yml:22:9:23:6 | After Run Step | | .github/workflows/poisonable_steps.yml:22:9:23:6 | Run Step | | .github/workflows/poisonable_steps.yml:22:14:22:40 | python venv/bin/activate.py | +| .github/workflows/poisonable_steps.yml:23:9:24:6 | After Run Step | | .github/workflows/poisonable_steps.yml:23:9:24:6 | Run Step | | .github/workflows/poisonable_steps.yml:23:14:23:50 | echo foo; python venv/bin/activate.py | +| .github/workflows/poisonable_steps.yml:24:9:25:6 | After Run Step | | .github/workflows/poisonable_steps.yml:24:9:25:6 | Run Step | | .github/workflows/poisonable_steps.yml:24:14:24:29 | pnpm run test:ct | +| .github/workflows/poisonable_steps.yml:25:9:26:6 | After Run Step | | .github/workflows/poisonable_steps.yml:25:9:26:6 | Run Step | | .github/workflows/poisonable_steps.yml:25:14:25:73 | pip install nbformat && python scripts/generate_notebooks.py | +| .github/workflows/poisonable_steps.yml:26:9:27:6 | After Run Step | | .github/workflows/poisonable_steps.yml:26:9:27:6 | Run Step | | .github/workflows/poisonable_steps.yml:26:14:26:78 | python scripts/generate_theme.py --outfile js/storybook/theme.css | +| .github/workflows/poisonable_steps.yml:27:9:28:6 | After Run Step | | .github/workflows/poisonable_steps.yml:27:9:28:6 | Run Step | | .github/workflows/poisonable_steps.yml:27:14:27:76 | ruby scripts/generate_theme.rb --outfile js/storybook/theme.css | +| .github/workflows/poisonable_steps.yml:28:9:29:6 | After Run Step | | .github/workflows/poisonable_steps.yml:28:9:29:6 | Run Step | | .github/workflows/poisonable_steps.yml:28:14:28:92 | bundle run exec ruby scripts/generate_theme.rb --outfile js/storybook/theme.css | +| .github/workflows/poisonable_steps.yml:29:9:30:6 | After Run Step | | .github/workflows/poisonable_steps.yml:29:9:30:6 | Run Step | | .github/workflows/poisonable_steps.yml:29:14:29:42 | xvfb-run ./mvnw clean package | +| .github/workflows/poisonable_steps.yml:30:9:31:6 | After Run Step | | .github/workflows/poisonable_steps.yml:30:9:31:6 | Run Step | | .github/workflows/poisonable_steps.yml:30:14:30:46 | echo "foo" && npm i && echo "bar" | +| .github/workflows/poisonable_steps.yml:31:9:32:6 | After Run Step | | .github/workflows/poisonable_steps.yml:31:9:32:6 | Run Step | | .github/workflows/poisonable_steps.yml:31:14:31:44 | echo "foo" \| npm i \| echo "bar" | +| .github/workflows/poisonable_steps.yml:32:9:33:6 | After Run Step | | .github/workflows/poisonable_steps.yml:32:9:33:6 | Run Step | | .github/workflows/poisonable_steps.yml:32:14:32:44 | echo "foo" \| npm i \| echo "bar" | +| .github/workflows/poisonable_steps.yml:33:9:34:6 | After Run Step | | .github/workflows/poisonable_steps.yml:33:9:34:6 | Run Step | | .github/workflows/poisonable_steps.yml:33:14:33:35 | echo "foo `npm i` bar" | +| .github/workflows/poisonable_steps.yml:34:9:35:6 | After Run Step | | .github/workflows/poisonable_steps.yml:34:9:35:6 | Run Step | | .github/workflows/poisonable_steps.yml:34:14:34:52 | dotnet test foo/Tests.csproj -c Release | +| .github/workflows/poisonable_steps.yml:35:9:36:6 | After Run Step | | .github/workflows/poisonable_steps.yml:35:9:36:6 | Run Step | | .github/workflows/poisonable_steps.yml:35:14:35:26 | go run foo.go | +| .github/workflows/poisonable_steps.yml:36:9:37:6 | After Run Step | | .github/workflows/poisonable_steps.yml:36:9:37:6 | Run Step | | .github/workflows/poisonable_steps.yml:36:14:36:86 | sed -i "s\|git_branch = .*\|git_branch = \\"$GITHUB_HEAD_REF\\"\|" config.json | +| .github/workflows/poisonable_steps.yml:37:9:38:6 | After Run Step | | .github/workflows/poisonable_steps.yml:37:9:38:6 | Run Step | | .github/workflows/poisonable_steps.yml:37:14:37:51 | sed -f ./config.sed file.txt > foo.txt | +| .github/workflows/poisonable_steps.yml:38:9:39:6 | After Run Step | | .github/workflows/poisonable_steps.yml:38:9:39:6 | Run Step | | .github/workflows/poisonable_steps.yml:38:14:38:45 | sed -f config file.txt > foo.txt | +| .github/workflows/poisonable_steps.yml:39:9:40:6 | After Run Step | | .github/workflows/poisonable_steps.yml:39:9:40:6 | Run Step | | .github/workflows/poisonable_steps.yml:39:14:39:55 | echo "foo" \| awk -f ./config.awk > foo.txt | +| .github/workflows/poisonable_steps.yml:40:9:41:6 | After Run Step | | .github/workflows/poisonable_steps.yml:40:9:41:6 | Run Step | | .github/workflows/poisonable_steps.yml:40:14:40:73 | gcloud builds submit --quiet --substitutions="COMMIT_SHA=foo | +| .github/workflows/poisonable_steps.yml:41:9:42:6 | After Run Step | | .github/workflows/poisonable_steps.yml:41:9:42:6 | Run Step | | .github/workflows/poisonable_steps.yml:41:14:41:22 | ./foo/cmd | +| .github/workflows/poisonable_steps.yml:42:9:46:111 | After Run Step | | .github/workflows/poisonable_steps.yml:42:9:46:111 | Run Step | | .github/workflows/poisonable_steps.yml:42:14:46:111 | sed -e 's##TITLE#' \\\n -e 's##${{ env.sot_repo }}#' \\\n -e 's##${TITLE}#' \\\n .github/workflows/common-copybara.bara.sky.template > .github/workflows/common-copybara.bara.sky\n | | .github/workflows/poisonable_steps.yml:44:32:44:50 | env.sot_repo | -| .github/workflows/shell.yml:1:1:22:32 | enter on: push | -| .github/workflows/shell.yml:1:1:22:32 | exit on: push | -| .github/workflows/shell.yml:1:1:22:32 | exit on: push (normal) | +| .github/workflows/shell.yml:1:1:22:32 | After on: push | +| .github/workflows/shell.yml:1:1:22:32 | Entry | +| .github/workflows/shell.yml:1:1:22:32 | Exit | +| .github/workflows/shell.yml:1:1:22:32 | Normal Exit | | .github/workflows/shell.yml:1:1:22:32 | on: push | +| .github/workflows/shell.yml:5:5:9:2 | After Job: job1 | | .github/workflows/shell.yml:5:5:9:2 | Job: job1 | +| .github/workflows/shell.yml:7:9:9:2 | After Run Step | | .github/workflows/shell.yml:7:9:9:2 | Run Step | | .github/workflows/shell.yml:8:14:8:31 | Write-Output "foo" | +| .github/workflows/shell.yml:10:5:14:2 | After Job: job2 | | .github/workflows/shell.yml:10:5:14:2 | Job: job2 | +| .github/workflows/shell.yml:12:9:14:2 | After Run Step | | .github/workflows/shell.yml:12:9:14:2 | Run Step | | .github/workflows/shell.yml:12:14:12:23 | echo "foo" | +| .github/workflows/shell.yml:15:5:19:2 | After Job: job3 | | .github/workflows/shell.yml:15:5:19:2 | Job: job3 | +| .github/workflows/shell.yml:17:9:19:2 | After Run Step | | .github/workflows/shell.yml:17:9:19:2 | Run Step | | .github/workflows/shell.yml:18:14:18:23 | echo "foo" | +| .github/workflows/shell.yml:20:5:22:32 | After Job: job4 | | .github/workflows/shell.yml:20:5:22:32 | Job: job4 | +| .github/workflows/shell.yml:22:9:22:32 | After Run Step | | .github/workflows/shell.yml:22:9:22:32 | Run Step | | .github/workflows/shell.yml:22:14:22:31 | Write-Output "foo" | -| .github/workflows/test.yml:1:1:40:53 | enter on: push | -| .github/workflows/test.yml:1:1:40:53 | exit on: push | -| .github/workflows/test.yml:1:1:40:53 | exit on: push (normal) | +| .github/workflows/test.yml:1:1:40:53 | After on: push | +| .github/workflows/test.yml:1:1:40:53 | Entry | +| .github/workflows/test.yml:1:1:40:53 | Exit | +| .github/workflows/test.yml:1:1:40:53 | Normal Exit | | .github/workflows/test.yml:1:1:40:53 | on: push | +| .github/workflows/test.yml:5:5:31:2 | After Job: job1 | | .github/workflows/test.yml:5:5:31:2 | Job: job1 | +| .github/workflows/test.yml:8:7:10:4 | After Job outputs node | | .github/workflows/test.yml:8:7:10:4 | Job outputs node | | .github/workflows/test.yml:8:20:8:50 | steps.step.outputs.value | | .github/workflows/test.yml:11:9:15:6 | Uses Step | | .github/workflows/test.yml:15:9:19:6 | Uses Step: source | +| .github/workflows/test.yml:19:9:26:6 | After Uses Step: step | | .github/workflows/test.yml:19:9:26:6 | Uses Step: step | | .github/workflows/test.yml:23:20:23:64 | steps.source.outputs.all_changed_files | +| .github/workflows/test.yml:26:9:28:6 | After Run Step: simplesink1 | | .github/workflows/test.yml:26:9:28:6 | Run Step: simplesink1 | | .github/workflows/test.yml:27:14:27:63 | echo ${{ steps.source.outputs.all_changed_files }} | | .github/workflows/test.yml:27:20:27:64 | steps.source.outputs.all_changed_files | +| .github/workflows/test.yml:28:9:31:2 | After Run Step: simplesink2 | | .github/workflows/test.yml:28:9:31:2 | Run Step: simplesink2 | | .github/workflows/test.yml:29:14:29:54 | ${{ github.event.pull_request.head.ref }} | | .github/workflows/test.yml:29:15:29:55 | github.event.pull_request.head.ref | +| .github/workflows/test.yml:32:5:40:53 | After Job: job2 | | .github/workflows/test.yml:32:5:40:53 | Job: job2 | +| .github/workflows/test.yml:39:9:40:53 | After Run Step: sink | | .github/workflows/test.yml:39:9:40:53 | Run Step: sink | | .github/workflows/test.yml:40:14:40:52 | echo ${{needs.job1.outputs.job_output}} | | .github/workflows/test.yml:40:20:40:53 | needs.job1.outputs.job_output | +cfgCycles +cfgDeadEnds dfNodes | .github/workflows/commands.yml:9:5:31:2 | Job: local_commands | | .github/workflows/commands.yml:15:9:18:6 | Run Step | @@ -1621,6 +1736,59 @@ scopes | .github/workflows/poisonable_steps.yml:1:1:46:111 | on: push | | .github/workflows/shell.yml:1:1:22:32 | on: push | | .github/workflows/test.yml:1:1:40:53 | on: push | +workflowScopes +| .github/workflows/commands.yml:1:1:39:30 | on: push | +| .github/workflows/controlcheck.yml:1:1:16:25 | on: | +| .github/workflows/expression_nodes.yml:1:1:21:47 | on: issue_comment | +| .github/workflows/many_strings.yml:1:1:18:1211 | on: | +| .github/workflows/multiline2.yml:1:1:89:35 | on: | +| .github/workflows/multiline.yml:1:1:89:29 | on: | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | on: push | +| .github/workflows/shell.yml:1:1:22:32 | on: push | +| .github/workflows/test.yml:1:1:40:53 | on: push | +compositeActionScopes +workflowCfgBounds +| .github/workflows/commands.yml:1:1:39:30 | on: push | 1 | 38 | +| .github/workflows/controlcheck.yml:1:1:16:25 | on: | 1 | 16 | +| .github/workflows/expression_nodes.yml:1:1:21:47 | on: issue_comment | 1 | 20 | +| .github/workflows/many_strings.yml:1:1:18:1211 | on: | 1 | 11 | +| .github/workflows/multiline2.yml:1:1:89:35 | on: | 1 | 86 | +| .github/workflows/multiline.yml:1:1:89:29 | on: | 1 | 86 | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | on: push | 1 | 44 | +| .github/workflows/shell.yml:1:1:22:32 | on: push | 1 | 22 | +| .github/workflows/test.yml:1:1:40:53 | on: push | 1 | 40 | +workflowCfgNodes +| .github/workflows/commands.yml:1:1:39:30 | on: push | +| .github/workflows/controlcheck.yml:1:1:16:25 | on: | +| .github/workflows/expression_nodes.yml:1:1:21:47 | on: issue_comment | +| .github/workflows/many_strings.yml:1:1:18:1211 | on: | +| .github/workflows/multiline2.yml:1:1:89:35 | on: | +| .github/workflows/multiline.yml:1:1:89:29 | on: | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | on: push | +| .github/workflows/shell.yml:1:1:22:32 | on: push | +| .github/workflows/test.yml:1:1:40:53 | on: push | +entryScopes +| .github/workflows/commands.yml:1:1:39:30 | Entry | .github/workflows/commands.yml:1:1:39:30 | on: push | +| .github/workflows/controlcheck.yml:1:1:16:25 | Entry | .github/workflows/controlcheck.yml:1:1:16:25 | on: | +| .github/workflows/expression_nodes.yml:1:1:21:47 | Entry | .github/workflows/expression_nodes.yml:1:1:21:47 | on: issue_comment | +| .github/workflows/many_strings.yml:1:1:18:1211 | Entry | .github/workflows/many_strings.yml:1:1:18:1211 | on: | +| .github/workflows/multiline2.yml:1:1:89:35 | Entry | .github/workflows/multiline2.yml:1:1:89:35 | on: | +| .github/workflows/multiline.yml:1:1:89:29 | Entry | .github/workflows/multiline.yml:1:1:89:29 | on: | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | Entry | .github/workflows/poisonable_steps.yml:1:1:46:111 | on: push | +| .github/workflows/shell.yml:1:1:22:32 | Entry | .github/workflows/shell.yml:1:1:22:32 | on: push | +| .github/workflows/test.yml:1:1:40:53 | Entry | .github/workflows/test.yml:1:1:40:53 | on: push | +normalExitNodes +| .github/workflows/commands.yml:1:1:39:30 | Normal Exit | +| .github/workflows/controlcheck.yml:1:1:16:25 | Normal Exit | +| .github/workflows/expression_nodes.yml:1:1:21:47 | Normal Exit | +| .github/workflows/many_strings.yml:1:1:18:1211 | Normal Exit | +| .github/workflows/multiline2.yml:1:1:89:35 | Normal Exit | +| .github/workflows/multiline.yml:1:1:89:29 | Normal Exit | +| .github/workflows/poisonable_steps.yml:1:1:46:111 | Normal Exit | +| .github/workflows/shell.yml:1:1:22:32 | Normal Exit | +| .github/workflows/test.yml:1:1:40:53 | Normal Exit | +legacyNodeProperties +legacyCfgSplits sources | AvraamMavridis/files-changed-action | * | output.CHANGED_FILES | filename | manual | | AvraamMavridis/files-changed-action | * | output.CHANGED_FILES_EXTENSIONS | filename | manual | diff --git a/actions/ql/test/library-tests/basic/test.ql b/actions/ql/test/library-tests/basic/test.ql index e4c1d9e443d0..5e6749da288b 100644 --- a/actions/ql/test/library-tests/basic/test.ql +++ b/actions/ql/test/library-tests/basic/test.ql @@ -37,6 +37,13 @@ query predicate parentNodes(AstNode child, AstNode parent) { child.getParentNode query predicate cfgNodes(Cfg::Node n) { any() } +query predicate cfgCycles(Cfg::Node n) { n.getASuccessor+() = n } + +query predicate cfgDeadEnds(Cfg::Node n) { + not n instanceof Cfg::ExitNode and + not exists(n.getASuccessor()) +} + query predicate dfNodes(DataFlow::Node e) { any() } query predicate argumentNodes(DataFlow::ArgumentNode e) { any() } @@ -47,6 +54,48 @@ query predicate nodeLocations(DataFlow::Node n, Location l) { n.getLocation() = query predicate scopes(Cfg::CfgScope c) { any() } +query predicate workflowScopes(Cfg::WorkflowScope c) { any() } + +query predicate compositeActionScopes(Cfg::CompositeActionScope c) { any() } + +query predicate workflowCfgBounds(Workflow workflow, int entryLine, int exitLine) { + exists(AstNode entry, AstNode exit | + entry = Cfg::getAControlFlowEntryNode(workflow) and + exit = Cfg::getAControlFlowExitNode(workflow) and + entryLine = entry.getLocation().getStartLine() and + exitLine = exit.getLocation().getStartLine() + ) +} + +query predicate workflowCfgNodes(Cfg::AstCfgNode node) { + Cfg::forceCachingInSameStage() and + node.getAstNode() instanceof Workflow and + Cfg::getNodeCfgScope(node) = node.getAstNode() +} + +query predicate entryScopes(Cfg::EntryNode entry, Cfg::CfgScope scope) { scope = entry.getScope() } + +query predicate normalExitNodes(Cfg::AnnotatedExitNode exit) { exit.isNormal() } + +query predicate legacyNodeProperties( + Cfg::Node node, Cfg::SuccessorType successorType, string property +) { + node = node.getASuccessor(successorType) and property = "successor" + or + node = node.getAPredecessor(successorType) and property = "predecessor" + or + node.isCondition() and property = "condition" + or + node.isJoin() and property = "join" + or + node.isBranch() and property = "branch" +} + +query predicate legacyCfgSplits(Cfg::AstCfgNode node) { + exists(node.getSplitsString()) or + exists(node.getASplit()) +} + query predicate sources(string action, string version, string output, string kind, string provenance) { actionsSourceModel(action, version, output, kind, provenance) } diff --git a/actions/ql/test/library-tests/cfg-callable/action.yml b/actions/ql/test/library-tests/cfg-callable/action.yml new file mode 100644 index 000000000000..81aa1df95a00 --- /dev/null +++ b/actions/ql/test/library-tests/cfg-callable/action.yml @@ -0,0 +1,12 @@ +name: Ambiguous root + +runs: + using: composite + steps: + - run: echo "${{ github.actor }}" + +jobs: + unexpected: + runs-on: ubuntu-latest + steps: + - run: echo "This must not make action.yml a workflow" diff --git a/actions/ql/test/library-tests/cfg-callable/test.expected b/actions/ql/test/library-tests/cfg-callable/test.expected new file mode 100644 index 000000000000..057e5f24e2c6 --- /dev/null +++ b/actions/ql/test/library-tests/cfg-callable/test.expected @@ -0,0 +1,5 @@ +roots +| 1 | 1 | +cfgScopes +| action.yml:1:1:12:61 | name: Ambiguous root | composite action | +cfgConsistency diff --git a/actions/ql/test/library-tests/cfg-callable/test.ql b/actions/ql/test/library-tests/cfg-callable/test.ql new file mode 100644 index 000000000000..fd60bb368954 --- /dev/null +++ b/actions/ql/test/library-tests/cfg-callable/test.ql @@ -0,0 +1,23 @@ +import codeql.actions.Ast +import codeql.actions.Cfg as Cfg + +query predicate roots(int workflows, int compositeActions) { + workflows = + strictcount(Workflow workflow | workflow.getLocation().getFile().getBaseName() = "action.yml") and + compositeActions = + strictcount(CompositeAction action | action.getLocation().getFile().getBaseName() = "action.yml") +} + +query predicate cfgScopes(Cfg::CfgScope scope, string kind) { + scope.getLocation().getFile().getBaseName() = "action.yml" and + ( + scope instanceof Cfg::WorkflowScope and kind = "workflow" + or + scope instanceof Cfg::CompositeActionScope and kind = "composite action" + ) +} + +query predicate cfgConsistency(string query, int results) { + Cfg::Consistency::consistencyOverview(query, results) and + results != 0 +} diff --git a/actions/ql/test/library-tests/cfg-environment/.github/workflows/test.yml b/actions/ql/test/library-tests/cfg-environment/.github/workflows/test.yml new file mode 100644 index 000000000000..1bd23112154a --- /dev/null +++ b/actions/ql/test/library-tests/cfg-environment/.github/workflows/test.yml @@ -0,0 +1,21 @@ +on: issues + +env: + GLOBAL_VALUE: ${{ github.event.issue.title }} + +jobs: + local: + runs-on: ubuntu-latest + env: + JOB_VALUE: ${{ github.event.issue.body }} + steps: + - uses: actions/checkout@v4 + - run: echo "$GLOBAL_VALUE $JOB_VALUE" + - run: echo "$GLOBAL_VALUE $JOB_VALUE $STEP_VALUE" + env: + STEP_VALUE: ${{ github.actor }} + + external: + uses: octo/example/.github/workflows/reusable.yml@main + with: + value: ${{ github.event.issue.number }} diff --git a/actions/ql/test/library-tests/cfg-environment/test.expected b/actions/ql/test/library-tests/cfg-environment/test.expected new file mode 100644 index 000000000000..4b9b38e9fdb8 --- /dev/null +++ b/actions/ql/test/library-tests/cfg-environment/test.expected @@ -0,0 +1,7 @@ +envCfgNodes +| .github/workflows/test.yml:4:18:4:48 | github.event.issue.title | +| .github/workflows/test.yml:10:19:10:48 | github.event.issue.body | +| .github/workflows/test.yml:16:24:16:42 | github.actor | +cfgCycles +cfgDeadEnds +cfgConsistency diff --git a/actions/ql/test/library-tests/cfg-environment/test.ql b/actions/ql/test/library-tests/cfg-environment/test.ql new file mode 100644 index 000000000000..99f1c23009b3 --- /dev/null +++ b/actions/ql/test/library-tests/cfg-environment/test.ql @@ -0,0 +1,19 @@ +import codeql.actions.Ast +import codeql.actions.Cfg as Cfg + +query predicate envCfgNodes(Expression expression) { + expression = any(Env env).getAnEnvVarExpr() and + exists(Cfg::AstCfgNode node | node.getAstNode() = expression) +} + +query predicate cfgCycles(Cfg::Node node) { node.getASuccessor+() = node } + +query predicate cfgDeadEnds(Cfg::Node node) { + not node instanceof Cfg::ExitNode and + not exists(node.getASuccessor()) +} + +query predicate cfgConsistency(string query, int results) { + Cfg::Consistency::consistencyOverview(query, results) and + results != 0 +} diff --git a/actions/ql/test/query-tests/Security/CWE-078/CONSISTENCY/CfgConsistency.expected b/actions/ql/test/query-tests/Security/CWE-078/CONSISTENCY/CfgConsistency.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/actions/ql/test/query-tests/Security/CWE-088/CONSISTENCY/CfgConsistency.expected b/actions/ql/test/query-tests/Security/CWE-088/CONSISTENCY/CfgConsistency.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/actions/ql/test/query-tests/Security/CWE-094/CONSISTENCY/CfgConsistency.expected b/actions/ql/test/query-tests/Security/CWE-094/CONSISTENCY/CfgConsistency.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/actions/ql/test/query-tests/Security/CWE-829/CONSISTENCY/CfgConsistency.expected b/actions/ql/test/query-tests/Security/CWE-829/CONSISTENCY/CfgConsistency.expected new file mode 100644 index 000000000000..e69de29bb2d1