Repository: avast/yaramod (language: python)

Issue:
Missing string operators



Discussion / hints:

Fix patch:
diff --git a/docs/rtd/creating_rulesets.rst b/docs/rtd/creating_rulesets.rst
index 58dffcf3..a685048f 100644
--- a/docs/rtd/creating_rulesets.rst
+++ b/docs/rtd/creating_rulesets.rst
@@ -254,6 +254,11 @@ basic expressions and find the most suitable one.
         * ``contains(rhs)`` - represents operator ``contains`` (``id("signature").contains(string_val("hello"))``)
         * ``matches(rhs)`` - represents operator ``matches`` (``id("signature").matches(regexp("^a.*b$", "i"))``)
         * ``iequals(rhs)`` - represents operator ``iequals`` (``id("signature").iequals(string_val("hello"))``)
+        * ``icontains(rhs)`` - represents operator ``icontains`` (``id("signature").icontains(string_val("hello"))``)
+        * ``endswith(rhs)`` - represents operator ``endswith`` (``id("signature").endswith(string_val("hello"))``)
+        * ``iendswith(rhs)`` - represents operator ``iendswith`` (``id("signature").iendswith(string_val("hello"))``)
+        * ``startswith(rhs)`` - represents operator ``startswith`` (``id("signature").startswith(string_val("hello"))``)
+        * ``istartswith(rhs)`` - represents operator ``istartswith`` (``id("signature").istartswith(string_val("hello"))``)
         * ``access(rhs)`` - represents operator ``.`` as access to structure (``id("pe").access("numer_of_sections")``)
         * ``__getitem__`` - represents operator ``[]`` as access to array (``id("pe").access("sections")[int_val(0)]``)
         * ``read_int8(be)`` - represents call to special function ``int8(be)`` (``int_val(100).read_int8()``)
@@ -367,6 +372,11 @@ basic expressions and find the most suitable one.
         * ``contains(rhs)`` - represents operator ``contains`` (``id("signature").contains(stringVal("hello"))``)
         * ``matches(rhs)`` - represents operator ``matches`` (``id("signature").matches(regexp("^a.*b$", "i"))``)
         * ``iequals(rhs)`` - represents operator ``iequals`` (``id("signature").iequals(stringVal("hello"))``)
+        * ``icontains(rhs)`` - represents operator ``icontains`` (``id("signature").icontains(stringVal("hello"))``)
+        * ``endswith(rhs)`` - represents operator ``endswith`` (``id("signature").endswith(stringVal("hello"))``)
+        * ``iendswith(rhs)`` - represents operator ``iendswith`` (``id("signature").iendswith(stringVal("hello"))``)
+        * ``startswith(rhs)`` - represents operator ``startswith`` (``id("signature").startswith(stringVal("hello"))``)
+        * ``istartswith(rhs)`` - represents operator ``istartswith`` (``id("signature").istartswith(stringVal("hello"))``)
         * ``access(rhs)`` - represents operator ``.`` as access to structure (``id("pe").access("numer_of_sections")``)
         * ``operator[]`` - represents operator ``[]`` as access to array (``id("pe").access("sections")[intVal(0)]``)
         * ``readInt8(be)`` - represents call to special function ``int8(be)`` (``intVal(100).readInt8()``)
diff --git a/docs/rtd/parsing_rulesets.rst b/docs/rtd/parsing_rulesets.rst
index baf1d719..25e0845d 100644
--- a/docs/rtd/parsing_rulesets.rst
+++ b/docs/rtd/parsing_rulesets.rst
@@ -339,6 +339,11 @@ All of these provide methods ``getLeftOperand()`` and ``getRightOperand()`` (``l
   * ``ContainsExpression`` - refers to ``contains`` operator (``pe.sections[0].name contains "text"``)
   * ``MatchesExpression`` - refers to ``matches`` operator (``pe.sections[0].name matches /(text|data)/``)
   * ``IequalsExpression`` - refers to ``iequals`` operator (``pe.sections[0].name iequals "text"``)
+  * ``IcontainsExpression`` - refers to ``icontains`` operator (``pe.sections[0].name icontains "text"``)
+  * ``EndsWithExpression`` - refers to ``endswith`` operator (``pe.sections[0].name endswith "text"``)
+  * ``IendsWithExpression`` - refers to ``iendswith`` operator (``pe.sections[0].name iendswith "text"``)
+  * ``StartsWithExpression`` - refers to ``startswith`` operator (``pe.sections[0].name startswith "text"``)
+  * ``IstartsWithExpression`` - refers to ``istartswith`` operator (``pe.sections[0].name istartswith "text"``)
   * ``PlusExpression`` - refers to ``+`` operator (``@str1 + 0x100``)
   * ``MinusExpression`` - refers to ``-`` operator (``@str1 - 0x100``)
   * ``MultiplyExpression`` - refers to ``*`` operator (``@str1 * 0x100``)
diff --git a/include/yaramod/builder/yara_expression_builder.h b/include/yaramod/builder/yara_expression_builder.h
index 6cbf3aa7..831e70d5 100644
--- a/include/yaramod/builder/yara_expression_builder.h
+++ b/include/yaramod/builder/yara_expression_builder.h
@@ -187,7 +187,12 @@ class YaraExpressionBuilder
 
 	YaraExpressionBuilder& contains(const YaraExpressionBuilder& other);
 	YaraExpressionBuilder& matches(const YaraExpressionBuilder& other);
+	YaraExpressionBuilder& icontains(const YaraExpressionBuilder& other);
+	YaraExpressionBuilder& iendswith(const YaraExpressionBuilder& other);
 	YaraExpressionBuilder& iequals(const YaraExpressionBuilder& other);
+	YaraExpressionBuilder& istartswith(const YaraExpressionBuilder& other);
+	YaraExpressionBuilder& startswith(const YaraExpressionBuilder& other);
+	YaraExpressionBuilder& endswith(const YaraExpressionBuilder& other);
 	YaraExpressionBuilder& defined();
 	YaraExpressionBuilder& percent();
 
diff --git a/include/yaramod/types/expressions.h b/include/yaramod/types/expressions.h
index 75cd888c..f9f74810 100644
--- a/include/yaramod/types/expressions.h
+++ b/include/yaramod/types/expressions.h
@@ -776,6 +776,26 @@ class ContainsExpression : public BinaryOpExpression
 	}
 };
 
