diff --git a/queries/inconsistent-action-input.ql b/queries/inconsistent-action-input.ql new file mode 100644 index 000000000..ec24fed06 --- /dev/null +++ b/queries/inconsistent-action-input.ql @@ -0,0 +1,51 @@ +/** + * @name Inconsistent action input + * @description If multiple actions define an input with the same name, then the input + * must be defined in an identical way to avoid confusion for the user. + * This also makes writing queries like required-action-input.ql easier. + * @kind problem + * @problem.severity error + * @id javascript/codeql-action/inconsistent-action-input + */ + +import javascript + +/** + * A declaration of a github action. + */ +class ActionDeclaration extends File { + ActionDeclaration() { + getRelativePath().matches("%/action.yml") + } + + /** + * The name of the action. + */ + string getName() { + result = getRelativePath().regexpCapture("(.*)/action.yml", 1) + } + + YAMLDocument getRootNode() { + result.getFile() = this + } + + YAMLValue getInput(string inputName) { + result = getRootNode().(YAMLMapping).lookup("inputs").(YAMLMapping).lookup(inputName) + } +} + +predicate areNotEquivalent(YAMLValue x, YAMLValue y) { + x.getTag() != y.getTag() + or + x.(YAMLScalar).getValue() != y.(YAMLScalar).getValue() + or + x.getNumChild() != y.getNumChild() + or + exists(int i | areNotEquivalent(x.getChild(i), y.getChild(i))) +} + +from ActionDeclaration actionA, ActionDeclaration actionB, string inputName +where actionA.getName() < actionB.getName() // prevent duplicates which are permutations of the names + and areNotEquivalent(actionA.getInput(inputName), actionB.getInput(inputName)) +select actionA, "Action $@ and action $@ both declare input $@, however their definitions are not identical. This may be confusing to users.", + actionA, actionA.getName(), actionB, actionB.getName(), inputName, inputName diff --git a/queries/required-action-input.ql b/queries/required-action-input.ql new file mode 100644 index 000000000..8bf9c565e --- /dev/null +++ b/queries/required-action-input.ql @@ -0,0 +1,93 @@ +/** + * @name Required action input + * @description For action inputs the core.input represents input with no value as the emptystring. + * This doesn't promote good type checking. Instead, use either actions-util.getOptionalInput or + * actions-util.getRequiredInput depending on if the input always has a value or not. The input + * will always have a value if it is required or has a default value. + * @kind problem + * @problem.severity error + * @id javascript/codeql-action/required-action-input + */ + +import javascript + +/** + * A declaration of a github action. + */ +class ActionDeclaration extends File { + ActionDeclaration() { + getRelativePath().matches("%/action.yml") + } + + YAMLDocument getRootNode() { + result.getFile() = this + } + + /** + * The name of any input to this action. + */ + string getAnInput() { + result = getRootNode().(YAMLMapping).lookup("inputs").(YAMLMapping).getKey(_).(YAMLString).getValue() + } + + /** + * The given input always has a value, either because it is required, + * or because it has a default value. + */ + predicate inputAlwaysHasValue(string input) { + exists(YAMLMapping value | + value = getRootNode().(YAMLMapping).lookup("inputs").(YAMLMapping).lookup(input) and + (exists(value.lookup("default")) or + value.lookup("required").(YAMLBool).getBoolValue() = true)) + } + + /** + * The function that is the entrypoint to this action. + */ + FunctionDeclStmt getEntrypoint() { + result.getFile().getRelativePath() = getRootNode(). + (YAMLMapping).lookup("runs"). + (YAMLMapping).lookup("main"). + (YAMLString).getValue().regexpReplaceAll("\\.\\./lib/(.*)\\.js", "src/$1.ts") and + result.getName() = "run" + } +} + +/** + * An import from "@actions/core" + */ +class ActionsLibImport extends ImportDeclaration { + ActionsLibImport() { + getImportedPath().getValue() = "@actions/core" + } + + Variable getAProvidedVariable() { + result = getASpecifier().getLocal().getVariable() + } +} + +/** + * A call to the core.getInput method. + */ +class CoreGetInputMethodCallExpr extends MethodCallExpr { + CoreGetInputMethodCallExpr() { + getMethodName() = "getInput" and + exists(ActionsLibImport libImport | + this.getReceiver() = libImport.getAProvidedVariable().getAnAccess() or + this.getReceiver().(PropAccess).getBase() = libImport.getAProvidedVariable().getAnAccess()) + } + + /** + * The name of the input being accessed. + */ + string getInputName() { + result = getArgument(0).(StringLiteral).getValue() + } +} + +from ActionDeclaration action, CoreGetInputMethodCallExpr getInputCall, string inputName, string alternateFunction +where action.getAnInput() = inputName + and getInputCall.getInputName() = inputName + and ((action.inputAlwaysHasValue(inputName) and alternateFunction = "getRequiredInput") + or (not action.inputAlwaysHasValue(inputName) and alternateFunction = "geOptionalInput")) +select getInputCall, "This input may be undefined. Please use actions-util.$@ instead.", alternateFunction, alternateFunction \ No newline at end of file