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..4ac41f1ce428 --- /dev/null +++ b/actions/ql/lib/change-notes/2026-09-29-shared-cfg.md @@ -0,0 +1,12 @@ +--- +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..4cc9ae397abf 100644 --- a/actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll +++ b/actions/ql/lib/codeql/actions/controlflow/internal/Cfg.qll @@ -1,288 +1,471 @@ 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; - class SimpleCompletion extends NormalCompletion, TSimpleCompletion { - override string toString() { result = "SimpleCompletion" } + AstNode getChild(AstNode node, int index) { result = getCfgChild(node, index) } - override predicate isValidFor(AstNode e) { not any(Completion c).isValidForSpecific(e) } + class Callable extends AstNode { + Callable() { this instanceof Workflow or this instanceof CompositeAction } + } - override DirectSuccessor getAMatchingSuccessorType() { any() } - } + AstNode callableGetBody(Callable callable) { result = callable } - class BooleanCompletion extends NormalCompletion, TBooleanCompletion { - boolean value; + Callable getEnclosingCallable(AstNode node) { + result = node.(Callable) + or + result = getEnclosingCallable(node.getParentNode()) + } - BooleanCompletion() { this = TBooleanCompletion(value) } + class Parameter extends AstNode { + Parameter() { none() } - override string toString() { result = "BooleanCompletion(" + value + ")" } + AstNode getPattern() { none() } - override predicate isValidForSpecific(AstNode e) { none() } + Expr getDefaultValue() { none() } + } - override BooleanSuccessor getAMatchingSuccessorType() { result.getValue() = value } + Parameter callableGetParameter(Callable callable, int index) { none() } - final boolean getValue() { result = value } - } + class Stmt extends AstNode { + Stmt() { none() } + } - class ReturnCompletion extends Completion, TReturnCompletion { - override string toString() { result = "ReturnCompletion" } + class Expr extends AstNode { + Expr() { none() } + } - override predicate isValidForSpecific(AstNode e) { none() } + class BlockStmt extends Stmt { + BlockStmt() { none() } - override ReturnSuccessor getAMatchingSuccessorType() { any() } - } -} + Stmt getStmt(int index) { none() } -module CfgScope { - abstract class CfgScope extends AstNode { } + Stmt getLastStmt() { none() } + } - class WorkflowScope extends CfgScope instanceof Workflow { } + class ExprStmt extends Stmt { + ExprStmt() { none() } - class CompositeActionScope extends CfgScope instanceof CompositeAction { } -} + Expr getExpr() { none() } + } -private module Implementation implements CfgShared::InputSig { - import codeql.actions.Ast - import Completion - import CfgScope + class IfStmt extends Stmt { + IfStmt() { none() } - predicate completionIsNormal(Completion c) { not c instanceof ReturnCompletion } + Expr getCondition() { none() } - // Not using CFG splitting, so the following are just dummy types. - private newtype TUnit = Unit() + Stmt getThen() { none() } - additional class SplitKindBase = TUnit; + Stmt getElse() { none() } + } - additional class Split extends TUnit { - abstract string toString(); - } + class LoopStmt extends Stmt { + LoopStmt() { none() } - predicate completionIsSimple(Completion c) { c instanceof SimpleCompletion } + Stmt getBody() { none() } + } - predicate completionIsValidFor(Completion c, AstNode e) { c.isValidFor(e) } + class WhileStmt extends LoopStmt { + WhileStmt() { 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 DoStmt extends LoopStmt { + DoStmt() { 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 UntilStmt extends LoopStmt { + UntilStmt() { none() } - SuccessorType getAMatchingSuccessorType(Completion c) { result = c.getAMatchingSuccessorType() } + Expr getCondition() { none() } + } - int idOfAstNode(AstNode node) { none() } + class ForStmt extends LoopStmt { + ForStmt() { none() } - int idOfCfgScope(CfgScope scope) { none() } -} + AstNode getInit(int index) { none() } -module CfgImpl = CfgShared::Make; + Expr getCondition() { none() } -private import CfgImpl -private import Completion -private import CfgScope + AstNode getUpdate(int index) { 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() - ) - } -} + class ForEachStmt extends LoopStmt { + ForEachStmt() { none() } -private class RunsTree extends StandardPreOrderTree instanceof Runs { - override ControlFlowTree getChildNode(int i) { result = super.getStep(i) } -} + Expr getVariable() { 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() - ) - } -} + Expr getCollection() { 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 BreakStmt extends Stmt { + BreakStmt() { 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 ContinueStmt extends Stmt { + ContinueStmt() { 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() - ) + class GotoStmt extends Stmt { + GotoStmt() { none() } + } + + class ReturnStmt extends Stmt { + ReturnStmt() { none() } + + 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-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