+/**
+ * Class representing icontains case-insensitive operation on two strings.
+ *
+ * For example:
+ * @code
+ * pe.sections[0] icontains "text"
+ * @endcode
+ */
+class IcontainsExpression : public BinaryOpExpression
+{
+public:
+	template <typename ExpPtr1, typename ExpPtr2>
+	IcontainsExpression(ExpPtr1&& left, TokenIt op, ExpPtr2&& right) : BinaryOpExpression(std::forward<ExpPtr1>(left), op, std::forward<ExpPtr2>(right)) {}
+
+	virtual VisitResult accept(Visitor* v) override
+	{
+		return v->visit(this);
+	}
+};
+
 /**
  * Class representing contains operation on string and regular expression.
  *
@@ -796,6 +816,86 @@ class MatchesExpression : public BinaryOpExpression
 	}
 };
 
+/**
+ * Class representing startswith operation for string starting with relation.
+ *
+ * For example:
+ * @code
+ * pe.sections[0].name startswith ".t"
+ * @endcode
+ */
+class StartsWithExpression : public BinaryOpExpression
+{
+public:
+	template <typename ExpPtr1, typename ExpPtr2>
+	StartsWithExpression(ExpPtr1&& left, TokenIt op, ExpPtr2&& right) : BinaryOpExpression(std::forward<ExpPtr1>(left), op, std::forward<ExpPtr2>(right)) {}
+
+	virtual VisitResult accept(Visitor* v) override
+	{
+		return v->visit(this);
+	}
+};
+
+/**
+ * Class representing istartswith operation for case-insensitive string starting with relation.
+ *
+ * For example:
+ * @code
+ * pe.sections[0].name istartswith ".t"
+ * @endcode
+ */
+class IstartsWithExpression : public BinaryOpExpression
+{
+public:
+	template <typename ExpPtr1, typename ExpPtr2>
+	IstartsWithExpression(ExpPtr1&& left, TokenIt op, ExpPtr2&& right) : BinaryOpExpression(std::forward<ExpPtr1>(left), op, std::forward<ExpPtr2>(right)) {}
+
+	virtual VisitResult accept(Visitor* v) override
+	{
+		return v->visit(this);
+	}
+};
+
+/**
+ * Class representing endswith operation for string ending with relation.
+ *
+ * For example:
+ * @code
+ * pe.sections[0].name endswith "xt"
+ * @endcode
+ */
+class EndsWithExpression : public BinaryOpExpression
+{
+public:
+	template <typename ExpPtr1, typename ExpPtr2>
+	EndsWithExpression(ExpPtr1&& left, TokenIt op, ExpPtr2&& right) : BinaryOpExpression(std::forward<ExpPtr1>(left), op, std::forward<ExpPtr2>(right)) {}
+
+	virtual VisitResult accept(Visitor* v) override
+	{
+		return v->visit(this);
+	}
+};
+
+/**
+ * Class representing iendswith operation for case-insensitive string ending with relation.
+ *
+ * For example:
+ * @code
+ * pe.sections[0].name iendswith ".t"
+ * @endcode
+ */
+class IendsWithExpression : public BinaryOpExpression
+{
+public:
+	template <typename ExpPtr1, typename ExpPtr2>
+	IendsWithExpression(ExpPtr1&& left, TokenIt op, ExpPtr2&& right) : BinaryOpExpression(std::forward<ExpPtr1>(left), op, std::forward<ExpPtr2>(right)) {}
+
+	virtual VisitResult accept(Visitor* v) override
+	{
+		return v->visit(this);
+	}
+};
+
 /**
  * Class representing iequals operation for case-insensitive string compare.
  *
diff --git a/include/yaramod/types/token_type.h b/include/yaramod/types/token_type.h
index 9019cb9e..d37b03d3 100644
--- a/include/yaramod/types/token_type.h
+++ b/include/yaramod/types/token_type.h
@@ -95,7 +95,12 @@ enum class TokenType
 	FILESIZE,
 	CONTAINS,
 	MATCHES,
+	ICONTAINS,
+	IENDSWITH,
 	IEQUALS,
+	ISTARTSWITH,
+	STARTSWITH,
+	ENDSWITH,
 	SLASH,
 	STRING_LITERAL,
 	INTEGER,
diff --git a/include/yaramod/utils/modifying_visitor.h b/include/yaramod/utils/modifying_visitor.h
index aec13a9b..3e9d59e3 100644
--- a/include/yaramod/utils/modifying_visitor.h
+++ b/include/yaramod/utils/modifying_visitor.h
@@ -220,11 +220,36 @@ class ModifyingVisitor : public Visitor
 		return _handleBinaryOperation(expr);
 	}
 
+	virtual VisitResult visit(IcontainsExpression* expr) override
+	{
+		return _handleBinaryOperation(expr);
+	}
+
+	virtual VisitResult visit(IendsWithExpression* expr) override
+	{
+		return _handleBinaryOperation(expr);
+	}
+
 	virtual VisitResult visit(IequalsExpression* expr) override
 	{
 		return _handleBinaryOperation(expr);
 	}
 
+	virtual VisitResult visit(IstartsWithExpression* expr) override
+	{
+		return _handleBinaryOperation(expr);
+	}
+
+	virtual VisitResult visit(EndsWithExpression* expr) override
+	{
+		return _handleBinaryOperation(expr);
+	}
+
+	virtual VisitResult visit(StartsWithExpression* expr) override
+	{
+		return _handleBinaryOperation(expr);
+	}
+
 	virtual VisitResult visit(PlusExpression* expr) override
 	{
 		return _handleBinaryOperation(expr);
diff --git a/include/yaramod/utils/observing_visitor.h b/include/yaramod/utils/observing_visitor.h
index ea28e2c9..3473b521 100644
--- a/include/yaramod/utils/observing_visitor.h
+++ b/include/yaramod/utils/observing_visitor.h
@@ -138,6 +138,13 @@ class ObservingVisitor : public Visitor
 		return {};
 	}
 
+	virtual VisitResult visit(EndsWithExpression* expr) override
+	{
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		return {};
+	}
+
 	virtual VisitResult visit(EqExpression* expr) override
 	{
 		expr->getLeftOperand()->accept(this);
@@ -166,6 +173,20 @@ class ObservingVisitor : public Visitor
 		return {};
 	}
 
+	virtual VisitResult visit(IcontainsExpression* expr) override
+	{
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		return {};
+	}
+
+	virtual VisitResult visit(IendsWithExpression* expr) override
+	{
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		return {};
+	}
+
 	virtual VisitResult visit(IequalsExpression* expr) override
 	{
 		expr->getLeftOperand()->accept(this);
@@ -173,6 +194,13 @@ class ObservingVisitor : public Visitor
 		return {};
 	}
 
+	virtual VisitResult visit(IstartsWithExpression* expr) override
+	{
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		return {};
+	}
+
 	virtual VisitResult visit(PlusExpression* expr) override
 	{
 		expr->getLeftOperand()->accept(this);
@@ -229,6 +257,13 @@ class ObservingVisitor : public Visitor
 		return {};
 	}
 
+	virtual VisitResult visit(StartsWithExpression* expr) override
+	{
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		return {};
+	}
+
 	virtual VisitResult visit(ShiftLeftExpression* expr) override
 	{
 		expr->getLeftOperand()->accept(this);
diff --git a/include/yaramod/utils/visitor.h b/include/yaramod/utils/visitor.h
index f00a68a6..f10aa87b 100644
--- a/include/yaramod/utils/visitor.h
+++ b/include/yaramod/utils/visitor.h
@@ -33,7 +33,12 @@ class GeExpression;
 class EqExpression;
 class NeqExpression;
 class ContainsExpression;
+class IcontainsExpression;
 class MatchesExpression;
+class StartsWithExpression;
+class IstartsWithExpression;
+class EndsWithExpression;
+class IendsWithExpression;
 class IequalsExpression;
 class PlusExpression;
 class MinusExpression;
@@ -102,7 +107,12 @@ class Visitor
 	virtual VisitResult visit(EqExpression* expr) = 0;
 	virtual VisitResult visit(NeqExpression* expr) = 0;
 	virtual VisitResult visit(ContainsExpression* expr) = 0;
+	virtual VisitResult visit(IcontainsExpression* expr) = 0;
 	virtual VisitResult visit(MatchesExpression* expr) = 0;
+	virtual VisitResult visit(StartsWithExpression* expr) = 0;
+	virtual VisitResult visit(IstartsWithExpression* expr) = 0;
+	virtual VisitResult visit(EndsWithExpression* expr) = 0;
+	virtual VisitResult visit(IendsWithExpression* expr) = 0;
 	virtual VisitResult visit(IequalsExpression* expr) = 0;
 	virtual VisitResult visit(PlusExpression* expr) = 0;
 	virtual VisitResult visit(MinusExpression* expr) = 0;
diff --git a/src/builder/yara_expression_builder.cpp b/src/builder/yara_expression_builder.cpp
index c282b9dc..de08a357 100644
--- a/src/builder/yara_expression_builder.cpp
+++ b/src/builder/yara_expression_builder.cpp
@@ -539,6 +539,40 @@ YaraExpressionBuilder& YaraExpressionBuilder::matches(const YaraExpressionBuilde
 	return *this;
 }
 
+/**
+ * Applies operation icontains on two expressions.
+ *
+ * @param other The other expression.
+ *
+ * @return Builder.
+ */
+YaraExpressionBuilder& YaraExpressionBuilder::icontains(const YaraExpressionBuilder& other)
+{
+	TokenIt token = _tokenStream->emplace_back(TokenType::ICONTAINS, "icontains");
+	_tokenStream->moveAppend(other.getTokenStream());
+
+	_expr = std::make_shared<IcontainsExpression>(std::move(_expr), token, other.get());
+	setType(Expression::Type::Bool);
+	return *this;
+}
+
+/**
+ * Applies operation iendswith on two expressions.
+ *
+ * @param other The other expression.
+ *
+ * @return Builder.
+ */
+YaraExpressionBuilder& YaraExpressionBuilder::iendswith(const YaraExpressionBuilder& other)
+{
+	TokenIt token = _tokenStream->emplace_back(TokenType::IENDSWITH, "iendswith");
+	_tokenStream->moveAppend(other.getTokenStream());
+
+	_expr = std::make_shared<IendsWithExpression>(std::move(_expr), token, other.get());
+	setType(Expression::Type::Bool);
+	return *this;
+}
+
 /**
  * Applies operation iequals on two expressions.
  *
@@ -556,6 +590,57 @@ YaraExpressionBuilder& YaraExpressionBuilder::iequals(const YaraExpressionBuilde
 	return *this;
 }
 
+/**
+ * Applies operation istartswith on two expressions.
+ *
+ * @param other The other expression.
+ *
+ * @return Builder.
+ */
+YaraExpressionBuilder& YaraExpressionBuilder::istartswith(const YaraExpressionBuilder& other)
+{
+	TokenIt token = _tokenStream->emplace_back(TokenType::ISTARTSWITH, "istartswith");
+	_tokenStream->moveAppend(other.getTokenStream());
+
+	_expr = std::make_shared<IstartsWithExpression>(std::move(_expr), token, other.get());
+	setType(Expression::Type::Bool);
+	return *this;
+}
+
+/**
+ * Applies operation startswith on two expressions.
+ *
+ * @param other The other expression.
+ *
+ * @return Builder.
+ */
+YaraExpressionBuilder& YaraExpressionBuilder::startswith(const YaraExpressionBuilder& other)
+{
+	TokenIt token = _tokenStream->emplace_back(TokenType::STARTSWITH, "startswith");
+	_tokenStream->moveAppend(other.getTokenStream());
+
+	_expr = std::make_shared<StartsWithExpression>(std::move(_expr), token, other.get());
+	setType(Expression::Type::Bool);
+	return *this;
+}
+
+/**
+ * Applies operation endswith on two expressions.
+ *
+ * @param other The other expression.
+ *
+ * @return Builder.
+ */
+YaraExpressionBuilder& YaraExpressionBuilder::endswith(const YaraExpressionBuilder& other)
+{
+	TokenIt token = _tokenStream->emplace_back(TokenType::ENDSWITH, "endswith");
+	_tokenStream->moveAppend(other.getTokenStream());
+
+	_expr = std::make_shared<EndsWithExpression>(std::move(_expr), token, other.get());
+	setType(Expression::Type::Bool);
+	return *this;
+}
+
 /**
  * Applies operation defined on the expression.
  *
diff --git a/src/examples/cpp/dump_rules_ast/dumper.h b/src/examples/cpp/dump_rules_ast/dumper.h
index 7071cdc4..8201f759 100644
--- a/src/examples/cpp/dump_rules_ast/dumper.h
+++ b/src/examples/cpp/dump_rules_ast/dumper.h
@@ -197,6 +197,66 @@ class Dumper : public yaramod::ObservingVisitor, public yaramod::ObservingRegexp
 		return {};
 	}
 
+	virtual yaramod::VisitResult visit(yaramod::IcontainsExpression* expr) override
+	{
+		dump("Icontains", expr);
+		indentUp();
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		indentDown();
+		return {};
+	}
+
+	virtual yaramod::VisitResult visit(yaramod::IendsWithExpression* expr) override
+	{
+		dump("IendsWith", expr);
+		indentUp();
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		indentDown();
+		return {};
+	}
+
+	virtual yaramod::VisitResult visit(yaramod::IequalsExpression* expr) override
+	{
+		dump("Iequals", expr);
+		indentUp();
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		indentDown();
+		return {};
+	}
+
+	virtual yaramod::VisitResult visit(yaramod::IstartsWithExpression* expr) override
+	{
+		dump("IstartsWith", expr);
+		indentUp();
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		indentDown();
+		return {};
+	}
+
+	virtual yaramod::VisitResult visit(yaramod::StartsWithExpression* expr) override
+	{
+		dump("StartsWith", expr);
+		indentUp();
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		indentDown();
+		return {};
+	}
+
+	virtual yaramod::VisitResult visit(yaramod::EndsWithExpression* expr) override
+	{
+		dump("EndsWith", expr);
+		indentUp();
+		expr->getLeftOperand()->accept(this);
+		expr->getRightOperand()->accept(this);
+		indentDown();
+		return {};
+	}
+
 	virtual yaramod::VisitResult visit(yaramod::MatchesExpression* expr) override
 	{
 		dump("Matches", expr);
diff --git a/src/examples/python/dump_rules_ast.py b/src/examples/python/dump_rules_ast.py
index 24c770ee..cd2c6c1b 100644
--- a/src/examples/python/dump_rules_ast.py
+++ b/src/examples/python/dump_rules_ast.py
@@ -136,6 +136,48 @@ def visit_ContainsExpression(self, expr):
         expr.right_operand.accept(self)
         self.indent_down()
 
+    def visit_IcontainsExpression(self, expr):
+        self.dump('Icontains', expr)
+        self.indent_up()
+        expr.left_operand.accept(self)
+        expr.right_operand.accept(self)
+        self.indent_down()
+
+    def visit_IendsWithExpression(self, expr):
+        self.dump('IendsWith', expr)
+        self.indent_up()
+        expr.left_operand.accept(self)
+        expr.right_operand.accept(self)
+        self.indent_down()
+
+    def visit_IequalsExpression(self, expr):
+        self.dump('Iequals', expr)
+        self.indent_up()
+        expr.left_operand.accept(self)
+        expr.right_operand.accept(self)
+        self.indent_down()
+
+    def visit_IstartsWithExpression(self, expr):
+        self.dump('IstartsWith', expr)
+        self.indent_up()
+        expr.left_operand.accept(self)
+        expr.right_operand.accept(self)
+        self.indent_down()
+
+    def visit_StartsWithExpression(self, expr):
+        self.dump('StartsWith', expr)
+        self.indent_up()
+        expr.left_operand.accept(self)
+        expr.right_operand.accept(self)
+        self.indent_down()
+
+    def visit_EndsWithExpression(self, expr):
+        self.dump('EndsWith', expr)
+        self.indent_up()
+        expr.left_operand.accept(self)
+        expr.right_operand.accept(self)
+        self.indent_down()
+
     def visit_MatchesExpression(self, expr):
         self.dump('Matches', expr)
         self.indent_up()
diff --git a/src/parser/parser_driver.cpp b/src/parser/parser_driver.cpp
index 8ca336b5..16e89631 100644
--- a/src/parser/parser_driver.cpp
+++ b/src/parser/parser_driver.cpp
@@ -157,7 +157,12 @@ void ParserDriver::defineTokens()
 	_parser.token("in").symbol("IN").description("in").action([&](std::string_view str) -> Value { return emplace_back(TokenType::OP_IN, std::string{str}); });
 	_parser.token("filesize").symbol("FILESIZE").description("filesize").action([&](std::string_view str) -> Value { return emplace_back(TokenType::FILESIZE, std::string{str}); });
 	_parser.token("contains").symbol("CONTAINS").description("contains").action([&](std::string_view str) -> Value { return emplace_back(TokenType::CONTAINS, std::string{str}); });
+	_parser.token("icontains").symbol("ICONTAINS").description("icontains").action([&](std::string_view str) -> Value { return emplace_back(TokenType::ICONTAINS, std::string{str}); });
 	_parser.token("matches").symbol("MATCHES").description("matches").action([&](std::string_view str) -> Value { return emplace_back(TokenType::MATCHES, std::string{str}); });
+	_parser.token("startswith").symbol("STARTSWITH").description("startswith").action([&](std::string_view str) -> Value { return emplace_back(TokenType::STARTSWITH, std::string{str}); });
+	_parser.token("istartswith").symbol("ISTARTSWITH").description("istartswith").action([&](std::string_view str) -> Value { return emplace_back(TokenType::ISTARTSWITH, std::string{str}); });
+	_parser.token("endswith").symbol("ENDSWITH").description("endswith").action([&](std::string_view str) -> Value { return emplace_back(TokenType::ENDSWITH, std::string{str}); });
+	_parser.token("iendswith").symbol("IENDSWITH").description("iendswith").action([&](std::string_view str) -> Value { return emplace_back(TokenType::IENDSWITH, std::string{str}); });
 	_parser.token("iequals").symbol("IEQUALS").description("iequals").action([&](std::string_view str) -> Value { return emplace_back(TokenType::IEQUALS, std::string{str}); });
 
 	// $include
@@ -1470,6 +1475,20 @@ void ParserDriver::defineGrammar()
 			output->setUid(_uidGen.next());
 			return output;
 		})
+		.production("primary_expression", "ICONTAINS", "primary_expression", [&](auto&& args) -> Value {
+			auto left = std::move(args[0].getExpression());
+			TokenIt op_token = args[1].getTokenIt();
+			auto right = std::move(args[2].getExpression());
+			if (!left->isString())
+				error_handle(op_token->getLocation(), "operator 'icontains' expects string on the left-hand side of the expression");
+			if (!right->isString())
+				error_handle(op_token->getLocation(), "operator 'icontains' expects string on the right-hand side of the expression");
+			auto output = std::make_shared<IcontainsExpression>(std::move(left), op_token, std::move(right));
+			output->setType(Expression::Type::Bool);
+			output->setTokenStream(currentTokenStream());
+			output->setUid(_uidGen.next());
+			return output;
+		})
 		.production("primary_expression", "MATCHES", "regexp", [&](auto&& args) -> Value {
 			auto left = std::move(args[0].getExpression());
 			TokenIt op_token = args[1].getTokenIt();
@@ -1483,6 +1502,62 @@ void ParserDriver::defineGrammar()
 			output->setUid(_uidGen.next());
 			return output;
 		})
+		.production("primary_expression", "STARTSWITH", "primary_expression", [&](auto&& args) -> Value {
+			auto left = std::move(args[0].getExpression());
+			TokenIt op_token = args[1].getTokenIt();
+			auto right = std::move(args[2].getExpression());
+			if (!left->isString())
+				error_handle(op_token->getLocation(), "operator 'startswith' expects string on the left-hand side of the expression");
+			if (!right->isString())
+				error_handle(op_token->getLocation(), "operator 'startswith' expects string on the right-hand side of the expression");
+			auto output = std::make_shared<StartsWithExpression>(std::move(left), op_token, std::move(right));
+			output->setType(Expression::Type::Bool);
+			output->setTokenStream(currentTokenStream());
+			output->setUid(_uidGen.next());
+			return output;
+		})
+		.production("primary_expression", "ISTARTSWITH", "primary_expression", [&](auto&& args) -> Value {
+			auto left = std::move(args[0].getExpression());
+			TokenIt op_token = args[1].getTokenIt();
+			auto right = std::move(args[2].getExpression());
+			if (!left->isString())
+				error_handle(op_token->getLocation(), "operator 'istartswith' expects string on the left-hand side of the expression");
+			if (!right->isString())
+				error_handle(op_token->getLocation(), "operator 'istartswith' expects string on the right-hand side of the expression");
+			auto output = std::make_shared<IstartsWithExpression>(std::move(left), op_token, std::move(right));
+			output->setType(Expression::Type::Bool);
+			output->setTokenStream(currentTokenStream());
+			output->setUid(_uidGen.next());
+			return output;
+		})
+		.production("primary_expression", "ENDSWITH", "primary_expression", [&](auto&& args) -> Value {
+			auto left = std::move(args[0].getExpression());
+			TokenIt op_token = args[1].getTokenIt();
+			auto right = std::move(args[2].getExpression());
+			if (!left->isString())
+				error_handle(op_token->getLocation(), "operator 'endswith' expects string on the left-hand side of the expression");
+			if (!right->isString())
+				error_handle(op_token->getLocation(), "operator 'endswith' expects string on the right-hand side of the expression");
+			auto output = std::make_shared<EndsWithExpression>(std::move(left), op_token, std::move(right));
+			output->setType(Expression::Type::Bool);
+			output->setTokenStream(currentTokenStream());
+			output->setUid(_uidGen.next());
+			return output;
+		})
+		.production("primary_expression", "IENDSWITH", "primary_expression", [&](auto&& args) -> Value {
+			auto left = std::move(args[0].getExpression());
+			TokenIt op_token = args[1].getTokenIt();
+			auto right = std::move(args[2].getExpression());
+			if (!left->isString())
+				error_handle(op_token->getLocation(), "operator 'iendswith' expects string on the left-hand side of the expression");
+			if (!right->isString())
+				error_handle(op_token->getLocation(), "operator 'iendswith' expects string on the right-hand side of the expression");
+			auto output = std::make_shared<IendsWithExpression>(std::move(left), op_token, std::move(right));
+			output->setType(Expression::Type::Bool);
+			output->setTokenStream(currentTokenStream());
+			output->setUid(_uidGen.next());
+			return output;
+		})
 		.production("primary_expression", "IEQUALS", "primary_expression", [&](auto&& args) -> Value {
 			auto left = std::move(args[0].getExpression());
 			TokenIt op_token = args[1].getTokenIt();
diff --git a/src/python/py_visitor.cpp b/src/python/py_visitor.cpp
index a5ab0e8b..92ccdc47 100644
--- a/src/python/py_visitor.cpp
+++ b/src/python/py_visitor.cpp
@@ -41,7 +41,12 @@ void addVisitorClasses(py::module& module)
 		.def("visit_NeqExpression", py::overload_cast<NeqExpression*>(&Visitor::visit))
 		.def("visit_ContainsExpression", py::overload_cast<ContainsExpression*>(&Visitor::visit))
 		.def("visit_MatchesExpression", py::overload_cast<MatchesExpression*>(&Visitor::visit))
+		.def("visit_IcontainsExpression", py::overload_cast<IcontainsExpression*>(&Visitor::visit))
+		.def("visit_IendsWithExpression", py::overload_cast<IendsWithExpression*>(&Visitor::visit))
 		.def("visit_IequalsExpression", py::overload_cast<IequalsExpression*>(&Visitor::visit))
+		.def("visit_IstartsWithExpression", py::overload_cast<IstartsWithExpression*>(&Visitor::visit))
+		.def("visit_StartsWithExpression", py::overload_cast<StartsWithExpression*>(&Visitor::visit))
+		.def("visit_EndsWithExpression", py::overload_cast<EndsWithExpression*>(&Visitor::visit))
 		.def("visit_PlusExpression", py::overload_cast<PlusExpression*>(&Visitor::visit))
 		.def("visit_MinusExpression", py::overload_cast<MinusExpression*>(&Visitor::visit))
 		.def("visit_MultiplyExpression", py::overload_cast<MultiplyExpression*>(&Visitor::visit))
@@ -103,7 +108,12 @@ void addVisitorClasses(py::module& module)
 		.def("visit_NeqExpression", py::overload_cast<NeqExpression*>(&ObservingVisitor::visit))
 		.def("visit_ContainsExpression", py::overload_cast<ContainsExpression*>(&ObservingVisitor::visit))
 		.def("visit_MatchesExpression", py::overload_cast<MatchesExpression*>(&ObservingVisitor::visit))
+		.def("visit_IcontainsExpression", py::overload_cast<IcontainsExpression*>(&ObservingVisitor::visit))
+		.def("visit_IendsWithExpression", py::overload_cast<IendsWithExpression*>(&ObservingVisitor::visit))
 		.def("visit_IequalsExpression", py::overload_cast<IequalsExpression*>(&ObservingVisitor::visit))
+		.def("visit_IstartsWithExpression", py::overload_cast<IstartsWithExpression*>(&ObservingVisitor::visit))
+		.def("visit_StartsWithExpression", py::overload_cast<StartsWithExpression*>(&ObservingVisitor::visit))
+		.def("visit_EndsWithExpression", py::overload_cast<EndsWithExpression*>(&ObservingVisitor::visit))
 		.def("visit_PlusExpression", py::overload_cast<PlusExpression*>(&ObservingVisitor::visit))
 		.def("visit_MinusExpression", py::overload_cast<MinusExpression*>(&ObservingVisitor::visit))
 		.def("visit_MultiplyExpression", py::overload_cast<MultiplyExpression*>(&ObservingVisitor::visit))
@@ -166,7 +176,12 @@ void addVisitorClasses(py::module& module)
 		.def("visit_NeqExpression", py::overload_cast<NeqExpression*>(&ModifyingVisitor::visit))
 		.def("visit_ContainsExpression", py::overload_cast<ContainsExpression*>(&ModifyingVisitor::visit))
 		.def("visit_MatchesExpression", py::overload_cast<MatchesExpression*>(&ModifyingVisitor::visit))
+		.def("visit_IcontainsExpression", py::overload_cast<IcontainsExpression*>(&ModifyingVisitor::visit))
+		.def("visit_IendsWithExpression", py::overload_cast<IendsWithExpression*>(&ModifyingVisitor::visit))
 		.def("visit_IequalsExpression", py::overload_cast<IequalsExpression*>(&ModifyingVisitor::visit))
+		.def("visit_IstartsWithExpression", py::overload_cast<IstartsWithExpression*>(&ModifyingVisitor::visit))
+		.def("visit_StartsWithExpression", py::overload_cast<StartsWithExpression*>(&ModifyingVisitor::visit))
+		.def("visit_EndsWithExpression", py::overload_cast<EndsWithExpression*>(&ModifyingVisitor::visit))
 		.def("visit_PlusExpression", py::overload_cast<PlusExpression*>(&ModifyingVisitor::visit))
 		.def("visit_MinusExpression", py::overload_cast<MinusExpression*>(&ModifyingVisitor::visit))
 		.def("visit_MultiplyExpression", py::overload_cast<MultiplyExpression*>(&ModifyingVisitor::visit))
@@ -221,7 +236,12 @@ void addVisitorClasses(py::module& module)
 		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, NeqExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
 		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, ContainsExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
 		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, MatchesExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
+		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, IcontainsExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
+		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, IendsWithExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
 		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, IequalsExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
+		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, IstartsWithExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
+		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, StartsWithExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
+		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, EndsWithExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
 		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, PlusExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
 		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, MinusExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
 		.def("default_handler", static_cast<VisitResult(ModifyingVisitor::*)(const TokenStreamContext&, MultiplyExpression*, const VisitResult&, const VisitResult&)>(&ModifyingVisitor::defaultHandler))
diff --git a/src/python/py_visitor.h b/src/python/py_visitor.h
index b97cf574..ce3169e9 100644
--- a/src/python/py_visitor.h
+++ b/src/python/py_visitor.h
@@ -50,7 +50,12 @@ class PyVisitor : public yaramod::Visitor
 	PURE_VISIT(NeqExpression)
 	PURE_VISIT(ContainsExpression)
 	PURE_VISIT(MatchesExpression)
+	PURE_VISIT(IcontainsExpression)
+	PURE_VISIT(IendsWithExpression)
 	PURE_VISIT(IequalsExpression)
+	PURE_VISIT(IstartsWithExpression)
+	PURE_VISIT(StartsWithExpression)
+	PURE_VISIT(EndsWithExpression)
 	PURE_VISIT(PlusExpression)
 	PURE_VISIT(MinusExpression)
 	PURE_VISIT(MultiplyExpression)
@@ -126,7 +131,12 @@ class PyObservingVisitor : public yaramod::ObservingVisitor
 	VISIT(ObservingVisitor, NeqExpression)
 	VISIT(ObservingVisitor, ContainsExpression)
 	VISIT(ObservingVisitor, MatchesExpression)
+	VISIT(ObservingVisitor, IcontainsExpression)
+	VISIT(ObservingVisitor, IendsWithExpression)
 	VISIT(ObservingVisitor, IequalsExpression)
+	VISIT(ObservingVisitor, IstartsWithExpression)
+	VISIT(ObservingVisitor, StartsWithExpression)
+	VISIT(ObservingVisitor, EndsWithExpression)
 	VISIT(ObservingVisitor, PlusExpression)
 	VISIT(ObservingVisitor, MinusExpression)
 	VISIT(ObservingVisitor, MultiplyExpression)
@@ -191,7 +201,12 @@ class PyModifyingVisitor : public yaramod::ModifyingVisitor
 	VISIT(ModifyingVisitor, NeqExpression)
 	VISIT(ModifyingVisitor, ContainsExpression)
 	VISIT(ModifyingVisitor, MatchesExpression)
+	VISIT(ModifyingVisitor, IcontainsExpression)
+	VISIT(ModifyingVisitor, IendsWithExpression)
 	VISIT(ModifyingVisitor, IequalsExpression)
+	VISIT(ModifyingVisitor, IstartsWithExpression)
+	VISIT(ModifyingVisitor, StartsWithExpression)
+	VISIT(ModifyingVisitor, EndsWithExpression)
 	VISIT(ModifyingVisitor, PlusExpression)
 	VISIT(ModifyingVisitor, MinusExpression)
 	VISIT(ModifyingVisitor, MultiplyExpression)
diff --git a/src/python/typings/__init__.pyi b/src/python/typings/__init__.pyi
index 8caeb9d7..79bebf64 100644
--- a/src/python/typings/__init__.pyi
+++ b/src/python/typings/__init__.pyi
@@ -82,6 +82,9 @@ class DivideExpression(BinaryOpExpression):
 class DoubleLiteralExpression(_DoubleLiteralExpression):
     def __init__(self, *args, **kwargs) -> None: ...
 
+class EndsWithExpression(BinaryOpExpression):
+    def __init__(self, *args, **kwargs) -> None: ...
+
 class EntrypointExpression(KeywordExpression):
     def __init__(self, *args, **kwargs) -> None: ...
 
@@ -218,9 +221,18 @@ class IdWildcardExpression(Expression):
     id: str
     def __init__(self, *args, **kwargs) -> None: ...
 
+class IcontainsExpression(BinaryOpExpression):
+    def __init__(self, *args, **kwargs) -> None: ...
+
+class IendsWithExpression(BinaryOpExpression):
+    def __init__(self, *args, **kwargs) -> None: ...
+
 class IequalsExpression(BinaryOpExpression):
     def __init__(self, *args, **kwargs) -> None: ...
 
+class IstartsWithExpression(BinaryOpExpression):
+    def __init__(self, *args, **kwargs) -> None: ...
+
 class IntFunctionEndianness:
     __members__: ClassVar[dict] = ...  # read-only
     Big: ClassVar[IntFunctionEndianness] = ...
@@ -391,8 +403,18 @@ class ModifyingVisitor(Visitor):
     @overload
     def default_handler(self, arg0: TokenStreamContext, arg1: MatchesExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
     @overload
+    def default_handler(self, arg0: TokenStreamContext, arg1: IcontainsExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
+    @overload
+    def default_handler(self, arg0: TokenStreamContext, arg1: IendsWithExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
+    @overload
     def default_handler(self, arg0: TokenStreamContext, arg1: IequalsExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
     @overload
+    def default_handler(self, arg0: TokenStreamContext, arg1: IstartsWithExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
+    @overload
+    def default_handler(self, arg0: TokenStreamContext, arg1: StartsWithExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
+    @overload
+    def default_handler(self, arg0: TokenStreamContext, arg1: EndsWithExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
+    @overload
     def default_handler(self, arg0: TokenStreamContext, arg1: PlusExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
     @overload
     def default_handler(self, arg0: TokenStreamContext, arg1: MinusExpression, arg2: Union[Expression,VisitAction], arg3: Union[Expression,VisitAction]) -> Union[Expression,VisitAction]: ...
@@ -450,6 +472,7 @@ class ModifyingVisitor(Visitor):
     def visit_DefinedExpression(self, arg0: DefinedExpression) -> Union[Expression,VisitAction]: ...
     def visit_DivideExpression(self, arg0: DivideExpression) -> Union[Expression,VisitAction]: ...
     def visit_DoubleLiteralExpression(self, arg0: DoubleLiteralExpression) -> Union[Expression,VisitAction]: ...
+    def visit_EndsWithExpression(self, arg0: EndsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_EntrypointExpression(self, arg0: EntrypointExpression) -> Union[Expression,VisitAction]: ...
     def visit_EqExpression(self, arg0: EqExpression) -> Union[Expression,VisitAction]: ...
     def visit_FilesizeExpression(self, arg0: FilesizeExpression) -> Union[Expression,VisitAction]: ...
@@ -459,11 +482,14 @@ class ModifyingVisitor(Visitor):
     def visit_FunctionCallExpression(self, arg0: FunctionCallExpression) -> Union[Expression,VisitAction]: ...
     def visit_GeExpression(self, arg0: GeExpression) -> Union[Expression,VisitAction]: ...
     def visit_GtExpression(self, arg0: GtExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IcontainsExpression(self, arg0: IcontainsExpression) -> Union[Expression,VisitAction]: ...
     def visit_IdExpression(self, arg0: IdExpression) -> Union[Expression,VisitAction]: ...
     def visit_IdWildcardExpression(self, arg0: IdWildcardExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IendsWithExpression(self, arg0: IendsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_IequalsExpression(self, arg0: IequalsExpression) -> Union[Expression,VisitAction]: ...
     def visit_IntFunctionExpression(self, arg0: IntFunctionExpression) -> Union[Expression,VisitAction]: ...
     def visit_IntLiteralExpression(self, arg0: IntLiteralExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IstartsWithExpression(self, arg0: IstartsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_IterableExpression(self, arg0: IterableExpression) -> Union[Expression,VisitAction]: ...
     def visit_LeExpression(self, arg0: LeExpression) -> Union[Expression,VisitAction]: ...
     def visit_LtExpression(self, arg0: LtExpression) -> Union[Expression,VisitAction]: ...
@@ -484,6 +510,7 @@ class ModifyingVisitor(Visitor):
     def visit_SetExpression(self, arg0: SetExpression) -> Union[Expression,VisitAction]: ...
     def visit_ShiftLeftExpression(self, arg0: ShiftLeftExpression) -> Union[Expression,VisitAction]: ...
     def visit_ShiftRightExpression(self, arg0: ShiftRightExpression) -> Union[Expression,VisitAction]: ...
+    def visit_StartsWithExpression(self, arg0: StartsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringAtExpression(self, arg0: StringAtExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringCountExpression(self, arg0: StringCountExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringExpression(self, arg0: StringExpression) -> Union[Expression,VisitAction]: ...
@@ -561,6 +588,7 @@ class ObservingVisitor(Visitor):
     def visit_DefinedExpression(self, arg0: DefinedExpression) -> Union[Expression,VisitAction]: ...
     def visit_DivideExpression(self, arg0: DivideExpression) -> Union[Expression,VisitAction]: ...
     def visit_DoubleLiteralExpression(self, arg0: DoubleLiteralExpression) -> Union[Expression,VisitAction]: ...
+    def visit_EndsWithExpression(self, arg0: EndsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_EntrypointExpression(self, arg0: EntrypointExpression) -> Union[Expression,VisitAction]: ...
     def visit_EqExpression(self, arg0: EqExpression) -> Union[Expression,VisitAction]: ...
     def visit_FilesizeExpression(self, arg0: FilesizeExpression) -> Union[Expression,VisitAction]: ...
@@ -570,11 +598,14 @@ class ObservingVisitor(Visitor):
     def visit_FunctionCallExpression(self, arg0: FunctionCallExpression) -> Union[Expression,VisitAction]: ...
     def visit_GeExpression(self, arg0: GeExpression) -> Union[Expression,VisitAction]: ...
     def visit_GtExpression(self, arg0: GtExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IcontainsExpression(self, arg0: IcontainsExpression) -> Union[Expression,VisitAction]: ...
     def visit_IdExpression(self, arg0: IdExpression) -> Union[Expression,VisitAction]: ...
     def visit_IdWildcardExpression(self, arg0: IdWildcardExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IendsWithExpression(self, arg0: IendsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_IequalsExpression(self, arg0: IequalsExpression) -> Union[Expression,VisitAction]: ...
     def visit_IntFunctionExpression(self, arg0: IntFunctionExpression) -> Union[Expression,VisitAction]: ...
     def visit_IntLiteralExpression(self, arg0: IntLiteralExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IstartsWithExpression(self, arg0: IstartsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_IterableExpression(self, arg0: IterableExpression) -> Union[Expression,VisitAction]: ...
     def visit_LeExpression(self, arg0: LeExpression) -> Union[Expression,VisitAction]: ...
     def visit_LtExpression(self, arg0: LtExpression) -> Union[Expression,VisitAction]: ...
@@ -595,6 +626,7 @@ class ObservingVisitor(Visitor):
     def visit_SetExpression(self, arg0: SetExpression) -> Union[Expression,VisitAction]: ...
     def visit_ShiftLeftExpression(self, arg0: ShiftLeftExpression) -> Union[Expression,VisitAction]: ...
     def visit_ShiftRightExpression(self, arg0: ShiftRightExpression) -> Union[Expression,VisitAction]: ...
+    def visit_StartsWithExpression(self, arg0: StartsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringAtExpression(self, arg0: StringAtExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringCountExpression(self, arg0: StringCountExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringExpression(self, arg0: StringExpression) -> Union[Expression,VisitAction]: ...
@@ -851,6 +883,9 @@ class ShiftLeftExpression(BinaryOpExpression):
 class ShiftRightExpression(BinaryOpExpression):
     def __init__(self, *args, **kwargs) -> None: ...
 
+class StartsWithExpression(BinaryOpExpression):
+    def __init__(self, *args, **kwargs) -> None: ...
+
 class String:
     def __init__(self, *args, **kwargs) -> None: ...
     def remove_modifiers(self) -> None: ...
@@ -1139,6 +1174,7 @@ class TokenType:
     Dot: ClassVar[TokenType] = ...
     Double: ClassVar[TokenType] = ...
     DoubleDot: ClassVar[TokenType] = ...
+    EndsWith: ClassVar[TokenType] = ...
     Entrypoint: ClassVar[TokenType] = ...
     Eq: ClassVar[TokenType] = ...
     Filesize: ClassVar[TokenType] = ...
@@ -1162,8 +1198,10 @@ class TokenType:
     HexWildcard: ClassVar[TokenType] = ...
     HexWildcardHigh: ClassVar[TokenType] = ...
     HexWildcardLow: ClassVar[TokenType] = ...
+    Icontains: ClassVar[TokenType] = ...
     Id: ClassVar[TokenType] = ...
     IdWildcard: ClassVar[TokenType] = ...
+    IendsWith: ClassVar[TokenType] = ...
     Iequals: ClassVar[TokenType] = ...
     ImportKeyword: ClassVar[TokenType] = ...
     ImportModule: ClassVar[TokenType] = ...
@@ -1172,6 +1210,7 @@ class TokenType:
     Integer: ClassVar[TokenType] = ...
     IntegerFunction: ClassVar[TokenType] = ...
     Invalid: ClassVar[TokenType] = ...
+    IstartsWith: ClassVar[TokenType] = ...
     Lcb: ClassVar[TokenType] = ...
     Le: ClassVar[TokenType] = ...
     Lp: ClassVar[TokenType] = ...
@@ -1231,6 +1270,7 @@ class TokenType:
     ShiftLeft: ClassVar[TokenType] = ...
     ShiftRight: ClassVar[TokenType] = ...
     Slash: ClassVar[TokenType] = ...
+    StartsWith: ClassVar[TokenType] = ...
     StringCount: ClassVar[TokenType] = ...
     StringId: ClassVar[TokenType] = ...
     StringIdAfterNewline: ClassVar[TokenType] = ...
@@ -1310,6 +1350,7 @@ class Visitor:
     def visit_DefinedExpression(self, arg0: DefinedExpression) -> Union[Expression,VisitAction]: ...
     def visit_DivideExpression(self, arg0: DivideExpression) -> Union[Expression,VisitAction]: ...
     def visit_DoubleLiteralExpression(self, arg0: DoubleLiteralExpression) -> Union[Expression,VisitAction]: ...
+    def visit_EndsWithExpression(self, arg0: EndsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_EntrypointExpression(self, arg0: EntrypointExpression) -> Union[Expression,VisitAction]: ...
     def visit_EqExpression(self, arg0: EqExpression) -> Union[Expression,VisitAction]: ...
     def visit_FilesizeExpression(self, arg0: FilesizeExpression) -> Union[Expression,VisitAction]: ...
@@ -1319,11 +1360,14 @@ class Visitor:
     def visit_FunctionCallExpression(self, arg0: FunctionCallExpression) -> Union[Expression,VisitAction]: ...
     def visit_GeExpression(self, arg0: GeExpression) -> Union[Expression,VisitAction]: ...
     def visit_GtExpression(self, arg0: GtExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IcontainsExpression(self, arg0: IcontainsExpression) -> Union[Expression,VisitAction]: ...
     def visit_IdExpression(self, arg0: IdExpression) -> Union[Expression,VisitAction]: ...
     def visit_IdWildcardExpression(self, arg0: IdWildcardExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IendsWithExpression(self, arg0: IendsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_IequalsExpression(self, arg0: IequalsExpression) -> Union[Expression,VisitAction]: ...
     def visit_IntFunctionExpression(self, arg0: IntFunctionExpression) -> Union[Expression,VisitAction]: ...
     def visit_IntLiteralExpression(self, arg0: IntLiteralExpression) -> Union[Expression,VisitAction]: ...
+    def visit_IstartsWithExpression(self, arg0: IstartsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_IterableExpression(self, arg0: IterableExpression) -> Union[Expression,VisitAction]: ...
     def visit_LeExpression(self, arg0: LeExpression) -> Union[Expression,VisitAction]: ...
     def visit_LtExpression(self, arg0: LtExpression) -> Union[Expression,VisitAction]: ...
@@ -1344,6 +1388,7 @@ class Visitor:
     def visit_SetExpression(self, arg0: SetExpression) -> Union[Expression,VisitAction]: ...
     def visit_ShiftLeftExpression(self, arg0: ShiftLeftExpression) -> Union[Expression,VisitAction]: ...
     def visit_ShiftRightExpression(self, arg0: ShiftRightExpression) -> Union[Expression,VisitAction]: ...
+    def visit_StartsWithExpression(self, arg0: StartsWithExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringAtExpression(self, arg0: StringAtExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringCountExpression(self, arg0: StringCountExpression) -> Union[Expression,VisitAction]: ...
     def visit_StringExpression(self, arg0: StringExpression) -> Union[Expression,VisitAction]: ...
@@ -1376,8 +1421,12 @@ class YaraExpressionBuilder:
     def comment_behind(self, message: str, multiline: bool = ..., indent: str = ..., linebreak: bool = ...) -> YaraExpressionBuilder: ...
     def contains(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def defined(self) -> YaraExpressionBuilder: ...
+    def endswith(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def get(self) -> Expression: ...
+    def icontains(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
+    def iendswith(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def iequals(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
+    def istartswith(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def matches(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def percent(self) -> YaraExpressionBuilder: ...
     def read_int16(self, arg0: IntFunctionEndianness) -> YaraExpressionBuilder: ...
@@ -1386,6 +1435,7 @@ class YaraExpressionBuilder:
     def read_uint16(self, arg0: IntFunctionEndianness) -> YaraExpressionBuilder: ...
     def read_uint32(self, arg0: IntFunctionEndianness) -> YaraExpressionBuilder: ...
     def read_uint8(self, arg0: IntFunctionEndianness) -> YaraExpressionBuilder: ...
+    def startswith(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def __add__(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def __and__(self, arg0: YaraExpressionBuilder) -> YaraExpressionBuilder: ...
     def __call__(self, *args) -> YaraExpressionBuilder: ...
diff --git a/src/python/yaramod_python.cpp b/src/python/yaramod_python.cpp
index dcfa7325..cf08b65d 100644
--- a/src/python/yaramod_python.cpp
+++ b/src/python/yaramod_python.cpp
@@ -201,7 +201,12 @@ void addEnums(py::module& module)
 		.value("Filesize", TokenType::FILESIZE)
 		.value("Contains", TokenType::CONTAINS)
 		.value("Matches", TokenType::MATCHES)
+		.value("Icontains", TokenType::ICONTAINS)
+		.value("IendsWith", TokenType::IENDSWITH)
 		.value("Iequals", TokenType::IEQUALS)
+		.value("IstartsWith", TokenType::ISTARTSWITH)
+		.value("StartsWith", TokenType::STARTSWITH)
+		.value("EndsWith", TokenType::ENDSWITH)
 		.value("Slash", TokenType::SLASH)
 		.value("StringLiteral", TokenType::STRING_LITERAL)
 		.value("Integer", TokenType::INTEGER)
@@ -658,7 +663,12 @@ void addExpressionClasses(py::module& module)
 	binaryOpClass<NeqExpression>(module, "NeqExpression");
 	binaryOpClass<ContainsExpression>(module, "ContainsExpression");
 	binaryOpClass<MatchesExpression>(module, "MatchesExpression");
+	binaryOpClass<IcontainsExpression>(module, "IcontainsExpression");
+	binaryOpClass<IendsWithExpression>(module, "IendsWithExpression");
 	binaryOpClass<IequalsExpression>(module, "IequalsExpression");
+	binaryOpClass<IstartsWithExpression>(module, "IstartsWithExpression");
+	binaryOpClass<StartsWithExpression>(module, "StartsWithExpression");
+	binaryOpClass<EndsWithExpression>(module, "EndsWithExpression");
 	binaryOpClass<PlusExpression>(module, "PlusExpression");
 	binaryOpClass<MinusExpression>(module, "MinusExpression");
 	binaryOpClass<MultiplyExpression>(module, "MultiplyExpression");
@@ -882,7 +892,12 @@ void addBuilderClasses(py::module& module)
 		.def("comment_behind", &YaraExpressionBuilder::commentBehind, py::arg("message"), py::arg("multiline") = false, py::arg("indent") = "", py::arg("linebreak") = true)
 		.def("contains", &YaraExpressionBuilder::contains)
 		.def("matches", &YaraExpressionBuilder::matches)
+		.def("icontains", &YaraExpressionBuilder::icontains)
+		.def("iendswith", &YaraExpressionBuilder::iendswith)
 		.def("iequals", &YaraExpressionBuilder::iequals)
+		.def("istartswith", &YaraExpressionBuilder::istartswith)
+		.def("startswith", &YaraExpressionBuilder::startswith)
+		.def("endswith", &YaraExpressionBuilder::endswith)
 		.def("defined", &YaraExpressionBuilder::defined)
 		.def("percent", &YaraExpressionBuilder::percent)
 		.def("read_int8", &YaraExpressionBuilder::readInt8)


Test patch:
diff --git a/tests/cpp/builder_tests.cpp b/tests/cpp/builder_tests.cpp
index c194be58..2301522d 100644
--- a/tests/cpp/builder_tests.cpp
+++ b/tests/cpp/builder_tests.cpp
@@ -2170,5 +2170,175 @@ StringAsBoolInConditionWorks) {
 )", yaraFile->getTextFormatted());
 }
 
+TEST_F(BuilderTests,
+IcontainsWorks) {
+	auto cond = id("pe").access("pdb_path").icontains(stringVal("C:\\PATH")).get();
+
+	YaraRuleBuilder newRule;
+	auto rule = newRule
+			.withName("icontains_builder")
+			.withCondition(cond)
+			.get();
+
+	YaraFileBuilder newFile;
+	auto yaraFile = newFile
+			.withModule("pe")
+			.withRule(std::move(rule))
+			.get(true);
+
+	ASSERT_NE(nullptr, yaraFile);
+	EXPECT_EQ(R"(import "pe"
+
+rule icontains_builder {
+	condition:
+		pe.pdb_path icontains "C:\\PATH"
+})", yaraFile->getText());
+
+	EXPECT_EQ(R"(import "pe"
+
+rule icontains_builder
+{
+	condition:
+		pe.pdb_path icontains "C:\\PATH"
+}
+)", yaraFile->getTextFormatted());
+}
+
+TEST_F(BuilderTests,
+IendsWithWorks) {
+	auto cond = id("pe").access("pdb_path").iendswith(stringVal("C:\\PATH")).get();
+
+	YaraRuleBuilder newRule;
+	auto rule = newRule
+			.withName("iendswith_builder")
+			.withCondition(cond)
+			.get();
+
+	YaraFileBuilder newFile;
+	auto yaraFile = newFile
+			.withModule("pe")
+			.withRule(std::move(rule))
+			.get(true);
+
+	ASSERT_NE(nullptr, yaraFile);
+	EXPECT_EQ(R"(import "pe"
+
+rule iendswith_builder {
+	condition:
+		pe.pdb_path iendswith "C:\\PATH"
+})", yaraFile->getText());
+
+	EXPECT_EQ(R"(import "pe"
+
+rule iendswith_builder
+{
+	condition:
+		pe.pdb_path iendswith "C:\\PATH"
+}
+)", yaraFile->getTextFormatted());
+}
+
+TEST_F(BuilderTests,
+IstartsWithWorks) {
+	auto cond = id("pe").access("pdb_path").istartswith(stringVal("C:\\PATH")).get();
+
+	YaraRuleBuilder newRule;
+	auto rule = newRule
+			.withName("istartswith_builder")
+			.withCondition(cond)
+			.get();
+
+	YaraFileBuilder newFile;
+	auto yaraFile = newFile
+			.withModule("pe")
+			.withRule(std::move(rule))
+			.get(true);
+
+	ASSERT_NE(nullptr, yaraFile);
+	EXPECT_EQ(R"(import "pe"
+
+rule istartswith_builder {
+	condition:
+		pe.pdb_path istartswith "C:\\PATH"
+})", yaraFile->getText());
+
+	EXPECT_EQ(R"(import "pe"
+
+rule istartswith_builder
+{
+	condition:
+		pe.pdb_path istartswith "C:\\PATH"
+}
+)", yaraFile->getTextFormatted());
+}
+
+TEST_F(BuilderTests,
+StartsWithWorks) {
+	auto cond = id("pe").access("pdb_path").startswith(stringVal("C:\\PATH")).get();
+
+	YaraRuleBuilder newRule;
+	auto rule = newRule
+			.withName("startswith_builder")
+			.withCondition(cond)
+			.get();
+
+	YaraFileBuilder newFile;
+	auto yaraFile = newFile
+			.withModule("pe")
+			.withRule(std::move(rule))
+			.get(true);
+
+	ASSERT_NE(nullptr, yaraFile);
+	EXPECT_EQ(R"(import "pe"
+
+rule startswith_builder {
+	condition:
+		pe.pdb_path startswith "C:\\PATH"
+})", yaraFile->getText());
+
+	EXPECT_EQ(R"(import "pe"
+
+rule startswith_builder
+{
+	condition:
+		pe.pdb_path startswith "C:\\PATH"
+}
+)", yaraFile->getTextFormatted());
+}
+
+TEST_F(BuilderTests,
+EndsWithWorks) {
+	auto cond = id("pe").access("pdb_path").endswith(stringVal("C:\\PATH")).get();
+
+	YaraRuleBuilder newRule;
+	auto rule = newRule
+			.withName("endswith_builder")
+			.withCondition(cond)
+			.get();
+
+	YaraFileBuilder newFile;
+	auto yaraFile = newFile
+			.withModule("pe")
+			.withRule(std::move(rule))
+			.get(true);
+
+	ASSERT_NE(nullptr, yaraFile);
+	EXPECT_EQ(R"(import "pe"
+
+rule endswith_builder {
+	condition:
+		pe.pdb_path endswith "C:\\PATH"
+})", yaraFile->getText());
+
+	EXPECT_EQ(R"(import "pe"
+
+rule endswith_builder
+{
+	condition:
+		pe.pdb_path endswith "C:\\PATH"
+}
+)", yaraFile->getTextFormatted());
+}
+
 }
 }
diff --git a/tests/cpp/parser_tests.cpp b/tests/cpp/parser_tests.cpp
index d0fafd83..4428bb28 100644
--- a/tests/cpp/parser_tests.cpp
+++ b/tests/cpp/parser_tests.cpp
@@ -8087,5 +8087,120 @@ HexStringNotWorks) {
 )", yaraFile.getTextFormatted());
 }
 
+TEST_F(ParserTests,
+IcontainsExpression) {
+	prepareInput(
+R"(import "pe"
+
+rule icontains_expr
+{
+	condition:
+		pe.sections[0].name icontains ".TEXT"
+}
+)");
+
+	EXPECT_TRUE(driver.parse(input));
+	ASSERT_EQ(1u, driver.getParsedFile().getRules().size());
+	const auto& rule = driver.getParsedFile().getRules()[0];
+
+	EXPECT_EQ("pe.sections[0].name icontains \".TEXT\"", rule->getCondition()->getText());
+	EXPECT_EQ("pe", rule->getCondition()->getFirstTokenIt()->getPureText());
+	EXPECT_EQ("\".TEXT\"", rule->getCondition()->getLastTokenIt()->getText());
+
+	EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted());
+}
+
+TEST_F(ParserTests,
+IendsWithExpression) {
+	prepareInput(
+R"(import "pe"
+
+rule iendswith_expr
+{
+	condition:
+		pe.sections[0].name iendswith ".TEXT"
+}
+)");
+
+	EXPECT_TRUE(driver.parse(input));
+	ASSERT_EQ(1u, driver.getParsedFile().getRules().size());
+	const auto& rule = driver.getParsedFile().getRules()[0];
+
+	EXPECT_EQ("pe.sections[0].name iendswith \".TEXT\"", rule->getCondition()->getText());
+	EXPECT_EQ("pe", rule->getCondition()->getFirstTokenIt()->getPureText());
+	EXPECT_EQ("\".TEXT\"", rule->getCondition()->getLastTokenIt()->getText());
+
+	EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted());
+}
+
+TEST_F(ParserTests,
+IstartsWithExpression) {
+	prepareInput(
+R"(import "pe"
+
+rule istartswith_expr
+{
+	condition:
+		pe.sections[0].name istartswith ".TEXT"
+}
+)");
+
+	EXPECT_TRUE(driver.parse(input));
+	ASSERT_EQ(1u, driver.getParsedFile().getRules().size());
+	const auto& rule = driver.getParsedFile().getRules()[0];
+
+	EXPECT_EQ("pe.sections[0].name istartswith \".TEXT\"", rule->getCondition()->getText());
+	EXPECT_EQ("pe", rule->getCondition()->getFirstTokenIt()->getPureText());
+	EXPECT_EQ("\".TEXT\"", rule->getCondition()->getLastTokenIt()->getText());
+
+	EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted());
+}
+
+TEST_F(ParserTests,
+StartsWithExpression) {
+	prepareInput(
+R"(import "pe"
+
+rule startswith_expr
+{
+	condition:
+		pe.sections[0].name startswith ".TEXT"
+}
+)");
+
+	EXPECT_TRUE(driver.parse(input));
+	ASSERT_EQ(1u, driver.getParsedFile().getRules().size());
+	const auto& rule = driver.getParsedFile().getRules()[0];
+
+	EXPECT_EQ("pe.sections[0].name startswith \".TEXT\"", rule->getCondition()->getText());
+	EXPECT_EQ("pe", rule->getCondition()->getFirstTokenIt()->getPureText());
+	EXPECT_EQ("\".TEXT\"", rule->getCondition()->getLastTokenIt()->getText());
+
+	EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted());
+}
+
+TEST_F(ParserTests,
+EndsWithExpression) {
+	prepareInput(
+R"(import "pe"
+
+rule endswith_expr
+{
+	condition:
+		pe.sections[0].name endswith ".TEXT"
+}
+)");
+
+	EXPECT_TRUE(driver.parse(input));
+	ASSERT_EQ(1u, driver.getParsedFile().getRules().size());
+	const auto& rule = driver.getParsedFile().getRules()[0];
+
+	EXPECT_EQ("pe.sections[0].name endswith \".TEXT\"", rule->getCondition()->getText());
+	EXPECT_EQ("pe", rule->getCondition()->getFirstTokenIt()->getPureText());
+	EXPECT_EQ("\".TEXT\"", rule->getCondition()->getLastTokenIt()->getText());
+
+	EXPECT_EQ(input_text, driver.getParsedFile().getTextFormatted());
+}
+
 }
 }