diff --git a/powershell/ql/lib/semmle/code/powershell/ArrayExpression.qll b/powershell/ql/lib/semmle/code/powershell/ArrayExpression.qll index b1841c94094..a8d8ba6735d 100644 --- a/powershell/ql/lib/semmle/code/powershell/ArrayExpression.qll +++ b/powershell/ql/lib/semmle/code/powershell/ArrayExpression.qll @@ -3,7 +3,7 @@ import powershell class ArrayExpr extends @array_expression, Expr { override SourceLocation getLocation() { array_expression_location(this, result) } - StmtBlock getStatementBlock() { array_expression(this, result) } + StmtBlock getStmtBlock() { array_expression(this, result) } - override string toString() { result = "ArrayExpression at: " + this.getLocation().toString() } + override string toString() { result = "@(...)" } } diff --git a/powershell/ql/lib/semmle/code/powershell/Attribute.qll b/powershell/ql/lib/semmle/code/powershell/Attribute.qll index 88c8b466e0f..72a061c09f2 100644 --- a/powershell/ql/lib/semmle/code/powershell/Attribute.qll +++ b/powershell/ql/lib/semmle/code/powershell/Attribute.qll @@ -15,7 +15,11 @@ class Attribute extends @attribute, AttributeBase { NamedAttributeArgument getANamedArgument() { result = this.getNamedArgument(_) } + int getNumberOfArguments() { result = count(this.getAPositionalArgument()) } + Expr getPositionalArgument(int i) { attribute_positional_argument(this, i, result) } Expr getAPositionalArgument() { result = this.getPositionalArgument(_) } + + int getNumberOfPositionalArguments() { result = count(this.getAPositionalArgument()) } } diff --git a/powershell/ql/lib/semmle/code/powershell/CatchClause.qll b/powershell/ql/lib/semmle/code/powershell/CatchClause.qll index 4bc589c49a8..6d3bf4cb297 100644 --- a/powershell/ql/lib/semmle/code/powershell/CatchClause.qll +++ b/powershell/ql/lib/semmle/code/powershell/CatchClause.qll @@ -16,4 +16,24 @@ class CatchClause extends @catch_clause, Ast { predicate isCatchAll() { not exists(this.getACatchType()) } TryStmt getTryStmt() { result.getACatchClause() = this } + + predicate isLast() { + exists(TryStmt ts, int last | + ts = this.getTryStmt() and + last = max(int i | exists(ts.getCatchClause(i))) and + this = ts.getCatchClause(last) + ) + } +} + +class GeneralCatchClause extends CatchClause { + GeneralCatchClause() { this.isCatchAll() } + + override string toString() { result = "catch {...}" } +} + +class SpecificCatchClause extends CatchClause { + SpecificCatchClause() { not this.isCatchAll() } + + override string toString() { result = "catch[...] {...}" } } diff --git a/powershell/ql/lib/semmle/code/powershell/Command.qll b/powershell/ql/lib/semmle/code/powershell/Command.qll index 29f17624493..73c2df41a7a 100644 --- a/powershell/ql/lib/semmle/code/powershell/Command.qll +++ b/powershell/ql/lib/semmle/code/powershell/Command.qll @@ -15,9 +15,28 @@ class Cmd extends @command, CmdBase { CmdElement getElement(int i) { command_command_element(this, i, result) } - Redirection getRedirection(int i) { command_redirection(this, i, result) } + StringConstExpr getCmdName() { result = this.getElement(0) } - CmdElement getAnElement() { result = this.getElement(_) } + Expr getArgument(int i) { + result = + rank[i + 1](CmdElement e, int j | + e = this.getElement(j) and + not e instanceof CmdParameter and + j > 0 // 0'th element is the command name itself + | + e order by j + ) + } + + Expr getNamedArgument(string name) { + exists(int i, CmdParameter p | + this.getElement(i) = p and + p.getName() = name and + result = p.getArgument() + ) + } + + Redirection getRedirection(int i) { command_redirection(this, i, result) } Redirection getARedirection() { result = this.getRedirection(_) } } diff --git a/powershell/ql/lib/semmle/code/powershell/CommandParameter.qll b/powershell/ql/lib/semmle/code/powershell/CommandParameter.qll index 9e2faa21064..e1bb400b87b 100644 --- a/powershell/ql/lib/semmle/code/powershell/CommandParameter.qll +++ b/powershell/ql/lib/semmle/code/powershell/CommandParameter.qll @@ -5,7 +5,19 @@ class CmdParameter extends @command_parameter, CmdElement { string getName() { command_parameter(this, result) } - Expr getArgument() { command_parameter_argument(this, result) } + Expr getArgument() { + // When an argumnt is of the form -Name:$var + command_parameter_argument(this, result) + or + // When an argument is of the form -Name $var + exists(int i, Cmd cmd | + cmd = this.getCmd() and + cmd.getElement(i) = this and + result = cmd.getElement(i + 1) + ) + } + + Cmd getCmd() { result.getElement(_) = this } override string toString() { command_parameter(this, result) } } diff --git a/powershell/ql/lib/semmle/code/powershell/IfStmt.qll b/powershell/ql/lib/semmle/code/powershell/IfStmt.qll index 08a1cb9be14..21ce4d68fc8 100644 --- a/powershell/ql/lib/semmle/code/powershell/IfStmt.qll +++ b/powershell/ql/lib/semmle/code/powershell/IfStmt.qll @@ -13,6 +13,10 @@ class IfStmt extends @if_statement, Stmt { StmtBlock getThen(int i) { if_statement_clause(this, i, _, result) } + int getNumberOfConditions() { result = count(this.getACondition()) } + + StmtBlock getAThen() { result = this.getThen(_) } + /** ..., if any. */ StmtBlock getElse() { if_statement_else(this, result) } diff --git a/powershell/ql/lib/semmle/code/powershell/InvokeMemberExpression.qll b/powershell/ql/lib/semmle/code/powershell/InvokeMemberExpression.qll index 90e04f0c5d1..1ff04f78e3a 100644 --- a/powershell/ql/lib/semmle/code/powershell/InvokeMemberExpression.qll +++ b/powershell/ql/lib/semmle/code/powershell/InvokeMemberExpression.qll @@ -1,9 +1,9 @@ import powershell -class InvokeMemberExpression extends @invoke_member_expression, MemberExprBase { +class InvokeMemberExpr extends @invoke_member_expression, MemberExprBase { override SourceLocation getLocation() { invoke_member_expression_location(this, result) } - Expr getExpression() { invoke_member_expression(this, result, _) } + Expr getBase() { invoke_member_expression(this, result, _) } CmdElement getMember() { invoke_member_expression(this, _, result) } @@ -11,5 +11,5 @@ class InvokeMemberExpression extends @invoke_member_expression, MemberExprBase { Expr getAnArgument() { invoke_member_expression_argument(this, _, result) } - override string toString() { result = "ArrayExpression at: " + this.getLocation().toString() } + override string toString() { result = "call to " + this.getMember() } } diff --git a/powershell/ql/lib/semmle/code/powershell/NamedAttributeArgument.qll b/powershell/ql/lib/semmle/code/powershell/NamedAttributeArgument.qll index ef6a667d213..150ff9992de 100644 --- a/powershell/ql/lib/semmle/code/powershell/NamedAttributeArgument.qll +++ b/powershell/ql/lib/semmle/code/powershell/NamedAttributeArgument.qll @@ -1,9 +1,9 @@ import powershell -class NamedAttributeArgument extends @named_attribute_argument { - string toString() { result = this.getValue().toString() } +class NamedAttributeArgument extends @named_attribute_argument, Ast { + final override string toString() { result = this.getValue().toString() } - SourceLocation getLocation() { named_attribute_argument_location(this, result) } + final override SourceLocation getLocation() { named_attribute_argument_location(this, result) } string getName() { named_attribute_argument(this, result, _) } diff --git a/powershell/ql/lib/semmle/code/powershell/StringConstantExpression.qll b/powershell/ql/lib/semmle/code/powershell/StringConstantExpression.qll index 788b2def636..2ca13df15b6 100644 --- a/powershell/ql/lib/semmle/code/powershell/StringConstantExpression.qll +++ b/powershell/ql/lib/semmle/code/powershell/StringConstantExpression.qll @@ -1,10 +1,10 @@ import powershell -class StringConstExpression extends @string_constant_expression, BaseConstExpr { +class StringConstExpr extends @string_constant_expression, BaseConstExpr { StringLiteral getValue() { string_constant_expression(this, result) } /** Get the full string literal with all its parts concatenated */ - override string toString() { result = getValue().toString() } + override string toString() { result = this.getValue().toString() } override SourceLocation getLocation() { string_constant_expression_location(this, result) } } diff --git a/powershell/ql/lib/semmle/code/powershell/StringLiteral.qll b/powershell/ql/lib/semmle/code/powershell/StringLiteral.qll index 4eb9a9d77a4..77ece5d7d1c 100644 --- a/powershell/ql/lib/semmle/code/powershell/StringLiteral.qll +++ b/powershell/ql/lib/semmle/code/powershell/StringLiteral.qll @@ -7,7 +7,11 @@ class StringLiteral extends @string_literal { /** Get the full string literal with all its parts concatenated */ string toString() { - result = concat(int i | i = [0 .. getNumContinuations()] | getContinuation(i), "\n") + result = this.getValue() + } + + string getValue() { + result = concat(int i | i = [0 .. this.getNumContinuations()] | this.getContinuation(i), "\n") } SourceLocation getLocation() { string_literal_location(this, result) } diff --git a/powershell/ql/lib/semmle/code/powershell/TryStmt.qll b/powershell/ql/lib/semmle/code/powershell/TryStmt.qll index c94c11524dc..300c59712e6 100644 --- a/powershell/ql/lib/semmle/code/powershell/TryStmt.qll +++ b/powershell/ql/lib/semmle/code/powershell/TryStmt.qll @@ -14,4 +14,5 @@ class TryStmt extends @try_statement, Stmt { StmtBlock getBody() { try_statement(this, result) } + predicate hasFinally() { exists(this.getFinally()) } } diff --git a/powershell/ql/lib/semmle/code/powershell/controlflow/ControlFlowGraph.qll b/powershell/ql/lib/semmle/code/powershell/controlflow/ControlFlowGraph.qll index 8c04fe644c8..09a58510063 100644 --- a/powershell/ql/lib/semmle/code/powershell/controlflow/ControlFlowGraph.qll +++ b/powershell/ql/lib/semmle/code/powershell/controlflow/ControlFlowGraph.qll @@ -69,10 +69,11 @@ module SuccessorTypes { * A conditional control flow successor. Either a Boolean successor (`BooleanSuccessor`) * or a matching successor (`MatchingSuccessor`) */ - class ConditionalSuccessor extends SuccessorType { + abstract class ConditionalSuccessor extends SuccessorType { boolean value; - ConditionalSuccessor() { this = CfgImpl::TBooleanSuccessor(value) } + bindingset[value] + ConditionalSuccessor() { any() } /** Gets the Boolean value of this successor. */ final boolean getValue() { result = value } @@ -80,7 +81,13 @@ module SuccessorTypes { override string toString() { result = this.getValue().toString() } } - class BooleanSuccessor extends ConditionalSuccessor, CfgImpl::TBooleanSuccessor { } + class BooleanSuccessor extends ConditionalSuccessor, CfgImpl::TBooleanSuccessor { + BooleanSuccessor() { this = CfgImpl::TBooleanSuccessor(value) } + } + + class MatchingSuccessor extends ConditionalSuccessor, CfgImpl::TMatchingSuccessor { + MatchingSuccessor() { this = CfgImpl::TMatchingSuccessor(value) } + } class ReturnSuccessor extends SuccessorType, CfgImpl::TReturnSuccessor { final override string toString() { result = "return" } @@ -94,8 +101,8 @@ module SuccessorTypes { final override string toString() { result = "continue" } } - class RaiseSuccessor extends SuccessorType, CfgImpl::TRaiseSuccessor { - final override string toString() { result = "raise" } + class ThrowSuccessor extends SuccessorType, CfgImpl::TThrowSuccessor { + final override string toString() { result = "throw" } } class ExitSuccessor extends SuccessorType, CfgImpl::TExitSuccessor { diff --git a/powershell/ql/lib/semmle/code/powershell/controlflow/internal/Completion.qll b/powershell/ql/lib/semmle/code/powershell/controlflow/internal/Completion.qll index 858779d50cd..ac51896d0c4 100644 --- a/powershell/ql/lib/semmle/code/powershell/controlflow/internal/Completion.qll +++ b/powershell/ql/lib/semmle/code/powershell/controlflow/internal/Completion.qll @@ -8,16 +8,23 @@ private import powershell private import semmle.code.powershell.controlflow.ControlFlowGraph private import ControlFlowGraphImpl as CfgImpl private import SuccessorTypes +private import codeql.util.Boolean // TODO: We most likely need a TrapCompletion as well private newtype TCompletion = TSimpleCompletion() or - TBooleanCompletion(boolean b) { b in [false, true] } or + TBooleanCompletion(Boolean b) or TReturnCompletion() or TBreakCompletion() or TContinueCompletion() or - TRaiseCompletion() or - TExitCompletion() + TThrowCompletion() or + TExitCompletion() or + TMatchingCompletion(Boolean b) + +private predicate commandThrows(Cmd c, boolean unconditional) { + c.getNamedArgument("ErrorAction").(StringConstExpr).getValue().getValue() = "Stop" and + if c.getName() = "Write-Error" then unconditional = true else unconditional = false +} pragma[noinline] private predicate completionIsValidForStmt(Ast n, Completion c) { @@ -26,6 +33,16 @@ private predicate completionIsValidForStmt(Ast n, Completion c) { or n instanceof ContinueStmt and c instanceof ContinueCompletion + or + n instanceof ThrowStmt and + c instanceof ThrowCompletion + or + exists(boolean unconditional | commandThrows(n, unconditional) | + c instanceof ThrowCompletion + or + unconditional = false and + c instanceof SimpleCompletion + ) } /** A completion of a statement or an expression. */ @@ -40,6 +57,14 @@ abstract class Completion extends TCompletion { not isBooleanConstant(n, _) and this = TBooleanCompletion(_) ) + or + mustHaveMatchingCompletion(n) and + ( + exists(boolean value | isMatchingConstant(n, value) | this = TMatchingCompletion(value)) + or + not isMatchingConstant(n, _) and + this = TMatchingCompletion(_) + ) } private predicate isValidForSpecific(Ast n) { this.isValidForSpecific0(n) } @@ -74,11 +99,18 @@ private predicate isBooleanConstant(Ast n, boolean value) { none() // TODO } +private predicate isMatchingConstant(Ast n, boolean value) { + inMatchingContext(n) and + none() // TODO +} + /** * Holds if a normal completion of `n` must be a Boolean completion. */ private predicate mustHaveBooleanCompletion(Ast n) { inBooleanContext(n) } +private predicate mustHaveMatchingCompletion(Ast n) { inMatchingContext(n) } + /** * Holds if `n` is used in a Boolean context. That is, the value * that `n` evaluates to determines a true/false branch successor. @@ -128,6 +160,22 @@ private predicate inBooleanContext(Ast n) { ) } +/** + * From: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_switch?view=powershell-7.4: + * ``` + * switch [-regex | -wildcard | -exact] [-casesensitive] () + * { + * "string" | number | variable | { } { } + * default { } # optional + * } + * ``` + */ +private predicate inMatchingContext(Ast n) { + n = any(SwitchStmt switch).getAPattern() + or + n = any(CatchClause cc).getACatchType() +} + /** * A completion that represents normal evaluation of a statement or an * expression. @@ -159,7 +207,7 @@ abstract class ConditionalCompletion extends NormalCompletion { * A completion that represents evaluation of an expression * with a Boolean value. */ -class BooleanCompletion extends ConditionalCompletion, NormalCompletion, TBooleanCompletion { +class BooleanCompletion extends ConditionalCompletion, TBooleanCompletion { BooleanCompletion() { this = TBooleanCompletion(value) } /** Gets the dual Boolean completion. */ @@ -180,6 +228,18 @@ class FalseCompletion extends BooleanCompletion { FalseCompletion() { this.getValue() = false } } +class MatchCompletion extends ConditionalCompletion, TMatchingCompletion { + MatchCompletion() { this = TMatchingCompletion(value) } + + override MatchingSuccessor getAMatchingSuccessorType() { result.getValue() = value } + + predicate isMatch() { this.getValue() = true } + + predicate isNonMatch() { this.getValue() = false } + + override string toString() { if this.isMatch() then result = "match" else result = "nonmatch" } +} + /** * A completion that represents evaluation of a statement or an * expression resulting in a return. @@ -214,10 +274,10 @@ class ContinueCompletion extends Completion, TContinueCompletion { * A completion that represents evaluation of a statement or an * expression resulting in a thrown exception. */ -class RaiseCompletion extends Completion, TRaiseCompletion { - override RaiseSuccessor getAMatchingSuccessorType() { any() } +class ThrowCompletion extends Completion, TThrowCompletion { + override ThrowSuccessor getAMatchingSuccessorType() { any() } - override string toString() { result = "raise" } + override string toString() { result = "throw" } } /** diff --git a/powershell/ql/lib/semmle/code/powershell/controlflow/internal/ControlFlowGraphImpl.qll b/powershell/ql/lib/semmle/code/powershell/controlflow/internal/ControlFlowGraphImpl.qll index 6d6dac7603f..de1adb9c921 100644 --- a/powershell/ql/lib/semmle/code/powershell/controlflow/internal/ControlFlowGraphImpl.qll +++ b/powershell/ql/lib/semmle/code/powershell/controlflow/internal/ControlFlowGraphImpl.qll @@ -5,6 +5,7 @@ private import powershell private import codeql.controlflow.Cfg as CfgShared +private import codeql.util.Boolean private import semmle.code.powershell.controlflow.ControlFlowGraph private import Completion @@ -51,7 +52,7 @@ private module CfgInput implements CfgShared::InputSig { } predicate isAbnormalExitType(SuccessorType t) { - t instanceof Cfg::SuccessorTypes::RaiseSuccessor or + t instanceof Cfg::SuccessorTypes::ThrowSuccessor or t instanceof Cfg::SuccessorTypes::ExitSuccessor } } @@ -418,6 +419,108 @@ module Trees { } } + class IfStmtTree extends PreOrderTree instanceof IfStmt { + final override predicate propagatesAbnormal(AstNode child) { + child = super.getACondition() + or + child = super.getAThen() + or + child = super.getElse() + } + + final override predicate last(AstNode last, Completion c) { + last(super.getAThen(), last, c) + or + last(super.getElse(), last, c) + } + + final override predicate succ(AstNode pred, AstNode succ, Completion c) { + this = pred and + first(super.getCondition(0), succ) and + completionIsSimple(c) + or + exists(int i, boolean value | + last(super.getCondition(i), pred, c) and value = c.(BooleanCompletion).getValue() + | + value = true and + first(super.getThen(i), succ) + or + value = false and + ( + first(super.getCondition(i + 1), succ) + or + i = super.getNumberOfConditions() - 1 and + first(super.getElse(), succ) + ) + ) + } + } + + class SwitchStmtTree extends PreOrderTree instanceof SwitchStmt { + final override predicate propagatesAbnormal(AstNode child) { + child = super.getCondition() + or + child = super.getACase() + or + child = super.getDefault() + or + child = super.getAPattern() + } + + final override predicate last(AstNode last, Completion c) { + // There are no cases and no default + not exists(super.getACase()) and + not exists(super.getDefault()) and + last(super.getCondition(), last, c) and + completionIsNormal(c) + or + // The last element can be the last statement in the default block + last(super.getDefault(), last, c) + or + // ... or any of the last elements in a case block + last(super.getACase(), last, c) + or + // No default and we reached the final pattern and failed to match + not exists(super.getDefault()) and + last(super.getPattern(super.getNumberOfCases() - 1), last, c) and + c.(MatchCompletion).isNonMatch() + } + + final override predicate succ(AstNode pred, AstNode succ, Completion c) { + // Preorder: Flow from the switch to the condition + pred = this and + first(super.getCondition(), succ) and + completionIsSimple(c) + or + // Flow from the condition to the first pattern + last(super.getCondition(), pred, c) and + completionIsNormal(c) and + first(super.getPattern(0), succ) + or + // Flow from a match to: + // 1. the corresponding case if the match succeeds, or + // 2. the next pattern if the match failed, or + // 3. the default case if this is the last pattern and the match failed. + exists(int i, boolean match | + last(super.getPattern(i), pred, c) and c.(MatchCompletion).getValue() = match + | + // Case 1 + match = true and + first(super.getCase(i), succ) + or + match = false and + ( + // Case 2 + first(super.getPattern(i + 1), succ) + or + // Case 3 + i = super.getNumberOfCases() - 1 and + first(super.getDefault(), succ) + ) + ) + } + } + class GotoStmtTree extends LeafTree instanceof GotoStmt { } class FunctionStmtTree extends LeafTree instanceof Function { } @@ -440,6 +543,81 @@ module Trees { override AstNode getChildNode(int i) { result = super.getElement(i) } } + class TypeConstraintTree extends LeafTree instanceof TypeConstraint { } + + class CatchClauseTree extends PreOrderTree instanceof CatchClause { + final override predicate propagatesAbnormal(Ast child) { none() } + + final override predicate last(AstNode last, Completion c) { + last(super.getBody(), last, c) + or + // The last catch type failed to matchs + last(super.getCatchType(super.getNumberOfCatchTypes() - 1), last, c) and + c.(MatchCompletion).isNonMatch() + } + + final override predicate succ(AstNode pred, AstNode succ, Completion c) { + // Preorder: Flow from the catch clause to the first catch type to test, + // or to the body if this is a catch all + pred = this and + completionIsSimple(c) and + ( + first(super.getCatchType(0), succ) + or + super.isCatchAll() and + first(super.getBody(), succ) + ) + or + // Flow from a catch type to the next catch type when it fails to match + exists(int i, boolean match | + last(super.getCatchType(i), pred, c) and + match = c.(MatchCompletion).getValue() + | + match = true and + first(super.getBody(), succ) + or + match = false and + first(super.getCatchType(i + 1), succ) + ) + } + } + + class TryStmtBlock extends PreOrderTree instanceof TryStmt { + final override predicate propagatesAbnormal(AstNode child) { child = super.getFinally() } + + final override predicate last(AstNode last, Completion c) { + last(super.getFinally(), last, c) + or + not super.hasFinally() and + ( + // Body exits without an exception + last(super.getBody(), last, c) and + completionIsNormal(c) + or + // In case there's an exception we exit by evaluating one of the catch clauses + // Note that there will always be at least one catch clause if there is no `try`. + last(super.getACatchClause(), last, c) + ) + } + + final override predicate succ(AstNode pred, AstNode succ, Completion c) { + // Preorder: The try/catch statment flows to the body + pred = this and + first(super.getBody(), succ) and + completionIsSimple(c) + or + // Flow from the body to the finally when the body didn't throw + last(super.getBody(), pred, c) and + if c instanceof ThrowCompletion + then first(super.getCatchClause(0), succ) + else first(super.getFinally(), succ) + } + } + + class ThrowStmtTree extends StandardPreOrderTree instanceof ThrowStmt { + override AstNode getChildNode(int i) { i = 0 and result = super.getPipeline() } + } + class ConstExprTree extends LeafTree instanceof ConstExpr { } class CmdExprTree extends StandardPreOrderTree instanceof CmdExpr { @@ -470,12 +648,13 @@ private module Cached { cached newtype TSuccessorType = TSuccessorSuccessor() or - TBooleanSuccessor(boolean b) { b in [false, true] } or + TBooleanSuccessor(Boolean b) or TReturnSuccessor() or TBreakSuccessor() or TContinueSuccessor() or - TRaiseSuccessor() or - TExitSuccessor() + TThrowSuccessor() or + TExitSuccessor() or + TMatchingSuccessor(Boolean b) } import Cached