From 61c24ebcbbb84169e8af6e410ca247293322c9a4 Mon Sep 17 00:00:00 2001 From: Ange Albertini Date: Thu, 28 May 2026 13:09:53 +0000 Subject: [PATCH 01/26] RelativeVirtualAddress deprecation warning --- capa/features/address.py | 9 +++++++++ tests/test_engine.py | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/capa/features/address.py b/capa/features/address.py index ea152c87..1aacf226 100644 --- a/capa/features/address.py +++ b/capa/features/address.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +import warnings class Address(abc.ABC): @@ -131,6 +132,14 @@ class DynamicCallAddress(Address): class RelativeVirtualAddress(int, Address): """a memory address relative to a base address""" + def __new__(cls, *args, **kwargs): + warnings.warn( + "RelativeVirtualAddress is deprecated", + DeprecationWarning, + stacklevel=2 + ) + return super().__new__(cls, *args, **kwargs) + def __repr__(self): return f"relative(0x{self:x})" diff --git a/tests/test_engine.py b/tests/test_engine.py index 1d4ba1f2..c8f57b5e 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import warnings + import pytest import capa.features.address @@ -23,6 +25,7 @@ from capa.features.address import ( DynamicCallAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress, + RelativeVirtualAddress, ) ADDR1 = capa.features.address.AbsoluteVirtualAddress(0x401001) @@ -55,6 +58,13 @@ def test_no_address_hash(): assert d[addr_zero] == "zero" +def test_relative_address(): + with pytest.raises(DeprecationWarning): + warnings.filterwarnings("error", category=DeprecationWarning) + _ = RelativeVirtualAddress(0) + warnings.resetwarnings() + + def test_dn_token_offset_address_cross_type_eq(): addr = DNTokenOffsetAddress(0x1000, 0x10) assert (addr == AbsoluteVirtualAddress(0x1010)) is False From 7f458f1844faaf777774ee6ac84614fa790d28df Mon Sep 17 00:00:00 2001 From: Ange Albertini Date: Thu, 28 May 2026 13:23:28 +0000 Subject: [PATCH 02/26] updated Changelog (RVA deprecation) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b0d4e4..5678f185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -146,6 +146,8 @@ This release includes Ghidra PyGhidra support, performance improvements, depende ### Breaking Changes +- deprecate RelativeVirtualAddress @corkamig #3072 + ### New Rules (26) - nursery/run-as-nodejs-native-module mehunhoff@google.com From c134af23044f60dee3091dae2bd29598bc4bd3fc Mon Sep 17 00:00:00 2001 From: Ange Albertini Date: Thu, 28 May 2026 13:34:48 +0000 Subject: [PATCH 03/26] Formatting fix --- capa/features/address.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/capa/features/address.py b/capa/features/address.py index 1aacf226..1a0ef966 100644 --- a/capa/features/address.py +++ b/capa/features/address.py @@ -133,11 +133,7 @@ class RelativeVirtualAddress(int, Address): """a memory address relative to a base address""" def __new__(cls, *args, **kwargs): - warnings.warn( - "RelativeVirtualAddress is deprecated", - DeprecationWarning, - stacklevel=2 - ) + warnings.warn("RelativeVirtualAddress is deprecated", DeprecationWarning, stacklevel=2) return super().__new__(cls, *args, **kwargs) def __repr__(self): From 7962d97b9a418159c18146ea016044166f4ef3e1 Mon Sep 17 00:00:00 2001 From: Ange Albertini Date: Thu, 28 May 2026 14:00:41 +0000 Subject: [PATCH 04/26] Better test --- tests/test_engine.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_engine.py b/tests/test_engine.py index c8f57b5e..637fa572 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import warnings - import pytest import capa.features.address @@ -59,10 +57,8 @@ def test_no_address_hash(): def test_relative_address(): - with pytest.raises(DeprecationWarning): - warnings.filterwarnings("error", category=DeprecationWarning) + with pytest.warns(DeprecationWarning): _ = RelativeVirtualAddress(0) - warnings.resetwarnings() def test_dn_token_offset_address_cross_type_eq(): From a14b463541c66b61dc2937010a0c5e870a62aa57 Mon Sep 17 00:00:00 2001 From: Ange Albertini <105304039+corkamig@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:24:39 +0200 Subject: [PATCH 05/26] More information for RVA deprecation --- capa/features/address.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/capa/features/address.py b/capa/features/address.py index 1a0ef966..f8ab6378 100644 --- a/capa/features/address.py +++ b/capa/features/address.py @@ -133,7 +133,8 @@ class RelativeVirtualAddress(int, Address): """a memory address relative to a base address""" def __new__(cls, *args, **kwargs): - warnings.warn("RelativeVirtualAddress is deprecated", DeprecationWarning, stacklevel=2) + #TODO(corkamig): Removal for v3.5.0 + warnings.warn("RelativeVirtualAddress is deprecated - cf issue #3072", DeprecationWarning, stacklevel=2) return super().__new__(cls, *args, **kwargs) def __repr__(self): From 687e07320e5ef1b249243008d229ef7026e3299f Mon Sep 17 00:00:00 2001 From: Ange Albertini <105304039+corkamig@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:31:40 +0200 Subject: [PATCH 06/26] Issue link --- capa/features/address.py | 1 + 1 file changed, 1 insertion(+) diff --git a/capa/features/address.py b/capa/features/address.py index f8ab6378..0d187a11 100644 --- a/capa/features/address.py +++ b/capa/features/address.py @@ -134,6 +134,7 @@ class RelativeVirtualAddress(int, Address): def __new__(cls, *args, **kwargs): #TODO(corkamig): Removal for v3.5.0 + #https://github.com/mandiant/capa/issues/3072 warnings.warn("RelativeVirtualAddress is deprecated - cf issue #3072", DeprecationWarning, stacklevel=2) return super().__new__(cls, *args, **kwargs) From cfff133ae01f96261b0cbfa2421fcebad9775c49 Mon Sep 17 00:00:00 2001 From: Ange Albertini Date: Mon, 1 Jun 2026 15:38:21 +0000 Subject: [PATCH 07/26] Formatting --- capa/features/address.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capa/features/address.py b/capa/features/address.py index 0d187a11..98b5998e 100644 --- a/capa/features/address.py +++ b/capa/features/address.py @@ -133,8 +133,8 @@ class RelativeVirtualAddress(int, Address): """a memory address relative to a base address""" def __new__(cls, *args, **kwargs): - #TODO(corkamig): Removal for v3.5.0 - #https://github.com/mandiant/capa/issues/3072 + # TODO(corkamig): Removal for v3.5.0 + # https://github.com/mandiant/capa/issues/3072 warnings.warn("RelativeVirtualAddress is deprecated - cf issue #3072", DeprecationWarning, stacklevel=2) return super().__new__(cls, *args, **kwargs) From 09f5bd5a5c2e4d20808e1dc7ba2c3ba30f4c7e3f Mon Sep 17 00:00:00 2001 From: Ange Albertini <105304039+corkamig@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:26:13 +0200 Subject: [PATCH 08/26] Version number for deprecation --- capa/features/address.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capa/features/address.py b/capa/features/address.py index 98b5998e..83822b01 100644 --- a/capa/features/address.py +++ b/capa/features/address.py @@ -133,7 +133,7 @@ class RelativeVirtualAddress(int, Address): """a memory address relative to a base address""" def __new__(cls, *args, **kwargs): - # TODO(corkamig): Removal for v3.5.0 + # TODO(corkamig): Removal for v10 # https://github.com/mandiant/capa/issues/3072 warnings.warn("RelativeVirtualAddress is deprecated - cf issue #3072", DeprecationWarning, stacklevel=2) return super().__new__(cls, *args, **kwargs) From c5921004954d3bbefa12b85c4d0b360795c77bc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:39:38 +0200 Subject: [PATCH 09/26] build(deps-dev): bump vitest from 3.0.9 to 4.1.0 in /web/explorer (#3092) Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.0.9 to 4.1.0. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.0/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Moritz --- web/explorer/package-lock.json | 375 ++++++++++++++++----------------- web/explorer/package.json | 2 +- 2 files changed, 177 insertions(+), 200 deletions(-) diff --git a/web/explorer/package-lock.json b/web/explorer/package-lock.json index b625d6cb..d95c7b6b 100644 --- a/web/explorer/package-lock.json +++ b/web/explorer/package-lock.json @@ -29,7 +29,7 @@ "prettier": "^3.2.5", "vite": "^6.4.2", "vite-plugin-singlefile": "^2.2.0", - "vitest": "^3.0.9" + "vitest": "^4.1.0" } }, "node_modules/@babel/parser": { @@ -613,9 +613,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { @@ -1081,6 +1081,31 @@ "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", "dev": true }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1109,38 +1134,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1152,26 +1179,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -1179,14 +1206,15 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -1194,28 +1222,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", - "loupe": "^3.1.3", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1490,16 +1515,6 @@ "node": ">=8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1524,20 +1539,13 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -1556,16 +1564,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1621,6 +1619,13 @@ "proto-list": "~1.2.1" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1707,16 +1712,6 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1848,9 +1843,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -2153,9 +2148,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.0.tgz", - "integrity": "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2859,13 +2854,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", @@ -2876,12 +2864,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { @@ -3015,6 +3003,17 @@ "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", "dev": true }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3156,16 +3155,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3558,9 +3547,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", - "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -3714,21 +3703,24 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -3738,11 +3730,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -3765,30 +3760,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -3974,29 +3949,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-singlefile": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.2.0.tgz", @@ -4043,62 +3995,71 @@ } }, "node_modules/vitest": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.9", - "@vitest/mocker": "3.0.9", - "@vitest/pretty-format": "^3.0.9", - "@vitest/runner": "3.0.9", - "@vitest/snapshot": "3.0.9", - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", - "chai": "^5.2.0", - "debug": "^4.4.0", - "expect-type": "^1.1.0", - "magic-string": "^0.30.17", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "std-env": "^3.8.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.9", - "@vitest/ui": "3.0.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { "optional": true }, "@vitest/ui": { @@ -4109,9 +4070,25 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vue": { "version": "3.4.31", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz", diff --git a/web/explorer/package.json b/web/explorer/package.json index 3d48a4a8..da00c2d7 100644 --- a/web/explorer/package.json +++ b/web/explorer/package.json @@ -35,6 +35,6 @@ "prettier": "^3.2.5", "vite": "^6.4.2", "vite-plugin-singlefile": "^2.2.0", - "vitest": "^3.0.9" + "vitest": "^4.1.0" } } From adffa80e8f74f611147df1fcedb85a5cd59068fa Mon Sep 17 00:00:00 2001 From: lakshit verma <51952975+vee1e@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:02:46 +0530 Subject: [PATCH 10/26] ida: show addresses for file level features in rulegen (#3009) * ida: show addresses for file level features in rulegen * ida(ai): remove redundant `int` conversion Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Moritz --- CHANGELOG.md | 3 ++- capa/ida/plugin/view.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5678f185..6aed7fd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,7 @@ ### capa Explorer Web ### capa Explorer IDA Pro plugin +- ida: display file-scope feature addresses (`file:0x...`) in rule generator tree and safely handle file-offset navigation/filtering @vee1e #3009 ### Development - tests: update binja version to 5.3 @mr-tz #3011 @@ -277,7 +278,7 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR ### Bug Fixes -- binja: fix a crash during feature extraction when the MLIL is unavailable @xusheng6 #2714 +- binja: fix a crash during feature extraction when the MLIL is unavailable @xusheng6 #2714 ### capa Explorer Web diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index 7e589ae9..7a775916 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -18,12 +18,13 @@ from collections import Counter import idc import idaapi +import ida_loader import capa.ida.helpers import capa.features.common import capa.features.basicblock from capa.ida.plugin.item import CapaExplorerFunctionItem -from capa.features.address import AbsoluteVirtualAddress, _NoAddress +from capa.features.address import FileOffsetAddress, AbsoluteVirtualAddress, _NoAddress from capa.ida.plugin.model import CapaExplorerDataModel from capa.ida.plugin.qt_compat import QtGui, QtCore, Signal, QAction, QtWidgets @@ -895,7 +896,22 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget): def slot_item_double_clicked(self, o, column): """ """ if column == CapaExplorerRulegenFeatures.get_column_address_index() and o.text(column): - idc.jumpto(int(o.text(column), 0x10)) + addr_text = o.text(column).strip() + + if addr_text.startswith("file:"): + try: + file_offset = int(addr_text[len("file:") :], 16) + except (ValueError, TypeError): + return + + ea = ida_loader.get_fileregion_ea(file_offset) + if ea != idc.BADADDR: + idc.jumpto(ea) + else: + try: + idc.jumpto(int(addr_text, 16)) + except (ValueError, TypeError): + return elif o.capa_type == CapaExplorerRulegenFeatures.get_node_type_leaf(): self.editor.update_features([o.data(0, 0x100)]) @@ -945,13 +961,18 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget): # read ea from "Address" column o_ea = o.text(CapaExplorerRulegenFeatures.get_column_address_index()) - if o_ea == "": - # ea may be empty, hide by default + if o_ea == "" or o_ea.startswith("file:"): + # ea may be empty or a file offset, hide by default when filtering by VA if not o.isHidden(): o.setHidden(True) continue - o_ea = int(o_ea, 16) + try: + o_ea = int(o_ea, 16) + except (ValueError, TypeError): + if not o.isHidden(): + o.setHidden(True) + continue if max_ea is not None and min_ea <= o_ea <= max_ea: show_item_and_parents(o) @@ -1035,8 +1056,9 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget): def format_address(e): if isinstance(e, AbsoluteVirtualAddress): return f"{hex(int(e))}" - else: - return "" + if isinstance(e, FileOffsetAddress): + return f"file:{hex(e)}" + return "" def format_feature(feature): """ """ From 58bfa7607ef892478ec0da4f46df1c30a806d7e5 Mon Sep 17 00:00:00 2001 From: Capa Bot Date: Tue, 9 Jun 2026 21:27:35 +0000 Subject: [PATCH 11/26] Sync capa-testfiles submodule --- tests/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data b/tests/data index c630d409..393599e4 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit c630d4093bc6d216796506d975b871320120c9bc +Subproject commit 393599e4a74b4cc114a25cf070198c97f32aa20a From ccf3a87e83ec2adc5c24831c3cbd959c810b94ab Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Tue, 9 Jun 2026 23:28:49 +0200 Subject: [PATCH 12/26] tests: add snapshot tests for feature extraction (#3069) Introduces data-driven snapshot tests that regenerate capa freeze files for a curated set of samples in the tests/data submodule and compare the bytes against committed fixtures under tests/fixtures/freezes/. Any change that perturbs feature extraction surfaces as a test failure with a feature-count delta and a truncated unified diff. --- CHANGELOG.md | 2 + capa/features/extractors/dotnetfile.py | 2 +- capa/features/freeze/__init__.py | 93 +++++++-- capa/features/freeze/__main__.py | 19 ++ tests/test_feature_snapshots.py | 256 +++++++++++++++++++++++++ tests/test_freeze_dynamic.py | 2 +- tests/test_freeze_static.py | 2 +- 7 files changed, 357 insertions(+), 19 deletions(-) create mode 100644 capa/features/freeze/__main__.py create mode 100644 tests/test_feature_snapshots.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aed7fd4..23813c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## master (unreleased) ### New Features +- freeze: add `--reproducible` flag that zeros dynamic header metadata ### Breaking Changes @@ -131,6 +132,7 @@ - ci: use explicit and per job permissions @mike-hunhoff #3002 - replace black/isort/flake8 with ruff @mike-hunhoff #2992 - ci: update GitHub Actions to support Node.js 24 (deprecate Node.js 20) @mr-tz #2984 +- tests: add snapshot tests for feature extraction @williballenthin #3069 ### Raw diffs - [capa v9.4.0...master](https://github.com/mandiant/capa/compare/v9.4.0...master) diff --git a/capa/features/extractors/dotnetfile.py b/capa/features/extractors/dotnetfile.py index ce0f2ab7..651751bc 100644 --- a/capa/features/extractors/dotnetfile.py +++ b/capa/features/extractors/dotnetfile.py @@ -98,7 +98,7 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[tuple # namespaces may be empty, discard namespaces.discard("") - for namespace in namespaces: + for namespace in sorted(namespaces): # namespace do not have an associated token, so we yield 0x0 yield Namespace(namespace), NO_ADDRESS diff --git a/capa/features/freeze/__init__.py b/capa/features/freeze/__init__.py index c6440dbc..15f906db 100644 --- a/capa/features/freeze/__init__.py +++ b/capa/features/freeze/__init__.py @@ -92,10 +92,7 @@ class Address(HashableModel): return cls(type=AddressType.THREAD, value=(a.process.ppid, a.process.pid, a.tid)) elif isinstance(a, capa.features.address.DynamicCallAddress): - return cls( - type=AddressType.CALL, - value=(a.thread.process.ppid, a.thread.process.pid, a.thread.tid, a.id), - ) + return cls(type=AddressType.CALL, value=(a.thread.process.ppid, a.thread.process.pid, a.thread.tid, a.id)) elif a == capa.features.address.NO_ADDRESS or isinstance(a, capa.features.address._NoAddress): return cls(type=AddressType.NO_ADDRESS, value=None) @@ -346,9 +343,14 @@ class Freeze(BaseModel): model_config = ConfigDict(populate_by_name=True) -def dumps_static(extractor: StaticFeatureExtractor) -> str: +def dumps_static(extractor: StaticFeatureExtractor, reproducible: bool = False) -> str: """ serialize the given extractor to a string + + When `reproducible` is true, the freeze's dynamic header metadata (e.g. the + embedded capa version) is zeroed out so that output is identical across + capa versions for a given extractor. This is used by the feature snapshot + tests to keep fixtures stable across version bumps. """ global_features: list[GlobalFeature] = [] for feature, _ in extractor.extract_global_features(): @@ -357,6 +359,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: feature=feature_from_capa(feature), ) ) + global_features.sort(key=lambda gf: gf.feature.model_dump_json()) file_features: list[FileFeature] = [] for feature, address in extractor.extract_file_features(): @@ -366,6 +369,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: address=Address.from_capa(address), ) ) + file_features.sort(key=lambda ff: (ff.address, ff.feature.model_dump_json())) function_features: list[FunctionFeatures] = [] for f in extractor.get_functions(): @@ -378,6 +382,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: ) for feature, addr in extractor.extract_function_features(f) ] + ffeatures.sort(key=lambda ff: (ff.address, ff.feature.model_dump_json())) basic_blocks = [] for bb in extractor.get_basic_blocks(f): @@ -390,6 +395,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: ) for feature, addr in extractor.extract_basic_block_features(f, bb) ] + bbfeatures.sort(key=lambda bf: (bf.address, bf.feature.model_dump_json())) instructions = [] for insn in extractor.get_instructions(f, bb): @@ -402,6 +408,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: ) for feature, addr in extractor.extract_insn_features(f, bb, insn) ] + ifeatures.sort(key=lambda i: (i.address, i.feature.model_dump_json())) instructions.append( InstructionFeatures( @@ -410,6 +417,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: ) ) + instructions.sort(key=lambda i: i.address) basic_blocks.append( BasicBlockFeatures( address=bbaddr, @@ -418,6 +426,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: ) ) + basic_blocks.sort(key=lambda bb: bb.address) function_features.append( FunctionFeatures( address=faddr, @@ -426,18 +435,21 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: ) ) + function_features.sort(key=lambda ff: ff.address) + features = StaticFeatures( global_=global_features, # type: ignore[call-arg] # pydantic alias "global" not recognized by type checkers file=tuple(file_features), functions=tuple(function_features), ) + extractor_version = "" if reproducible else capa.version.__version__ freeze = Freeze( version=CURRENT_VERSION, base_address=Address.from_capa(extractor.get_base_address()), # type: ignore[call-arg] # pydantic alias "base address" not recognized by type checkers sample_hashes=extractor.get_sample_hashes(), flavor="static", - extractor=Extractor(name=extractor.__class__.__name__), + extractor=Extractor(name=extractor.__class__.__name__, version=extractor_version), features=features, ) # type checkers are unable to recognise `base_address` as an argument due to alias @@ -445,9 +457,11 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str: return freeze.model_dump_json() -def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: +def dumps_dynamic(extractor: DynamicFeatureExtractor, reproducible: bool = False) -> str: """ serialize the given extractor to a string + + See `dumps_static` for `reproducible`. """ global_features: list[GlobalFeature] = [] for feature, _ in extractor.extract_global_features(): @@ -456,6 +470,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: feature=feature_from_capa(feature), ) ) + global_features.sort(key=lambda gf: gf.feature.model_dump_json()) file_features: list[FileFeature] = [] for feature, address in extractor.extract_file_features(): @@ -465,6 +480,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: address=Address.from_capa(address), ) ) + file_features.sort(key=lambda ff: (ff.address, ff.feature.model_dump_json())) process_features: list[ProcessFeatures] = [] for p in extractor.get_processes(): @@ -478,6 +494,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: ) for feature, addr in extractor.extract_process_features(p) ] + pfeatures.sort(key=lambda pf: (pf.address, pf.feature.model_dump_json())) threads = [] for t in extractor.get_threads(p): @@ -490,6 +507,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: ) for feature, addr in extractor.extract_thread_features(p, t) ] + tfeatures.sort(key=lambda tf: (tf.address, tf.feature.model_dump_json())) calls = [] for call in extractor.get_calls(p, t): @@ -503,6 +521,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: ) for feature, addr in extractor.extract_call_features(p, t, call) ] + cfeatures.sort(key=lambda cf: (cf.address, cf.feature.model_dump_json())) calls.append( CallFeatures( @@ -512,6 +531,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: ) ) + calls.sort(key=lambda c: c.address) threads.append( ThreadFeatures( address=taddr, @@ -520,6 +540,7 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: ) ) + threads.sort(key=lambda t: t.address) process_features.append( ProcessFeatures( address=paddr, @@ -529,6 +550,8 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: ) ) + process_features.sort(key=lambda pf: pf.address) + features = DynamicFeatures( global_=global_features, # type: ignore[call-arg] # pydantic alias "global" not recognized by type checkers file=tuple(file_features), @@ -539,12 +562,13 @@ def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str: get_base_addr = getattr(extractor, "get_base_address", None) base_addr = get_base_addr() if get_base_addr else capa.features.address.NO_ADDRESS + extractor_version = "" if reproducible else capa.version.__version__ freeze = Freeze( version=CURRENT_VERSION, base_address=Address.from_capa(base_addr), # type: ignore[call-arg] # pydantic alias "base address" not recognized by type checkers sample_hashes=extractor.get_sample_hashes(), flavor="dynamic", - extractor=Extractor(name=extractor.__class__.__name__), + extractor=Extractor(name=extractor.__class__.__name__, version=extractor_version), features=features, ) # type checkers are unable to recognise `base_address` as an argument due to alias @@ -627,28 +651,28 @@ def loads_dynamic(s: str) -> DynamicFeatureExtractor: MAGIC = "capa0000".encode("ascii") -def dumps(extractor: FeatureExtractor) -> str: +def dumps(extractor: FeatureExtractor, reproducible: bool = False) -> str: """serialize the given extractor to a string.""" if isinstance(extractor, StaticFeatureExtractor): - doc = dumps_static(extractor) + doc = dumps_static(extractor, reproducible=reproducible) elif isinstance(extractor, DynamicFeatureExtractor): - doc = dumps_dynamic(extractor) + doc = dumps_dynamic(extractor, reproducible=reproducible) else: raise ValueError("Invalid feature extractor") return doc -def dump(extractor: FeatureExtractor) -> bytes: +def dump(extractor: FeatureExtractor, reproducible: bool = False) -> bytes: """serialize the given extractor to a byte array.""" - return MAGIC + zlib.compress(dumps(extractor).encode("utf-8")) + return MAGIC + zlib.compress(dumps(extractor, reproducible=reproducible).encode("utf-8")) def is_freeze(buf: bytes) -> bool: return buf[: len(MAGIC)] == MAGIC -def loads(s: str): +def loads(s: str) -> FeatureExtractor: doc = json.loads(s) if doc["version"] != CURRENT_VERSION: @@ -662,7 +686,7 @@ def loads(s: str): raise ValueError(f"unsupported freeze format flavor: {doc['flavor']}") -def load(buf: bytes): +def load(buf: bytes) -> FeatureExtractor: """deserialize a set of features (as a NullFeatureExtractor) from a byte array.""" if not is_freeze(buf): raise ValueError("missing magic header") @@ -685,6 +709,11 @@ def main(argv=None): parser = argparse.ArgumentParser(description="save capa features to a file") capa.main.install_common_args(parser, {"input_file", "format", "backend", "os", "signatures"}) parser.add_argument("output", type=str, help="Path to output file") + parser.add_argument( + "--reproducible", + action="store_true", + help="zero out dynamic header metadata (e.g. capa version) so output is stable across capa versions", + ) args = parser.parse_args(args=argv) try: @@ -696,11 +725,43 @@ def main(argv=None): except capa.main.ShouldExitError as e: return e.status_code - Path(args.output).write_bytes(dump(extractor)) + output_path = Path(args.output) + output_path.write_bytes(dump(extractor, reproducible=args.reproducible)) + + # Log a manifest entry for the feature snapshot tests at INFO level. This + # makes it easy to copy/paste into + # `tests/fixtures/snapshots/features/manifest.json` when adding a new + # fixture or refreshing an existing one. + entry: dict[str, str] = { + "name": output_path.stem, + "sample": str(args.input_file), + "freeze": output_path.name, + } + if args.format and args.format != "auto": + entry["format"] = args.format + if args.backend and args.backend != "auto": + entry["backend"] = args.backend + if args.os and args.os != "auto": + entry["os"] = args.os + commit = _git_head_commit() + if commit: + entry["generated_at_commit"] = commit + logger.info("manifest entry: %s", json.dumps(entry)) return 0 +def _git_head_commit() -> str: + """Return the HEAD commit, or empty string if this isn't a git checkout.""" + import subprocess + + try: + out = subprocess.check_output(["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL) + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + return "" + return out.decode("ascii", errors="replace").strip() + + if __name__ == "__main__": import sys diff --git a/capa/features/freeze/__main__.py b/capa/features/freeze/__main__.py new file mode 100644 index 00000000..aa3f3526 --- /dev/null +++ b/capa/features/freeze/__main__.py @@ -0,0 +1,19 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +from capa.features.freeze import main + +sys.exit(main()) diff --git a/tests/test_feature_snapshots.py b/tests/test_feature_snapshots.py new file mode 100644 index 00000000..53a48ba4 --- /dev/null +++ b/tests/test_feature_snapshots.py @@ -0,0 +1,256 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Data-driven feature snapshot tests. + +For every entry in `tests/fixtures/snapshots/features/manifest.json`, this +module regenerates a capa freeze from the corresponding sample via +`capa.features.freeze.main --reproducible`, compares it byte-for-byte +against the committed `.frz` file, and on mismatch renders a unified diff +of the freeze contents so a reviewer can see which features appeared, +disappeared, or moved. + +A failing test means capa now extracts different features from the same +sample than it used to. That can be intentional (you changed an extractor) +or accidental (an unrelated change perturbed extraction); see the failure +message for how to update the fixture or investigate. + +Refreshing a fixture after an intentional change:: + + python -m capa.features.freeze --reproducible \\ + tests/data/ tests/fixtures/snapshots/features/.frz + +The manifest is edited by hand when samples are added or removed. +""" + +from __future__ import annotations + +import json +import zlib +import difflib +import tempfile +from typing import Any, Optional +from pathlib import Path + +import pytest +from pydantic import BaseModel, ConfigDict + +import capa.features.freeze + +TESTS_DIR = Path(__file__).resolve().parent +TESTS_DATA_DIR = TESTS_DIR / "data" +FEATURE_SNAPSHOTS_DIR = TESTS_DATA_DIR / "fixtures" / "snapshots" / "features" +MANIFEST_PATH = FEATURE_SNAPSHOTS_DIR / "manifest.json" + + +class FeatureSnapshot(BaseModel): + """One entry in the feature snapshot manifest.""" + + model_config = ConfigDict(frozen=True) + + name: str + sample: str + freeze: str + explanation: str = "" + # Git commit at which this fixture was last regenerated. Purely informational: + # on test failure we surface it so a reviewer can run `git log ..HEAD` + # to see what's changed since. Not validated — humans keep it accurate. + generated_at_commit: Optional[str] = None + format: Optional[str] = None + backend: Optional[str] = None + os: Optional[str] = None + + @property + def sample_path(self) -> Path: + return TESTS_DATA_DIR / self.sample + + @property + def freeze_path(self) -> Path: + return FEATURE_SNAPSHOTS_DIR / self.freeze + + +class Manifest(BaseModel): + version: int = 1 + description: str = "" + snapshots: list[FeatureSnapshot] + + @classmethod + def from_file(cls, path: Path = MANIFEST_PATH) -> Manifest: + return cls.model_validate_json(path.read_text(encoding="utf-8")) + + +_SNAPSHOTS = Manifest.from_file().snapshots + + +def _ids(snapshots: list[FeatureSnapshot]) -> list[str]: + return [s.name for s in snapshots] + + +def _regenerate(snapshot: FeatureSnapshot) -> bytes: + """Run the freeze CLI against the sample and return the produced bytes.""" + import logging + + root = logging.getLogger() + handlers_before = list(root.handlers) + + with tempfile.TemporaryDirectory() as tmp: + out_path = Path(tmp) / "out.frz" + argv = [str(snapshot.sample_path), str(out_path), "--reproducible"] + if snapshot.format is not None: + argv += ["--format", snapshot.format] + if snapshot.backend is not None: + argv += ["--backend", snapshot.backend] + if snapshot.os is not None: + argv += ["--os", snapshot.os] + rc = capa.features.freeze.main(argv) + + # capa.main.handle_common_args() unconditionally appends a RichHandler + # to the root logger on every call. Since we call freeze.main() once per + # snapshot, handlers accumulate and duplicate every log line. Remove + # whatever was added so the next iteration starts clean. + for h in root.handlers[:]: + if h not in handlers_before: + root.removeHandler(h) + + if rc != 0: + raise RuntimeError(f"capa.features.freeze.main exited with status {rc}") + return out_path.read_bytes() + + +def _doc_to_lines(doc: dict[str, Any]) -> list[str]: + """ + Render a freeze JSON document to a list of lines suitable for unified-diffing. + + We pretty-print with sorted keys so that field reordering (which is + meaningful for features) is preserved while key ordering within objects is + normalized. + """ + return json.dumps(doc, indent=2, sort_keys=True).splitlines(keepends=True) + + +def _load_freeze_doc(buf: bytes) -> dict[str, Any]: + """deserialize bytes to capa.features.freeze.Freeze, as JSON-like object. + + capa.features.freeze.loads() deserializes into a FeatureExtractor, not Freeze (or JSON, which we need for diffing). + """ + magic = capa.features.freeze.MAGIC + assert buf[: len(magic)] == magic, "missing freeze magic header" + return json.loads(zlib.decompress(buf[len(magic) :]).decode("utf-8")) + + +def _format_mismatch(snapshot: FeatureSnapshot, expected: bytes, actual: bytes) -> str: + """Build a failure message describing how the freezes differ.""" + lines = [ + f"feature snapshot drift for {snapshot.name!r}:", + f" sample: {snapshot.sample}", + f" expected freeze: {snapshot.freeze_path}", + " actual freeze: ", + f" expected size: {len(expected):,} bytes", + f" actual size: {len(actual):,} bytes", + ] + if snapshot.generated_at_commit: + lines.append(f" last regenerated at: {snapshot.generated_at_commit}") + + expected_doc = _load_freeze_doc(expected) + actual_doc = _load_freeze_doc(actual) + + expected_lines = _doc_to_lines(expected_doc) + actual_lines = _doc_to_lines(actual_doc) + + # difflib.unified_diff uses SequenceMatcher which is O(n^2) for dissimilar + # sequences. Large freeze documents (e.g. mimikatz) expand to millions of + # JSON lines, making a naive diff take hours. Skip it when the input is too + # large — the regeneration command below is the intended way to inspect. + MAX_DIFFABLE_LINES = 100_000 + MAX_DIFF_LINES = 200 + + total_lines = len(expected_lines) + len(actual_lines) + lines.append("") + if total_lines > MAX_DIFFABLE_LINES: + lines.append( + f"diff skipped: documents too large ({len(expected_lines):,} + {len(actual_lines):,} lines)." + " Regenerate the fixture locally to inspect." + ) + else: + diff = list( + difflib.unified_diff( + expected_lines, + actual_lines, + fromfile=f"expected/{snapshot.freeze}", + tofile=f"actual/{snapshot.freeze}", + n=2, + ) + ) + + if len(diff) > MAX_DIFF_LINES: + lines.append(f"unified diff ({len(diff)} lines, truncated to {MAX_DIFF_LINES}):") + diff = diff[:MAX_DIFF_LINES] + else: + lines.append(f"unified diff ({len(diff)} lines):") + lines.extend(line.rstrip("\n") for line in diff) + lines.append("") + lines.append("how and when to update this snapshot:") + lines.append(" If this change to feature extraction is INTENTIONAL (you edited an extractor):") + lines.append(" 1. regenerate the fixture:") + lines.append( + f" python -m capa.features.freeze --reproducible \\\n" + f" {snapshot.sample_path} {snapshot.freeze_path}" + ) + lines.append( + " 2. update `generated_at_commit` in manifest.json to HEAD (the freeze CLI emits a suggested entry at INFO)." + ) + lines.append(" If it is ACCIDENTAL (extraction shifted as a side effect of an unrelated change),") + lines.append(" do NOT update the fixture; fix the root cause instead.") + if snapshot.generated_at_commit: + lines.append( + f" To see what's changed since this fixture was last regenerated:\n" + f" git log {snapshot.generated_at_commit}..HEAD -- capa/" + ) + return "\n".join(lines) + + +_BACKEND_AVAILABLE: dict[str, bool] = {} + + +def _is_backend_available(backend: str) -> bool: + if backend not in _BACKEND_AVAILABLE: + if backend == "ida": + try: + import idapro # noqa: F401 + + _BACKEND_AVAILABLE[backend] = True + except ImportError: + _BACKEND_AVAILABLE[backend] = False + else: + _BACKEND_AVAILABLE[backend] = True + return _BACKEND_AVAILABLE[backend] + + +@pytest.mark.parametrize("snapshot", _SNAPSHOTS, ids=_ids(_SNAPSHOTS)) +def test_feature_snapshot(snapshot: FeatureSnapshot): + """ + Regenerate the freeze for `snapshot.sample` and assert it matches + `snapshot.freeze` byte-for-byte. + """ + if snapshot.backend and not _is_backend_available(snapshot.backend): + pytest.skip(f"{snapshot.backend} backend not available") + + expected = snapshot.freeze_path.read_bytes() + actual = _regenerate(snapshot) + + if actual == expected: + return + + pytest.fail(_format_mismatch(snapshot, expected, actual)) diff --git a/tests/test_freeze_dynamic.py b/tests/test_freeze_dynamic.py index 5934a9f1..5ea4b69a 100644 --- a/tests/test_freeze_dynamic.py +++ b/tests/test_freeze_dynamic.py @@ -122,7 +122,7 @@ def test_null_feature_extractor(): def compare_extractors(a: DynamicFeatureExtractor, b: DynamicFeatureExtractor): - assert list(a.extract_file_features()) == list(b.extract_file_features()) + assert sorted(set(a.extract_file_features())) == sorted(set(b.extract_file_features())) assert addresses(a.get_processes()) == addresses(b.get_processes()) for p in a.get_processes(): diff --git a/tests/test_freeze_static.py b/tests/test_freeze_static.py index 57ee6f89..fe82f7eb 100644 --- a/tests/test_freeze_static.py +++ b/tests/test_freeze_static.py @@ -129,7 +129,7 @@ def test_null_feature_extractor(): def compare_extractors(a, b): - assert list(a.extract_file_features()) == list(b.extract_file_features()) + assert sorted(set(a.extract_file_features())) == sorted(set(b.extract_file_features())) assert addresses(a.get_functions()) == addresses(b.get_functions()) for f in a.get_functions(): From 8a18bd0e542f7986e4606c9d8c8139a9fec86d48 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 11 Jun 2026 10:18:14 +0200 Subject: [PATCH 13/26] tests: add more granular ELF OS detection tests, data-driven (#3098) to better support ports of this logic to other languages/runtimes --- CHANGELOG.md | 1 + tests/test_elf_os_detection.py | 139 +++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/test_elf_os_detection.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 23813c38..a67b77d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,7 @@ - replace black/isort/flake8 with ruff @mike-hunhoff #2992 - ci: update GitHub Actions to support Node.js 24 (deprecate Node.js 20) @mr-tz #2984 - tests: add snapshot tests for feature extraction @williballenthin #3069 +- tests: add more tests to exercise ELF OS detection @williballenthin #3098 ### Raw diffs - [capa v9.4.0...master](https://github.com/mandiant/capa/compare/v9.4.0...master) diff --git a/tests/test_elf_os_detection.py b/tests/test_elf_os_detection.py new file mode 100644 index 00000000..12e766e8 --- /dev/null +++ b/tests/test_elf_os_detection.py @@ -0,0 +1,139 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from pathlib import Path + +import pytest + +import capa.features.extractors.elf as elf + +CD = Path(__file__).resolve().parent + +ALGORITHM_FUNCTIONS = { + "osabi": elf.guess_os_from_osabi, + "ph_notes": elf.guess_os_from_ph_notes, + "sh_notes": elf.guess_os_from_sh_notes, + "ident_directive": elf.guess_os_from_ident_directive, + "linker": elf.guess_os_from_linker, + "abi_versions_needed": elf.guess_os_from_abi_versions_needed, + "needed_dependencies": elf.guess_os_from_needed_dependencies, + "symtab": elf.guess_os_from_symtab, + "go_buildinfo": elf.guess_os_from_go_buildinfo, + "go_source": elf.guess_os_from_go_source, + "vdso_strings": elf.guess_os_from_vdso_strings, +} + +FIXTURES = json.loads( + """ +[ + { + "path": "2f7f5fb5de175e770d7eae87666f9831.elf_", + "os": "linux", + "algorithms": { + "sh_notes": "linux", + "ident_directive": "linux", + "vdso_strings": "linux" + } + }, + { + "path": "7351f8a40c5450557b24622417fc478d.elf_", + "os": "linux", + "algorithms": { + "ph_notes": "linux", + "sh_notes": "linux", + "ident_directive": "linux", + "linker": "linux", + "abi_versions_needed": "linux" + } + }, + { + "path": "b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_", + "os": "hurd", + "algorithms": { + "sh_notes": "hurd", + "abi_versions_needed": "hurd", + "needed_dependencies": "hurd" + } + }, + { + "path": "bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_", + "os": "hurd", + "algorithms": { + "sh_notes": "hurd", + "abi_versions_needed": "hurd" + } + }, + { + "path": "2bf18d0403677378adad9001b1243211.elf_", + "os": "linux", + "algorithms": { + "symtab": "linux" + } + }, + { + "path": "1038a23daad86042c66bfe6c9d052d27048de9653bde5750dc0f240c792d9ac8.elf_", + "os": "android", + "algorithms": { + "ph_notes": "android", + "needed_dependencies": "android" + } + }, + { + "path": "3da7c2c70a2d93ac4643f20339d5c7d61388bddd77a4a5fd732311efad78e535.elf_", + "os": "linux", + "algorithms": { + "go_buildinfo": "linux", + "go_source": "linux", + "vdso_strings": "linux" + } + } +] +""" +) + + +def _generate_algorithm_params(): + params = [] + for fixture in FIXTURES: + path = fixture["path"] + short_id = path[:8] + algorithms = fixture.get("algorithms", {}) + for alg_name in ALGORITHM_FUNCTIONS: + expected = algorithms.get(alg_name) + test_id = f"{short_id}-{alg_name}" + params.append(pytest.param(path, alg_name, expected, id=test_id)) + return params + + +def _generate_detection_params(): + return [pytest.param(f["path"], f["os"], id=f["path"][:8]) for f in FIXTURES] + + +@pytest.mark.parametrize("path,algorithm,expected", _generate_algorithm_params()) +def test_elf_os_algorithm(path, algorithm, expected): + with (CD / "data" / path).open("rb") as f: + e = elf.ELF(f) + result = ALGORITHM_FUNCTIONS[algorithm](e) + if expected is None: + assert result is None, f"{algorithm} should return None, got {result}" + else: + assert result is not None, f"{algorithm} should return {expected}, got None" + assert result.value == expected + + +@pytest.mark.parametrize("path,expected_os", _generate_detection_params()) +def test_elf_os_detection(path, expected_os): + with (CD / "data" / path).open("rb") as f: + assert elf.detect_elf_os(f) == expected_os From e69eb70d55d89fe3d937fc7b4191347e75dbb0af Mon Sep 17 00:00:00 2001 From: Capa Bot Date: Thu, 11 Jun 2026 08:19:25 +0000 Subject: [PATCH 14/26] Sync capa-testfiles submodule --- tests/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data b/tests/data index 393599e4..ef87fced 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 393599e4a74b4cc114a25cf070198c97f32aa20a +Subproject commit ef87fcedcc69fa18acf669d8cf194f11a03b26ff From 028aa533b13489a55f1dfc4a9b312f3982d4b6db Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 11 Jun 2026 10:42:10 +0200 Subject: [PATCH 15/26] tests: add more ELF OS detection cases (#3099) * tests: add more ELF OS detection cases --- tests/test_elf_os_detection.py | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_elf_os_detection.py b/tests/test_elf_os_detection.py index 12e766e8..b89cdbc4 100644 --- a/tests/test_elf_os_detection.py +++ b/tests/test_elf_os_detection.py @@ -98,6 +98,61 @@ FIXTURES = json.loads( "go_source": "linux", "vdso_strings": "linux" } + }, + { + "path": "9486f2c5d514c1f39833b852ab03f4c0297e9e82b456b0dccad1a2d7d15c3385.elf_", + "os": "freebsd", + "algorithms": { + "osabi": "freebsd" + } + }, + { + "path": "a72ac9f1cf6cc01103765f866aae2dd85ea48208fb180c01076ca982684a4032.elf_", + "os": "openbsd", + "algorithms": { + "osabi": "openbsd" + } + }, + { + "path": "5e4263575796c6ea2445505a843e616111e0e540ec49441e3bb3fc99be7d3afb.elf_", + "os": "hpux", + "algorithms": { + "osabi": "hpux" + } + }, + { + "path": "ccb5eefc47d09672c6d62368a55f48a80259b1408b4f3d260b131d40b487f262.elf_", + "os": "linux", + "algorithms": { + "ident_directive": "linux", + "vdso_strings": "linux" + } + }, + { + "path": "c2b2d7cea94179e6c2b6913205e37c45b67ccc0156d8f266548f7f1f95144285.elf_", + "os": "linux", + "algorithms": { + "ph_notes": "linux", + "linker": "linux" + } + }, + { + "path": "fb5cf3df103336ca6b84a5d0f314bdd394ce7465adac3cf1914d390d62884582.elf_", + "os": "freebsd", + "algorithms": { + "osabi": "freebsd", + "go_source": "freebsd" + } + }, + { + "path": "ef0ef9693ac433b768dc4cd260132788c3d95afa12addab711c15de61f3631bf.elf_", + "os": "openbsd", + "algorithms": { + "osabi": "openbsd", + "ph_notes": "openbsd", + "sh_notes": "openbsd", + "go_buildinfo": "openbsd" + } } ] """ From 0fba3e58eef8c2ed42b66206eb34c625322b36f3 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 11 Jun 2026 11:51:12 +0200 Subject: [PATCH 16/26] tests: split out ELF OS detection fixtures into their own JSON --- tests/fixtures/elf/os-detection.json | 118 +++++++++++++++++++++++++ tests/test_elf_os_detection.py | 124 +-------------------------- 2 files changed, 120 insertions(+), 122 deletions(-) create mode 100644 tests/fixtures/elf/os-detection.json diff --git a/tests/fixtures/elf/os-detection.json b/tests/fixtures/elf/os-detection.json new file mode 100644 index 00000000..10201ea3 --- /dev/null +++ b/tests/fixtures/elf/os-detection.json @@ -0,0 +1,118 @@ +[ + { + "path": "2f7f5fb5de175e770d7eae87666f9831.elf_", + "os": "linux", + "algorithms": { + "sh_notes": "linux", + "ident_directive": "linux", + "vdso_strings": "linux" + } + }, + { + "path": "7351f8a40c5450557b24622417fc478d.elf_", + "os": "linux", + "algorithms": { + "ph_notes": "linux", + "sh_notes": "linux", + "ident_directive": "linux", + "linker": "linux", + "abi_versions_needed": "linux" + } + }, + { + "path": "b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_", + "os": "hurd", + "algorithms": { + "sh_notes": "hurd", + "abi_versions_needed": "hurd", + "needed_dependencies": "hurd" + } + }, + { + "path": "bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_", + "os": "hurd", + "algorithms": { + "sh_notes": "hurd", + "abi_versions_needed": "hurd" + } + }, + { + "path": "2bf18d0403677378adad9001b1243211.elf_", + "os": "linux", + "algorithms": { + "symtab": "linux" + } + }, + { + "path": "1038a23daad86042c66bfe6c9d052d27048de9653bde5750dc0f240c792d9ac8.elf_", + "os": "android", + "algorithms": { + "ph_notes": "android", + "needed_dependencies": "android" + } + }, + { + "path": "3da7c2c70a2d93ac4643f20339d5c7d61388bddd77a4a5fd732311efad78e535.elf_", + "os": "linux", + "algorithms": { + "go_buildinfo": "linux", + "go_source": "linux", + "vdso_strings": "linux" + } + }, + { + "path": "9486f2c5d514c1f39833b852ab03f4c0297e9e82b456b0dccad1a2d7d15c3385.elf_", + "os": "freebsd", + "algorithms": { + "osabi": "freebsd" + } + }, + { + "path": "a72ac9f1cf6cc01103765f866aae2dd85ea48208fb180c01076ca982684a4032.elf_", + "os": "openbsd", + "algorithms": { + "osabi": "openbsd" + } + }, + { + "path": "5e4263575796c6ea2445505a843e616111e0e540ec49441e3bb3fc99be7d3afb.elf_", + "os": "hpux", + "algorithms": { + "osabi": "hpux" + } + }, + { + "path": "ccb5eefc47d09672c6d62368a55f48a80259b1408b4f3d260b131d40b487f262.elf_", + "os": "linux", + "algorithms": { + "ident_directive": "linux", + "vdso_strings": "linux" + } + }, + { + "path": "c2b2d7cea94179e6c2b6913205e37c45b67ccc0156d8f266548f7f1f95144285.elf_", + "os": "linux", + "algorithms": { + "ph_notes": "linux", + "linker": "linux" + } + }, + { + "path": "fb5cf3df103336ca6b84a5d0f314bdd394ce7465adac3cf1914d390d62884582.elf_", + "os": "freebsd", + "algorithms": { + "osabi": "freebsd", + "go_source": "freebsd" + } + }, + { + "path": "ef0ef9693ac433b768dc4cd260132788c3d95afa12addab711c15de61f3631bf.elf_", + "os": "openbsd", + "algorithms": { + "osabi": "openbsd", + "ph_notes": "openbsd", + "sh_notes": "openbsd", + "go_buildinfo": "openbsd" + } + } +] diff --git a/tests/test_elf_os_detection.py b/tests/test_elf_os_detection.py index b89cdbc4..c2beb733 100644 --- a/tests/test_elf_os_detection.py +++ b/tests/test_elf_os_detection.py @@ -35,128 +35,8 @@ ALGORITHM_FUNCTIONS = { "vdso_strings": elf.guess_os_from_vdso_strings, } -FIXTURES = json.loads( - """ -[ - { - "path": "2f7f5fb5de175e770d7eae87666f9831.elf_", - "os": "linux", - "algorithms": { - "sh_notes": "linux", - "ident_directive": "linux", - "vdso_strings": "linux" - } - }, - { - "path": "7351f8a40c5450557b24622417fc478d.elf_", - "os": "linux", - "algorithms": { - "ph_notes": "linux", - "sh_notes": "linux", - "ident_directive": "linux", - "linker": "linux", - "abi_versions_needed": "linux" - } - }, - { - "path": "b5f0524e69b3a3cf636c7ac366ca57bf5e3a8fdc8a9f01caf196c611a7918a87.elf_", - "os": "hurd", - "algorithms": { - "sh_notes": "hurd", - "abi_versions_needed": "hurd", - "needed_dependencies": "hurd" - } - }, - { - "path": "bf7a9c8bdfa6d47e01ad2b056264acc3fd90cf43fe0ed8deec93ab46b47d76cb.elf_", - "os": "hurd", - "algorithms": { - "sh_notes": "hurd", - "abi_versions_needed": "hurd" - } - }, - { - "path": "2bf18d0403677378adad9001b1243211.elf_", - "os": "linux", - "algorithms": { - "symtab": "linux" - } - }, - { - "path": "1038a23daad86042c66bfe6c9d052d27048de9653bde5750dc0f240c792d9ac8.elf_", - "os": "android", - "algorithms": { - "ph_notes": "android", - "needed_dependencies": "android" - } - }, - { - "path": "3da7c2c70a2d93ac4643f20339d5c7d61388bddd77a4a5fd732311efad78e535.elf_", - "os": "linux", - "algorithms": { - "go_buildinfo": "linux", - "go_source": "linux", - "vdso_strings": "linux" - } - }, - { - "path": "9486f2c5d514c1f39833b852ab03f4c0297e9e82b456b0dccad1a2d7d15c3385.elf_", - "os": "freebsd", - "algorithms": { - "osabi": "freebsd" - } - }, - { - "path": "a72ac9f1cf6cc01103765f866aae2dd85ea48208fb180c01076ca982684a4032.elf_", - "os": "openbsd", - "algorithms": { - "osabi": "openbsd" - } - }, - { - "path": "5e4263575796c6ea2445505a843e616111e0e540ec49441e3bb3fc99be7d3afb.elf_", - "os": "hpux", - "algorithms": { - "osabi": "hpux" - } - }, - { - "path": "ccb5eefc47d09672c6d62368a55f48a80259b1408b4f3d260b131d40b487f262.elf_", - "os": "linux", - "algorithms": { - "ident_directive": "linux", - "vdso_strings": "linux" - } - }, - { - "path": "c2b2d7cea94179e6c2b6913205e37c45b67ccc0156d8f266548f7f1f95144285.elf_", - "os": "linux", - "algorithms": { - "ph_notes": "linux", - "linker": "linux" - } - }, - { - "path": "fb5cf3df103336ca6b84a5d0f314bdd394ce7465adac3cf1914d390d62884582.elf_", - "os": "freebsd", - "algorithms": { - "osabi": "freebsd", - "go_source": "freebsd" - } - }, - { - "path": "ef0ef9693ac433b768dc4cd260132788c3d95afa12addab711c15de61f3631bf.elf_", - "os": "openbsd", - "algorithms": { - "osabi": "openbsd", - "ph_notes": "openbsd", - "sh_notes": "openbsd", - "go_buildinfo": "openbsd" - } - } -] -""" -) +FIXTURES_FILE = CD / "fixtures/elf/os-detection.json" +FIXTURES = json.loads(FIXTURES_FILE.read_text(encoding="utf-8")) def _generate_algorithm_params(): From 2c12cbb4854265e013f03c42d1bb1404f198adc6 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Thu, 11 Jun 2026 13:42:29 +0200 Subject: [PATCH 17/26] tests: add data-driven test fixtures for rule matcher (#2987) --- CHANGELOG.md | 1 + tests/{fixtures.py => fixtures/__init__.py} | 5 +- tests/fixtures/matcher/README.md | 74 ++ tests/fixtures/matcher/dynamic/call.yml | 53 ++ tests/fixtures/matcher/dynamic/process.yml | 40 + tests/fixtures/matcher/dynamic/span.yml | 316 ++++++++ tests/fixtures/matcher/dynamic/thread.yml | 38 + tests/fixtures/matcher/static/composition.yml | 122 +++ tests/fixtures/matcher/static/features.yml | 361 +++++++++ tests/fixtures/matcher/static/logic.yml | 712 ++++++++++++++++++ tests/fixtures/matcher/static/parsing.yml | 33 + tests/fixtures/matcher/static/scopes.yml | 141 ++++ tests/fixtures/matcher/static/strings.yml | 274 +++++++ tests/test_dynamic_span_of_calls_scope.py | 412 ---------- tests/test_engine.py | 132 +--- tests/test_match.py | 660 ---------------- tests/test_match_fixtures.py | 711 +++++++++++++++++ tests/test_rules.py | 242 +----- 18 files changed, 2881 insertions(+), 1446 deletions(-) rename tests/{fixtures.py => fixtures/__init__.py} (99%) create mode 100644 tests/fixtures/matcher/README.md create mode 100644 tests/fixtures/matcher/dynamic/call.yml create mode 100644 tests/fixtures/matcher/dynamic/process.yml create mode 100644 tests/fixtures/matcher/dynamic/span.yml create mode 100644 tests/fixtures/matcher/dynamic/thread.yml create mode 100644 tests/fixtures/matcher/static/composition.yml create mode 100644 tests/fixtures/matcher/static/features.yml create mode 100644 tests/fixtures/matcher/static/logic.yml create mode 100644 tests/fixtures/matcher/static/parsing.yml create mode 100644 tests/fixtures/matcher/static/scopes.yml create mode 100644 tests/fixtures/matcher/static/strings.yml create mode 100644 tests/test_match_fixtures.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a67b77d1..e1381441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,6 +211,7 @@ This release includes Ghidra PyGhidra support, performance improvements, depende ### Development +- tests: add data-driven rule matcher fixtures with a show-features-like DSL and authoring documentation #2985 - doc: document that default output shows top-level matches only; -v/-vv show nested matches @devs6186 #1410 - doc: fix typo in usage.md, add documentation links to README @devs6186 #2274 - doc: add table comparing ways to consume capa output (CLI, IDA, Ghidra, dynamic sandbox, web) @devs6186 #2273 diff --git a/tests/fixtures.py b/tests/fixtures/__init__.py similarity index 99% rename from tests/fixtures.py rename to tests/fixtures/__init__.py index 3396644b..d8d8e25d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures/__init__.py @@ -41,8 +41,9 @@ from capa.features.extractors.base_extractor import ( from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor logger = logging.getLogger(__name__) -CD = Path(__file__).resolve().parent -FIXTURE_MANIFEST_DIR = CD / "fixtures" / "features" +_FIXTURES_DIR = Path(__file__).resolve().parent +CD = _FIXTURES_DIR.parent +FIXTURE_MANIFEST_DIR = _FIXTURES_DIR / "features" DNFILE_TESTFILES = CD / "data" / "dotnet" / "dnfile-testfiles" diff --git a/tests/fixtures/matcher/README.md b/tests/fixtures/matcher/README.md new file mode 100644 index 00000000..c0a0e1d1 --- /dev/null +++ b/tests/fixtures/matcher/README.md @@ -0,0 +1,74 @@ +Data-driven matcher tests. Each test pairs a rule fragment, a synthetic feature listing, and the exact matches that capa should report. These test the matcher itself, not end-to-end binary analysis. Tests for rule parsing and representation (e.g. how a rule looks after deserialization) belong in `test_rules.py`, not here. + +Fixture files live under `static/` and `dynamic/` directories, organized by theme (e.g. `logic.yml`, `scopes.yml`, `strings.yml`). Flavor is inferred from the directory. The pytest entrypoint and DSL parser both live in `tests/test_match_fixtures.py`. + +```sh +pytest -q tests/test_match_fixtures.py +pytest -q tests/test_match_fixtures.py -k +``` + +## Fixture file format + +Each file is a YAML list. Each element is one test case. + +```yaml +- name: and-both-present + description: and requires all children to match + rules: + - name: and-match + description: should match because the function contains both mov and number 0x10 + scopes: + static: function + features: + - and: + - mnemonic: mov + - number: 0x10 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + insn: 0x402000: number(0x10) + expect: + matches: + and-match: + - 0x402000 +``` + +The `name` field is a stable human-readable identifier that appears in pytest ids. The `description` explains the behavior under test. Rules are specified under `rules` with `name`, `scopes`, and `features` at the top level (no `meta:` wrapper needed); the loader fills in the missing scope side with `unsupported`. The `features` field is a block string or list of strings in the DSL described below. Expected results go in `expect.matches`, mapping rule names to exact match locations. + +Optional fields: `base address` (static only, defaults to `0`) and `options.span size` (patches `SPAN_SIZE` for that test). + +Keep tests small and focused: each test case should have its own minimal feature set. Prefer many small individual tests over grouped rules sharing features. + +## Feature DSL + +Line prefixes for static tests: `global:`, `file:`, `func:`, `bb:`, `insn:`. +Line prefixes for dynamic tests: `global:`, `file:`, `proc:`, `thread:`, `call:`. + +Static examples: +``` +global: global: os(windows) + file: 0x402345: characteristic(embedded pe) + func: 0x401000 + func: 0x401000: string(hello world) + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401000: offset(0x402000) -> 0x402000 + insn: 0x401000: string(key: value) +``` + +Dynamic examples: +``` +proc: sample.exe (pid=3052) + thread: 3064 + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) +``` + +`-> ` overrides the feature location. Feature text may contain `: `. Dynamic call IDs must be unique within a test and can be used as shorthand in `expect.matches`. + +## Address syntax + +String forms: `0x401000`, `base address+0x100`, `file+0x20`, `token(0x1234)`, `token(0x1234)+0x10`, `global`, `process{pid:3052}`, `process{pid:3052,tid:3064}`, `process{pid:3052,tid:3064,call:11}` (with optional `ppid:`). + +Dynamic tests may use a bare integer call ID in `expect.matches` when that call ID is unique within the test. diff --git a/tests/fixtures/matcher/dynamic/call.yml b/tests/fixtures/matcher/dynamic/call.yml new file mode 100644 index 00000000..41a1ea5e --- /dev/null +++ b/tests/fixtures/matcher/dynamic/call.yml @@ -0,0 +1,53 @@ +- name: call-scope-single-api + description: call scope matches a single API at the correct call + rules: + - name: call-api + scopes: + dynamic: call + features: + - api: GetSystemTimeAsFileTime + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 8: api(GetSystemTimeAsFileTime) + call: 9: api(GetSystemInfo) + expect: + matches: + call-api: + - 8 + +- name: call-scope-multiple-matches + description: call scope reports multiple matching calls + rules: + - name: call-multi + scopes: + dynamic: call + features: + - api: LdrGetDllHandle + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 12: api(LdrGetDllHandle) + expect: + matches: + call-multi: + - 10 + - 12 + +- name: call-scope-no-match + description: call scope does not match when no call has the required feature + rules: + - name: call-absent + scopes: + dynamic: call + features: + - api: CreateFileW + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 8: api(GetSystemTimeAsFileTime) + call: 9: api(GetSystemInfo) + expect: + matches: {} diff --git a/tests/fixtures/matcher/dynamic/process.yml b/tests/fixtures/matcher/dynamic/process.yml new file mode 100644 index 00000000..308d934a --- /dev/null +++ b/tests/fixtures/matcher/dynamic/process.yml @@ -0,0 +1,40 @@ +- name: process-scope-basic + description: process scope matches features aggregated across threads + rules: + - name: process-apis + scopes: + dynamic: process + features: + - and: + - api: CreateFileW + - api: WriteFile + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 1: api(CreateFileW) + thread: 3065 + call: 2: api(WriteFile) + expect: + matches: + process-apis: + - "process{pid:3052}" + +- name: process-scope-no-match + description: process scope does not match when features are split across processes + rules: + - name: process-split + scopes: + dynamic: process + features: + - and: + - api: CreateFileW + - api: WriteFile + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 1: api(CreateFileW) + proc: other.exe (pid=3053) + thread: 4000 + call: 2: api(WriteFile) + expect: + matches: {} diff --git a/tests/fixtures/matcher/dynamic/span.yml b/tests/fixtures/matcher/dynamic/span.yml new file mode 100644 index 00000000..23536f09 --- /dev/null +++ b/tests/fixtures/matcher/dynamic/span.yml @@ -0,0 +1,316 @@ +- name: span-window-contains-all + description: span-of-calls matches when all features fall within the window + options: + span size: 2 + rules: + - name: span-resolve-add-veh + description: should match the span ending at the call that resolves AddVectoredExceptionHandler + scopes: + dynamic: span of calls + features: + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: AddVectoredExceptionHandler + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) + call: 12: api(RtlAddVectoredExceptionHandler) + expect: + matches: + span-resolve-add-veh: + - 11 + +- name: span-window-too-small + description: span-of-calls does not match when the window is too small to contain all features + options: + span size: 2 + rules: + - name: span-window-too-small + description: should not match because the configured span window does not include both APIs together + scopes: + dynamic: span of calls + features: + - and: + - api: LdrGetDllHandle + - api: RtlAddVectoredExceptionHandler + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 12: api(RtlAddVectoredExceptionHandler) + expect: + matches: {} + +- name: span-with-count + description: span matches when count constraint is satisfied within the window + rules: + - name: span-count + scopes: + dynamic: span of calls + features: + - and: + - api: GetSystemTimeAsFileTime + - api: GetSystemInfo + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - count(api(LdrGetDllHandle)): 2 + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 8: api(GetSystemTimeAsFileTime) + call: 9: api(GetSystemInfo) + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) + call: 12: api(LdrGetDllHandle) + expect: + matches: + span-count: + - 12 + +- name: span-size-exactly-fits + description: span matches when features are exactly at the span window boundary + options: + span size: 3 + rules: + - name: span-boundary + scopes: + dynamic: span of calls + features: + - and: + - api: GetSystemTimeAsFileTime + - api: LdrGetDllHandle + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 8: api(GetSystemTimeAsFileTime) + call: 9: api(GetSystemInfo) + call: 10: api(LdrGetDllHandle) + expect: + matches: + span-boundary: + - 10 + +- name: span-size-off-by-one + description: span does not match when features are just outside the window boundary + options: + span size: 2 + rules: + - name: span-off-by-one + scopes: + dynamic: span of calls + features: + - and: + - api: GetSystemTimeAsFileTime + - api: LdrGetDllHandle + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 8: api(GetSystemTimeAsFileTime) + call: 9: api(GetSystemInfo) + call: 10: api(LdrGetDllHandle) + expect: + matches: {} + +- name: span-length-too-short + description: span does not match when features are outside the span window + options: + span size: 5 + rules: + - name: span-length + scopes: + dynamic: span of calls + features: + - and: + - api: GetSystemTimeAsFileTime + - api: RtlAddVectoredExceptionHandler + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 8: api(GetSystemTimeAsFileTime) + call: 9: api(GetSystemInfo) + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 12: api(LdrGetDllHandle) + call: 13: api(LdrGetProcedureAddress) + call: 14: api(RtlAddVectoredExceptionHandler) + expect: + matches: {} + +- name: span-call-subscope + description: call subscope within span matches features at a single call + rules: + - name: span-call-sub + scopes: + dynamic: span of calls + features: + - and: + - call: + - and: + - api: LdrGetProcedureAddress + - string: AddVectoredExceptionHandler + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) + call: 12: api(LdrGetDllHandle) + expect: + matches: + span-call-sub: + - 11 + +- name: span-nested-span-subscopes + description: nested span subscopes match when each sub-span is satisfied + rules: + - name: span-nested + scopes: + dynamic: span of calls + features: + - and: + - span of calls: + - description: resolve add VEH + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: AddVectoredExceptionHandler + - span of calls: + - description: resolve remove VEH + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: RemoveVectoredExceptionHandler + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) + call: 12: api(LdrGetDllHandle) + call: 13: api(LdrGetProcedureAddress) + call: 13: string(RemoveVectoredExceptionHandler) + expect: + matches: + span-nested: + - 13 + +- name: span-example-practical + description: practical span pattern combining call subscopes with direct API feature + rules: + - name: span-practical + scopes: + dynamic: span of calls + features: + - and: + - call: + - and: + - api: LdrGetDllHandle + - string: "kernel32.dll" + - call: + - and: + - api: LdrGetProcedureAddress + - string: "AddVectoredExceptionHandler" + - api: RtlAddVectoredExceptionHandler + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 10: string(kernel32.dll) + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) + call: 12: api(LdrGetDllHandle) + call: 12: string(kernel32.dll) + call: 13: api(LdrGetProcedureAddress) + call: 13: string(RemoveVectoredExceptionHandler) + call: 14: api(RtlAddVectoredExceptionHandler) + expect: + matches: + span-practical: + - 14 + +- name: span-overlapping-single-event + description: overlapping spans that match on a single event return only the first match + rules: + - name: span-overlap + scopes: + dynamic: span of calls + features: + - and: + - call: + - and: + - api: LdrGetProcedureAddress + - string: "AddVectoredExceptionHandler" + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) + call: 12: api(LdrGetDllHandle) + call: 13: api(LdrGetProcedureAddress) + call: 13: string(RemoveVectoredExceptionHandler) + expect: + matches: + span-overlap: + - 11 + +- name: span-match-statements + description: match statements work within span-of-calls rules including namespace matching + rules: + - name: resolve add VEH + namespace: linking/runtime-linking/veh + scopes: + dynamic: span of calls + features: + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: AddVectoredExceptionHandler + - name: resolve remove VEH + namespace: linking/runtime-linking/veh + scopes: + dynamic: span of calls + features: + - and: + - api: LdrGetDllHandle + - api: LdrGetProcedureAddress + - string: RemoveVectoredExceptionHandler + - name: resolve add and remove VEH + scopes: + dynamic: span of calls + features: + - and: + - match: resolve add VEH + - match: resolve remove VEH + - name: has VEH runtime linking + scopes: + dynamic: span of calls + features: + - and: + - match: linking/runtime-linking/veh + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 10: api(LdrGetDllHandle) + call: 11: api(LdrGetProcedureAddress) + call: 11: string(AddVectoredExceptionHandler) + call: 12: api(LdrGetDllHandle) + call: 13: api(LdrGetProcedureAddress) + call: 13: string(RemoveVectoredExceptionHandler) + expect: + matches: + resolve add VEH: + - 11 + - 13 + resolve remove VEH: + - 13 + resolve add and remove VEH: + - 13 + has VEH runtime linking: + - 11 diff --git a/tests/fixtures/matcher/dynamic/thread.yml b/tests/fixtures/matcher/dynamic/thread.yml new file mode 100644 index 00000000..af99e3c7 --- /dev/null +++ b/tests/fixtures/matcher/dynamic/thread.yml @@ -0,0 +1,38 @@ +- name: thread-scope-basic + description: thread scope matches features aggregated across calls within a thread + rules: + - name: thread-apis + scopes: + dynamic: thread + features: + - and: + - api: CreateFileW + - api: WriteFile + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 1: api(CreateFileW) + call: 2: api(WriteFile) + expect: + matches: + thread-apis: + - "process{pid:3052,tid:3064}" + +- name: thread-scope-no-match + description: thread scope does not match when features are split across different threads + rules: + - name: thread-split + scopes: + dynamic: thread + features: + - and: + - api: CreateFileW + - api: WriteFile + features: | + proc: sample.exe (pid=3052) + thread: 3064 + call: 1: api(CreateFileW) + thread: 3065 + call: 2: api(WriteFile) + expect: + matches: {} diff --git a/tests/fixtures/matcher/static/composition.yml b/tests/fixtures/matcher/static/composition.yml new file mode 100644 index 00000000..cffe31dc --- /dev/null +++ b/tests/fixtures/matcher/static/composition.yml @@ -0,0 +1,122 @@ +- name: match-rule-dependency + description: a rule using match can depend on another rule's result + rules: + - name: base rule + scopes: + static: function + features: + - number: 100 + - name: dependent rule + scopes: + static: function + features: + - match: base rule + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + expect: + matches: + base rule: + - 0x401000 + dependent rule: + - 0x401000 + +- name: namespace-match-direct + description: match on a namespace prefix matches rules in that namespace + rules: + - name: CreateFile API + namespace: file/create/CreateFile + scopes: + static: function + features: + - api: CreateFile + - name: file-create + scopes: + static: function + features: + - match: file/create + - name: filesystem-any + scopes: + static: function + features: + - match: file + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: api(CreateFile) + expect: + matches: + CreateFile API: + - 0x401000 + file-create: + - 0x401000 + filesystem-any: + - 0x401000 + +- name: namespace-match-intermediate-prefix + description: namespace match at an intermediate level matches rules below it + rules: + - name: kernel32 CreateFile + namespace: file/create/kernel32 + scopes: + static: function + features: + - api: CreateFile + - name: ntdll NtCreateFile + namespace: file/create/ntdll + scopes: + static: function + features: + - api: NtCreateFile + - name: any-file-create + scopes: + static: function + features: + - match: file/create + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: api(CreateFile) + expect: + matches: + kernel32 CreateFile: + - 0x401000 + any-file-create: + - 0x401000 + +- name: namespace-match-sibling-no-match + description: namespace match does not match sibling namespaces + rules: + - name: CreateFile API + namespace: file/create/CreateFile + scopes: + static: function + features: + - api: CreateFile + - name: WriteFile API + namespace: file/write + scopes: + static: function + features: + - api: WriteFile + - name: file-create + scopes: + static: function + features: + - match: file/create + - name: filesystem-any + scopes: + static: function + features: + - match: file + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: api(WriteFile) + expect: + matches: + WriteFile API: + - 0x401000 + filesystem-any: + - 0x401000 diff --git a/tests/fixtures/matcher/static/features.yml b/tests/fixtures/matcher/static/features.yml new file mode 100644 index 00000000..6992ed43 --- /dev/null +++ b/tests/fixtures/matcher/static/features.yml @@ -0,0 +1,361 @@ +- name: simple-number-match + description: basic number feature matches at function scope + rules: + - name: simple-number + scopes: + static: function + features: + - number: 100 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + expect: + matches: + simple-number: + - 0x401000 + +- name: not-with-and + description: not inside and prevents match when the negated feature is present + rules: + - name: not-with-and-present + scopes: + static: function + features: + - and: + - mnemonic: mov + - not: + - number: 99 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + insn: 0x401000: mnemonic(mov) + expect: + matches: + not-with-and-present: + - 0x401000 + +- name: operand-number-match + description: operand number matches by index and value + rules: + - name: operand-number + scopes: + static: function + features: + - operand[0].number: 0x10 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: operand[0].number(0x10) + expect: + matches: + operand-number: + - 0x401000 + +- name: operand-number-wrong-index + description: operand number does not match when the operand index differs + rules: + - name: operand-number-idx + scopes: + static: function + features: + - operand[0].number: 0x10 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: operand[1].number(0x10) + expect: + matches: {} + +- name: operand-number-wrong-value + description: operand number does not match when the value differs + rules: + - name: operand-number-val + scopes: + static: function + features: + - operand[0].number: 0x10 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: operand[0].number(0x11) + expect: + matches: {} + +- name: operand-offset-match + description: operand offset matches by index and value + rules: + - name: operand-offset + scopes: + static: function + features: + - operand[0].offset: 0x10 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: operand[0].offset(0x10) + expect: + matches: + operand-offset: + - 0x401000 + +- name: operand-offset-wrong-index + description: operand offset does not match when the operand index differs + rules: + - name: operand-offset-idx + scopes: + static: function + features: + - operand[0].offset: 0x10 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: operand[1].offset(0x10) + expect: + matches: {} + +- name: operand-offset-wrong-value + description: operand offset does not match when the value differs + rules: + - name: operand-offset-val + scopes: + static: function + features: + - operand[0].offset: 0x10 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: operand[0].offset(0x11) + expect: + matches: {} + +- name: property-read-match + description: property/read matches the correct property name and access mode + rules: + - name: property-read + scopes: + static: function + features: + - property/read: System.IO.FileInfo::Length + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: property/read(System.IO.FileInfo::Length) + expect: + matches: + property-read: + - 0x401000 + +- name: property-read-wrong-access + description: property/read does not match a property/write feature + rules: + - name: property-read-access + scopes: + static: function + features: + - property/read: System.IO.FileInfo::Length + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: property/write(System.IO.FileInfo::Length) + expect: + matches: {} + +- name: property-read-wrong-value + description: property/read does not match a different property name + rules: + - name: property-read-value + scopes: + static: function + features: + - property/read: System.IO.FileInfo::Length + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: property/read(System.IO.FileInfo::Size) + expect: + matches: {} + +- name: os-any-matches-specific + description: os any matches when a specific os feature is present + rules: + - name: os-any-specific + scopes: + static: function + features: + - or: + - and: + - or: + - os: windows + - os: linux + - os: macos + - string: "Hello world" + - and: + - os: any + - string: "Goodbye world" + features: | + func: 0x401000 + func: 0x401000: string(Hello world) + bb: 0x401000: basic block + insn: 0x401000: os(windows) + expect: + matches: + os-any-specific: + - 0x401000 + +- name: os-any-matches-any + description: os any feature matches for "Goodbye world" path + rules: + - name: os-any-goodbye + scopes: + static: function + features: + - or: + - and: + - os: any + - string: "Goodbye world" + features: | + func: 0x401000 + func: 0x401000: string(Goodbye world) + bb: 0x401000: basic block + insn: 0x401000: os(any) + expect: + matches: + os-any-goodbye: + - 0x401000 + +- name: os-any-matches-specific-os + description: rule with os any matches when extracted feature is a specific os + rules: + - name: os-any-wildcard + scopes: + static: function + features: + - and: + - os: any + - string: "Hello world" + features: | + func: 0x401000 + func: 0x401000: string(Hello world) + bb: 0x401000: basic block + insn: 0x401000: os(windows) + expect: + matches: + os-any-wildcard: + - 0x401000 + +- name: bytes-exact-match + description: bytes feature matches when extracted bytes are identical + rules: + - name: bytes-exact + scopes: + static: function + features: + - bytes: 90 90 90 90 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: bytes(90909090) + expect: + matches: + bytes-exact: + - 0x401000 + +- name: bytes-prefix-match + description: bytes feature matches when extracted bytes start with the rule pattern + rules: + - name: bytes-prefix + scopes: + static: function + features: + - bytes: 90 90 90 90 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: bytes(9090909090909090) + expect: + matches: + bytes-prefix: + - 0x401000 + +- name: bytes-no-match + description: bytes feature does not match when extracted bytes differ + rules: + - name: bytes-diff + scopes: + static: function + features: + - bytes: 90 90 90 90 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: bytes(00000000) + expect: + matches: {} + +- name: bytes-too-short + description: bytes feature does not match when extracted bytes are shorter than the pattern + rules: + - name: bytes-short + scopes: + static: function + features: + - bytes: 90 90 90 90 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: bytes(9090) + expect: + matches: {} + +- name: negative-number-match + description: negative number matches correctly + rules: + - name: negative-num + scopes: + static: function + features: + - number: -1 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(-1) + expect: + matches: + negative-num: + - 0x401000 + +- name: number-zero-match + description: number zero matches correctly and is not confused with absence + rules: + - name: num-zero + scopes: + static: function + features: + - number: 0 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(0) + expect: + matches: + num-zero: + - 0x401000 + +- name: characteristic-match + description: characteristic feature matches correctly + rules: + - name: char-nzxor + scopes: + static: function + features: + - characteristic: nzxor + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: characteristic(nzxor) + expect: + matches: + char-nzxor: + - 0x401000 diff --git a/tests/fixtures/matcher/static/logic.yml b/tests/fixtures/matcher/static/logic.yml new file mode 100644 index 00000000..935f97e0 --- /dev/null +++ b/tests/fixtures/matcher/static/logic.yml @@ -0,0 +1,712 @@ +- name: and-both-present + description: and requires all children to match + rules: + - name: and-match + description: should match because the function contains both mov and number 0x10 + scopes: + static: function + features: + - and: + - mnemonic: mov + - number: 0x10 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + insn: 0x402000: number(0x10) + expect: + matches: + and-match: + - 0x402000 + +- name: or-one-branch-present + description: or requires at least one child to match + rules: + - name: or-match + description: should match because one branch of the or is satisfied by number 0x10 + scopes: + static: function + features: + - or: + - api: CreateFileW + - number: 0x10 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: number(0x10) + expect: + matches: + or-match: + - 0x402000 + +- name: not-absent-feature + description: not succeeds when the child feature is absent + rules: + - name: not-match + description: should match because mov is present and number 0x20 is absent + scopes: + static: function + features: + - and: + - mnemonic: mov + - not: + - number: 0x20 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + expect: + matches: + not-match: + - 0x402000 + +- name: optional-absent-feature + description: optional does not prevent a match when the child feature is absent + rules: + - name: optional-match + description: should match even though the optional child is absent + scopes: + static: function + features: + - and: + - mnemonic: mov + - optional: + - number: 0x30 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + expect: + matches: + optional-match: + - 0x402000 + +- name: count-exact + description: count matches when the feature appears the exact number of times + rules: + - name: count-exact-match + description: should match because number 0x10 appears exactly twice + scopes: + static: function + features: + - count(number(0x10)): 2 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: number(0x10) + insn: 0x402002: number(0x10) + expect: + matches: + count-exact-match: + - 0x402000 + +- name: count-range + description: count matches when the feature count falls within the range + rules: + - name: count-range-match + description: should match because number 0x10 appears within the allowed range + scopes: + static: function + features: + - count(number(0x10)): (1, 2) + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: number(0x10) + insn: 0x402002: number(0x10) + expect: + matches: + count-range-match: + - 0x402000 + +- name: count-mismatch + description: count does not match when the feature count differs + rules: + - name: count-negative-no-match + description: should not match because number 0x10 does not appear three times + scopes: + static: function + features: + - count(number(0x10)): 3 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: number(0x10) + insn: 0x402002: number(0x10) + expect: + matches: {} + +- name: count-exact-not-enough + description: count does not match when there are fewer occurrences than required + rules: + - name: count-exact-not-enough + scopes: + static: function + features: + - count(number(100)): 2 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + expect: + matches: {} + +- name: count-exact-too-many + description: count does not match when there are more occurrences than required + rules: + - name: count-exact-too-many + scopes: + static: function + features: + - count(number(100)): 2 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + insn: 0x401002: number(100) + insn: 0x401004: number(100) + expect: + matches: {} + +- name: count-range-at-lower-bound + description: count range matches at the lower bound + rules: + - name: count-range-lower + scopes: + static: function + features: + - count(number(100)): (2, 3) + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + insn: 0x401002: number(100) + expect: + matches: + count-range-lower: + - 0x401000 + +- name: count-range-at-upper-bound + description: count range matches at the upper bound + rules: + - name: count-range-upper + scopes: + static: function + features: + - count(number(100)): (2, 3) + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + insn: 0x401002: number(100) + insn: 0x401004: number(100) + expect: + matches: + count-range-upper: + - 0x401000 + +- name: count-range-below-lower-bound + description: count range does not match when below the lower bound + rules: + - name: count-range-below + scopes: + static: function + features: + - count(number(100)): (2, 3) + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + expect: + matches: {} + +- name: count-range-above-upper-bound + description: count range does not match when above the upper bound + rules: + - name: count-range-above + scopes: + static: function + features: + - count(number(100)): (2, 3) + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(100) + insn: 0x401002: number(100) + insn: 0x401004: number(100) + insn: 0x401006: number(100) + expect: + matches: {} + +- name: count-zero-absent + description: count zero matches when the feature is absent + rules: + - name: count-zero-absent + scopes: + static: function + features: + - and: + - count(number(100)): 0 + - mnemonic: mov + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + expect: + matches: + count-zero-absent: + - 0x401000 + +- name: count-zero-present + description: count zero does not match when the feature is present + rules: + - name: count-zero-present + scopes: + static: function + features: + - and: + - count(number(100)): 0 + - mnemonic: mov + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401000: number(100) + expect: + matches: {} + +- name: count-range-with-zero-absent + description: count range including zero matches when feature is absent + rules: + - name: count-range-zero + scopes: + static: function + features: + - and: + - count(number(100)): (0, 1) + - mnemonic: mov + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + expect: + matches: + count-range-zero: + - 0x401000 + +- name: count-range-with-zero-one-present + description: count range including zero matches when feature appears once + rules: + - name: count-range-zero-one + scopes: + static: function + features: + - and: + - count(number(100)): (0, 1) + - mnemonic: mov + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401002: number(100) + expect: + matches: + count-range-zero-one: + - 0x401000 + +- name: count-range-with-zero-too-many + description: count range including zero does not match when feature appears too many times + rules: + - name: count-range-zero-many + scopes: + static: function + features: + - and: + - count(number(100)): (0, 1) + - mnemonic: mov + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401002: number(100) + insn: 0x401004: number(100) + expect: + matches: {} + +- name: two-or-more-match + description: N-or-more matches when at least two children are satisfied + rules: + - name: two-or-more + scopes: + static: function + features: + - or: + - and: + - number: 1 + - number: 2 + - or: + - number: 3 + - 2 or more: + - number: 4 + - number: 5 + - number: 6 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(5) + insn: 0x401002: number(6) + expect: + matches: + two-or-more: + - 0x401000 + +- name: two-or-more-not-enough + description: N-or-more does not match when fewer than two children are satisfied + rules: + - name: two-or-more-fail + scopes: + static: function + features: + - or: + - and: + - number: 1 + - number: 2 + - or: + - number: 3 + - 2 or more: + - number: 4 + - number: 5 + - number: 6 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(6) + expect: + matches: {} + +- name: count-string-match + description: count on string features matches the exact occurrence count + rules: + - name: count-string + scopes: + static: function + features: + - count(string(foo)): 2 + features: | + func: 0x401000 + func: 0x401000: string(foo) + bb: 0x401000: basic block + insn: 0x401002: string(foo) + expect: + matches: + count-string: + - 0x401000 + +- name: count-string-not-enough + description: count on string features does not match with too few occurrences + rules: + - name: count-string-few + scopes: + static: function + features: + - count(string(foo)): 2 + features: | + func: 0x401000 + func: 0x401000: string(foo) + expect: + matches: {} + +- name: count-string-too-many + description: count on string features does not match with too many occurrences + rules: + - name: count-string-many + scopes: + static: function + features: + - count(string(foo)): 2 + features: | + func: 0x401000 + func: 0x401000: string(foo) + bb: 0x401000: basic block + insn: 0x401002: string(foo) + insn: 0x401004: string(foo) + expect: + matches: {} + +- name: count-api-match + description: count on api features matches the exact occurrence count + rules: + - name: count-api + scopes: + static: function + features: + - count(api(CreateFileA)): 1 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: api(CreateFileA) + expect: + matches: + count-api: + - 0x401000 + +- name: count-api-no-match + description: count on api features does not match with zero occurrences + rules: + - name: count-api-zero + scopes: + static: function + features: + - count(api(CreateFileA)): 1 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: api(CreateFile) + expect: + matches: {} + +- name: count-offset-match + description: count on offset features matches the exact occurrence count + rules: + - name: count-offset + scopes: + static: function + features: + - or: + - count(offset(2)): 1 + - count(offset(0x100)): 2 or more + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: offset(2) + expect: + matches: + count-offset: + - 0x401000 + +- name: count-offset-or-more + description: count "2 or more" on offset features matches when threshold is met + rules: + - name: count-offset-more + scopes: + static: function + features: + - or: + - count(offset(2)): 1 + - count(offset(0x100)): 2 or more + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: offset(0x100) + insn: 0x401002: offset(0x100) + insn: 0x401004: offset(0x100) + expect: + matches: + count-offset-more: + - 0x401000 + +- name: and-one-missing + description: and does not match when one operand is absent + rules: + - name: and-incomplete + scopes: + static: function + features: + - and: + - mnemonic: mov + - number: 0x10 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + expect: + matches: {} + +- name: or-no-branches-present + description: or does not match when no branches are satisfied + rules: + - name: or-none + scopes: + static: function + features: + - or: + - api: CreateFileW + - number: 0x10 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + expect: + matches: {} + +- name: not-present-prevents-match + description: not blocks a match when the negated feature is present + rules: + - name: not-blocks + scopes: + static: function + features: + - and: + - mnemonic: mov + - not: + - number: 0x10 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + insn: 0x402000: number(0x10) + expect: + matches: {} + +- name: optional-present-still-matches + description: optional does not prevent a match when the child feature is present + rules: + - name: optional-present + scopes: + static: function + features: + - and: + - mnemonic: mov + - optional: + - number: 0x10 + features: | + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(mov) + insn: 0x402002: number(0x10) + expect: + matches: + optional-present: + - 0x402000 + +- name: count-inside-not + description: not negates a count that is satisfied + rules: + - name: count-not + scopes: + static: function + features: + - and: + - mnemonic: mov + - not: + - count(number(100)): 2 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401002: number(100) + insn: 0x401004: number(100) + expect: + matches: {} + +- name: count-inside-not-unsatisfied + description: not succeeds when the count is not satisfied + rules: + - name: count-not-ok + scopes: + static: function + features: + - and: + - mnemonic: mov + - not: + - count(number(100)): 3 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401002: number(100) + insn: 0x401004: number(100) + expect: + matches: + count-not-ok: + - 0x401000 + +- name: deeply-nested-logic + description: three levels of nesting evaluate correctly + rules: + - name: deep-nest + scopes: + static: function + features: + - and: + - or: + - and: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(1) + insn: 0x401002: number(2) + insn: 0x401004: number(4) + expect: + matches: + deep-nest: + - 0x401000 + +- name: deeply-nested-logic-inner-fail + description: deeply nested logic fails when inner and is not satisfied + rules: + - name: deep-nest-fail + scopes: + static: function + features: + - and: + - or: + - and: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(1) + insn: 0x401004: number(4) + expect: + matches: {} + +- name: three-or-more-at-threshold + description: N-or-more matches when exactly N children are satisfied + rules: + - name: three-or-more + scopes: + static: function + features: + - 3 or more: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(1) + insn: 0x401002: number(2) + insn: 0x401004: number(3) + expect: + matches: + three-or-more: + - 0x401000 + +- name: three-or-more-below-threshold + description: N-or-more does not match when fewer than N children are satisfied + rules: + - name: three-or-more-fail + scopes: + static: function + features: + - 3 or more: + - number: 1 + - number: 2 + - number: 3 + - number: 4 + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: number(1) + insn: 0x401002: number(2) + expect: + matches: {} diff --git a/tests/fixtures/matcher/static/parsing.yml b/tests/fixtures/matcher/static/parsing.yml new file mode 100644 index 00000000..ad77545e --- /dev/null +++ b/tests/fixtures/matcher/static/parsing.yml @@ -0,0 +1,33 @@ +- name: hex-number-with-e + description: hex literals containing the letter e are parsed as integers, not floats + rules: + - name: hex-e-match + scopes: + static: function + features: + - number: 0x1e + features: | + func: 0x600000 + bb: 0x600000: basic block + insn: 0x600000: number(0x1e) + expect: + matches: + hex-e-match: + - 0x600000 + +- name: colon-in-feature-text + description: feature text containing a colon is parsed correctly + rules: + - name: colon-string-match + scopes: + static: function + features: + - string: "key: value" + features: | + func: 0x600000 + bb: 0x600000: basic block + insn: 0x600001: string(key: value) + expect: + matches: + colon-string-match: + - 0x600000 diff --git a/tests/fixtures/matcher/static/scopes.yml b/tests/fixtures/matcher/static/scopes.yml new file mode 100644 index 00000000..ee4f24db --- /dev/null +++ b/tests/fixtures/matcher/static/scopes.yml @@ -0,0 +1,141 @@ +- name: function-scope-aggregates-basic-blocks + description: function scope sees features from all basic blocks in the function + rules: + - name: function-cross-basic-block + description: should match when function scope aggregates features from different basic blocks + scopes: + static: function + features: + - and: + - mnemonic: mov + - mnemonic: add + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + bb: 0x401010: basic block + insn: 0x401010: mnemonic(add) + expect: + matches: + function-cross-basic-block: + - 0x401000 + +- name: basic-block-scope-does-not-aggregate + description: basic block scope only sees features within a single basic block + rules: + - name: basic-block-cross-basic-block + description: should not match because no single basic block contains both mnemonics + scopes: + static: basic block + features: + - and: + - mnemonic: mov + - mnemonic: add + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + bb: 0x401010: basic block + insn: 0x401010: mnemonic(add) + expect: + matches: {} + +- name: instruction-scope-single-instruction + description: instruction scope matches at the single instruction containing the feature + rules: + - name: instruction-single-mnemonic + description: should match once at the instruction that contains mov + scopes: + static: instruction + features: + - mnemonic: mov + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + bb: 0x401010: basic block + insn: 0x401010: mnemonic(add) + expect: + matches: + instruction-single-mnemonic: + - 0x401000 + +- name: function-scope-isolation + description: features from one function do not leak into another function's match + rules: + - name: both-features + scopes: + static: function + features: + - and: + - mnemonic: mov + - mnemonic: add + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + func: 0x402000 + bb: 0x402000: basic block + insn: 0x402000: mnemonic(add) + expect: + matches: {} + +- name: file-scope-basic + description: file scope matches features extracted at file level + rules: + - name: file-import + scopes: + static: file + features: + - import: kernel32.CreateFileW + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + file: 0x401000: import(kernel32.CreateFileW) + expect: + matches: + file-import: + - "no address" + +- name: basic-block-scope-match + description: basic block scope matches when a single BB contains all required features + rules: + - name: bb-both + scopes: + static: basic block + features: + - and: + - mnemonic: mov + - mnemonic: add + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401002: mnemonic(add) + bb: 0x401010: basic block + insn: 0x401010: mnemonic(xor) + expect: + matches: + bb-both: + - 0x401000 + +- name: instruction-scope-multiple-matches + description: instruction scope reports multiple matching instructions + rules: + - name: insn-multi + scopes: + static: instruction + features: + - mnemonic: mov + features: | + func: 0x401000 + bb: 0x401000: basic block + insn: 0x401000: mnemonic(mov) + insn: 0x401002: mnemonic(add) + insn: 0x401004: mnemonic(mov) + expect: + matches: + insn-multi: + - 0x401000 + - 0x401004 diff --git a/tests/fixtures/matcher/static/strings.yml b/tests/fixtures/matcher/static/strings.yml new file mode 100644 index 00000000..a5054292 --- /dev/null +++ b/tests/fixtures/matcher/static/strings.yml @@ -0,0 +1,274 @@ +- name: exact-string + description: string feature matches only the exact value + rules: + - name: exact-string-match + scopes: + static: function + features: + - string: hello world + features: | + func: 0x500000 + func: 0x500000: string(hello world) + expect: + matches: + exact-string-match: + - 0x500000 + +- name: substring + description: substring feature matches when the value appears within a string + rules: + - name: substring-match + scopes: + static: function + features: + - substring: abc + features: | + func: 0x500000 + func: 0x500000: string(zabczz) + expect: + matches: + substring-match: + - 0x500000 + +- name: regex-unanchored + description: regex feature matches when the pattern appears anywhere in a string + rules: + - name: regex-match + scopes: + static: function + features: + - string: /bbbb/ + features: | + func: 0x500000 + func: 0x500000: string(abbbba) + expect: + matches: + regex-match: + - 0x500000 + +- name: regex-case-insensitive + description: regex /i flag enables case-insensitive matching + rules: + - name: regex-ignorecase-match + scopes: + static: function + features: + - string: /BBBB/i + features: | + func: 0x500000 + func: 0x500000: string(abbbba) + expect: + matches: + regex-ignorecase-match: + - 0x500000 + +- name: regex-anchored-no-match + description: anchored regex does not match when the string does not start with the pattern + rules: + - name: regex-anchor-no-match + scopes: + static: function + features: + - string: /^bbbb/ + features: | + func: 0x500000 + func: 0x500000: string(abbbba) + expect: + matches: {} + +- name: substring-no-match + description: substring does not match when the value is not contained in any string + rules: + - name: substring-no-match + scopes: + static: function + features: + - substring: abc + features: | + func: 0x500000 + func: 0x500000: string(aaaa) + expect: + matches: {} + +- name: substring-exact + description: substring matches when the string is exactly the substring value + rules: + - name: substring-exact + scopes: + static: function + features: + - substring: abc + features: | + func: 0x500000 + func: 0x500000: string(abc) + expect: + matches: + substring-exact: + - 0x500000 + +- name: substring-prefix + description: substring matches when it appears at the start of a string + rules: + - name: substring-prefix + scopes: + static: function + features: + - substring: abc + features: | + func: 0x500000 + func: 0x500000: string(abc222) + expect: + matches: + substring-prefix: + - 0x500000 + +- name: substring-suffix + description: substring matches when it appears at the end of a string + rules: + - name: substring-suffix + scopes: + static: function + features: + - substring: abc + features: | + func: 0x500000 + func: 0x500000: string(111abc) + expect: + matches: + substring-suffix: + - 0x500000 + +- name: regex-no-match-wrong-type + description: regex does not match when only non-string features are present + rules: + - name: regex-wrong-type + scopes: + static: function + features: + - string: /.*bbbb.*/ + features: | + func: 0x500000 + bb: 0x500000: basic block + insn: 0x500000: number(100) + expect: + matches: {} + +- name: regex-no-match-wrong-value + description: regex does not match when the string does not contain the pattern + rules: + - name: regex-wrong-value + scopes: + static: function + features: + - string: /.*bbbb.*/ + features: | + func: 0x500000 + func: 0x500000: string(aaaa) + expect: + matches: {} + +- name: regex-no-match-case-sensitive + description: regex without /i flag does not match different-case strings + rules: + - name: regex-case-sensitive + scopes: + static: function + features: + - string: /.*bbbb.*/ + features: | + func: 0x500000 + func: 0x500000: string(aBBBBa) + expect: + matches: {} + +- name: regex-explicit-wildcards + description: regex with explicit .* wildcards matches the same as implied wildcards + rules: + - name: regex-explicit + scopes: + static: function + features: + - string: /.*bbbb.*/ + - name: regex-implied + scopes: + static: function + features: + - string: /bbbb/ + features: | + func: 0x500000 + func: 0x500000: string(abbbba) + expect: + matches: + regex-explicit: + - 0x500000 + regex-implied: + - 0x500000 + +- name: regex-complex-backslash + description: regex with escaped backslashes and spaces matches correctly + rules: + - name: regex-backslash + scopes: + static: function + features: + - or: + - string: /.*HARDWARE\\Key\\key with spaces\\.*/i + features: | + func: 0x500000 + func: 0x500000: string(Hardware\Key\key with spaces\some value) + expect: + matches: + regex-backslash: + - 0x500000 + +- name: regex-numeric-string + description: regex matches numeric string values correctly + rules: + - name: regex-numeric + scopes: + static: function + features: + - or: + - string: /123/ + - string: /0x123/ + features: | + func: 0x500000 + func: 0x500000: string(123) + expect: + matches: + regex-numeric: + - 0x500000 + +- name: regex-hex-numeric-string + description: regex matches hex-prefixed numeric string values + rules: + - name: regex-hex-numeric + scopes: + static: function + features: + - or: + - string: /123/ + - string: /0x123/ + features: | + func: 0x500000 + func: 0x500000: string(0x123) + expect: + matches: + regex-hex-numeric: + - 0x500000 + +- name: regex-ignorecase-explicit-wildcards + description: regex /i flag with explicit wildcards matches case-insensitively + rules: + - name: regex-ignorecase-explicit + scopes: + static: function + features: + - string: /.*bbbb.*/i + features: | + func: 0x500000 + func: 0x500000: string(aBBBBa) + expect: + matches: + regex-ignorecase-explicit: + - 0x500000 diff --git a/tests/test_dynamic_span_of_calls_scope.py b/tests/test_dynamic_span_of_calls_scope.py index d06f80e9..bb9abf20 100644 --- a/tests/test_dynamic_span_of_calls_scope.py +++ b/tests/test_dynamic_span_of_calls_scope.py @@ -12,258 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -# tests/data/dynamic/cape/v2.2/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# ... -# thread: 3064 -# call 8: GetSystemTimeAsFileTime() -# call 9: GetSystemInfo() -# call 10: LdrGetDllHandle(1974337536, kernel32.dll) -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 12: LdrGetDllHandle(1974337536, kernel32.dll) -# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 14: RtlAddVectoredExceptionHandler(1921490089, 0) -# call 15: GetSystemTime() -# call 16: NtAllocateVirtualMemory(no, 4, 786432, 4784128, 4294967295) -# call 17: NtAllocateVirtualMemory(no, 4, 12288, 4784128, 4294967295) -# call 18: GetSystemInfo() -# ... -# ... - import textwrap -from typing import Iterator -from functools import lru_cache import pytest -import fixtures import capa.rules -import capa.capabilities.dynamic -from capa.features.extractors.base_extractor import ThreadFilter, DynamicFeatureExtractor -def filter_threads(extractor: DynamicFeatureExtractor, ppid: int, pid: int, tid: int) -> DynamicFeatureExtractor: - for ph in extractor.get_processes(): - if (ph.address.ppid, ph.address.pid) != (ppid, pid): - continue - - for th in extractor.get_threads(ph): - if th.address.tid != tid: - continue - - return ThreadFilter( - extractor, - { - th.address, - }, - ) - - raise ValueError("failed to find target thread") - - -@lru_cache(maxsize=1) -def get_0000a657_thread3064(): - extractor = fixtures.get_cape_extractor( - fixtures.CD - / "data" - / "dynamic" - / "cape" - / "v2.2" - / "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz" - ) - extractor = filter_threads(extractor, 2456, 3052, 3064) - return extractor - - -def get_call_ids(matches) -> Iterator[int]: - for address, _ in matches: - yield address.id - - -# sanity check: match the first call -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# call 8: GetSystemTimeAsFileTime() -def test_dynamic_call_scope(): - extractor = get_0000a657_thread3064() - - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: unsupported - dynamic: call - features: - - api: GetSystemTimeAsFileTime - """) - - r = capa.rules.Rule.from_yaml(rule) - ruleset = capa.rules.RuleSet([r]) - - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in capabilities.matches - assert 8 in get_call_ids(capabilities.matches[r.name]) - - -# match the first span. -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# call 8: GetSystemTimeAsFileTime() -# call 9: GetSystemInfo() -# call 10: LdrGetDllHandle(1974337536, kernel32.dll) -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 12: LdrGetDllHandle(1974337536, kernel32.dll) -def test_dynamic_span_scope(): - extractor = get_0000a657_thread3064() - - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - api: GetSystemTimeAsFileTime - - api: GetSystemInfo - - api: LdrGetDllHandle - - api: LdrGetProcedureAddress - - count(api(LdrGetDllHandle)): 2 - """) - - r = capa.rules.Rule.from_yaml(rule) - ruleset = capa.rules.RuleSet([r]) - - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in capabilities.matches - assert 12 in get_call_ids(capabilities.matches[r.name]) - - -# show that when the span is only 5 calls long (for example), it doesn't match beyond that 5-tuple. -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# call 8: GetSystemTimeAsFileTime() -# call 9: GetSystemInfo() -# call 10: LdrGetDllHandle(1974337536, kernel32.dll) -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 12: LdrGetDllHandle(1974337536, kernel32.dll) -# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 14: RtlAddVectoredExceptionHandler(1921490089, 0) -# call 15: GetSystemTime() -# call 16: NtAllocateVirtualMemory(no, 4, 786432, 4784128, 4294967295) -def test_dynamic_span_scope_length(): - extractor = get_0000a657_thread3064() - - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - api: GetSystemTimeAsFileTime - - api: RtlAddVectoredExceptionHandler - """) - - r = capa.rules.Rule.from_yaml(rule) - ruleset = capa.rules.RuleSet([r]) - - # patch SPAN_SIZE since we may use a much larger value in the real world. - from pytest import MonkeyPatch - - with MonkeyPatch.context() as m: - m.setattr(capa.capabilities.dynamic, "SPAN_SIZE", 5) - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - - assert r.name not in capabilities.matches - - -# show that you can use a call subscope in span-of-calls rules. -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# ... -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# ... -def test_dynamic_span_call_subscope(): - extractor = get_0000a657_thread3064() - - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - call: - - and: - - api: LdrGetProcedureAddress - - string: AddVectoredExceptionHandler - """) - - r = capa.rules.Rule.from_yaml(rule) - ruleset = capa.rules.RuleSet([r]) - - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in capabilities.matches - assert 11 in get_call_ids(capabilities.matches[r.name]) - - -# show that you can use a span subscope in span rules. -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# ... -# call 10: LdrGetDllHandle(1974337536, kernel32.dll) -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 12: LdrGetDllHandle(1974337536, kernel32.dll) -# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) -# ... -def test_dynamic_span_scope_span_subscope(): - extractor = get_0000a657_thread3064() - - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - span of calls: - - description: resolve add VEH # should match at 11 - - and: - - api: LdrGetDllHandle - - api: LdrGetProcedureAddress - - string: AddVectoredExceptionHandler - - span of calls: - - description: resolve remove VEH # should match at 13 - - and: - - api: LdrGetDllHandle - - api: LdrGetProcedureAddress - - string: RemoveVectoredExceptionHandler - """) - - r = capa.rules.Rule.from_yaml(rule) - ruleset = capa.rules.RuleSet([r]) - - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in capabilities.matches - assert 13 in get_call_ids(capabilities.matches[r.name]) - - -# show that you can't use thread subscope in span rules. def test_dynamic_span_scope_thread_subscope(): rule = textwrap.dedent(""" rule: @@ -280,170 +35,3 @@ def test_dynamic_span_scope_thread_subscope(): with pytest.raises(capa.rules.InvalidRule): capa.rules.Rule.from_yaml(rule) - - -# show how you might use a span-of-calls rule: to match a small window for a collection of features. -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# call 10: LdrGetDllHandle(1974337536, kernel32.dll) -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 12: ... -# call 13: ... -# call 14: RtlAddVectoredExceptionHandler(1921490089, 0) -def test_dynamic_span_example(): - extractor = get_0000a657_thread3064() - - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - call: - - and: - - api: LdrGetDllHandle - - string: "kernel32.dll" - - call: - - and: - - api: LdrGetProcedureAddress - - string: "AddVectoredExceptionHandler" - - api: RtlAddVectoredExceptionHandler - """) - - r = capa.rules.Rule.from_yaml(rule) - ruleset = capa.rules.RuleSet([r]) - - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in capabilities.matches - assert 14 in get_call_ids(capabilities.matches[r.name]) - - -# show how spans that overlap a single event are handled. -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# ... -# call 10: ... -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 12: ... -# call 13: ... -# call 14: ... -# call 15: ... -# ... -def test_dynamic_span_multiple_spans_overlapping_single_event(): - extractor = get_0000a657_thread3064() - - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - call: - - and: - - api: LdrGetProcedureAddress - - string: "AddVectoredExceptionHandler" - """) - - r = capa.rules.Rule.from_yaml(rule) - ruleset = capa.rules.RuleSet([r]) - - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - assert r.name in capabilities.matches - # we only match the first overlapping span - assert [11] == list(get_call_ids(capabilities.matches[r.name])) - - -# show that you can use match statements in span-of-calls rules. -# -# proc: 0000A65749F5902C4D82.exe (ppid=2456, pid=3052) -# thread: 3064 -# ... -# call 10: LdrGetDllHandle(1974337536, kernel32.dll) -# call 11: LdrGetProcedureAddress(2010595649, 0, AddVectoredExceptionHandler, 1974337536, kernel32.dll) -# call 12: LdrGetDllHandle(1974337536, kernel32.dll) -# call 13: LdrGetProcedureAddress(2010595072, 0, RemoveVectoredExceptionHandler, 1974337536, kernel32.dll) -# ... -def test_dynamic_span_scope_match_statements(): - extractor = get_0000a657_thread3064() - - ruleset = capa.rules.RuleSet([ - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: resolve add VEH - namespace: linking/runtime-linking/veh - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - api: LdrGetDllHandle - - api: LdrGetProcedureAddress - - string: AddVectoredExceptionHandler - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: resolve remove VEH - namespace: linking/runtime-linking/veh - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - api: LdrGetDllHandle - - api: LdrGetProcedureAddress - - string: RemoveVectoredExceptionHandler - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: resolve add and remove VEH - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - match: resolve add VEH - - match: resolve remove VEH - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: has VEH runtime linking - scopes: - static: unsupported - dynamic: span of calls - features: - - and: - - match: linking/runtime-linking/veh - """) - ), - ]) - - capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) - - # basic functionality, already known to work - assert "resolve add VEH" in capabilities.matches - assert "resolve remove VEH" in capabilities.matches - - # requires `match: ` to be working - assert "resolve add and remove VEH" in capabilities.matches - - # requires `match: ` to be working - assert "has VEH runtime linking" in capabilities.matches diff --git a/tests/test_engine.py b/tests/test_engine.py index 637fa572..a4ece82e 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -15,7 +15,7 @@ import pytest import capa.features.address -from capa.engine import Or, And, Not, Some, Range +from capa.engine import Or from capa.features.insn import Number from capa.features.address import ( ThreadAddress, @@ -78,136 +78,6 @@ def test_dn_token_offset_address_cross_type_lt(): assert (addr < DNTokenOffsetAddress(0x1000, 0x10)) is False -def test_number(): - assert bool(Number(1).evaluate({Number(0): {ADDR1}})) is False - assert bool(Number(1).evaluate({Number(1): {ADDR1}})) is True - assert bool(Number(1).evaluate({Number(2): {ADDR1, ADDR2}})) is False - - -def test_and(): - assert bool(And([Number(1)]).evaluate({Number(0): {ADDR1}})) is False - assert bool(And([Number(1)]).evaluate({Number(1): {ADDR1}})) is True - assert bool(And([Number(1), Number(2)]).evaluate({Number(0): {ADDR1}})) is False - assert bool(And([Number(1), Number(2)]).evaluate({Number(1): {ADDR1}})) is False - assert bool(And([Number(1), Number(2)]).evaluate({Number(2): {ADDR1}})) is False - assert bool(And([Number(1), Number(2)]).evaluate({Number(1): {ADDR1}, Number(2): {ADDR2}})) is True - - -def test_or(): - assert bool(Or([Number(1)]).evaluate({Number(0): {ADDR1}})) is False - assert bool(Or([Number(1)]).evaluate({Number(1): {ADDR1}})) is True - assert bool(Or([Number(1), Number(2)]).evaluate({Number(0): {ADDR1}})) is False - assert bool(Or([Number(1), Number(2)]).evaluate({Number(1): {ADDR1}})) is True - assert bool(Or([Number(1), Number(2)]).evaluate({Number(2): {ADDR1}})) is True - assert bool(Or([Number(1), Number(2)]).evaluate({Number(1): {ADDR1}, Number(2): {ADDR2}})) is True - - -def test_not(): - assert bool(Not(Number(1)).evaluate({Number(0): {ADDR1}})) is True - assert bool(Not(Number(1)).evaluate({Number(1): {ADDR1}})) is False - - -def test_some(): - assert bool(Some(0, [Number(1)]).evaluate({Number(0): {ADDR1}})) is True - assert bool(Some(1, [Number(1)]).evaluate({Number(0): {ADDR1}})) is False - - assert bool(Some(2, [Number(1), Number(2), Number(3)]).evaluate({Number(0): {ADDR1}})) is False - assert bool(Some(2, [Number(1), Number(2), Number(3)]).evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}})) is False - assert ( - bool( - Some(2, [Number(1), Number(2), Number(3)]).evaluate({ - Number(0): {ADDR1}, - Number(1): {ADDR1}, - Number(2): {ADDR1}, - }) - ) - is True - ) - assert ( - bool( - Some(2, [Number(1), Number(2), Number(3)]).evaluate({ - Number(0): {ADDR1}, - Number(1): {ADDR1}, - Number(2): {ADDR1}, - Number(3): {ADDR1}, - }) - ) - is True - ) - assert ( - bool( - Some(2, [Number(1), Number(2), Number(3)]).evaluate({ - Number(0): {ADDR1}, - Number(1): {ADDR1}, - Number(2): {ADDR1}, - Number(3): {ADDR1}, - Number(4): {ADDR1}, - }) - ) - is True - ) - - -def test_complex(): - assert True is bool( - Or([ - And([Number(1), Number(2)]), - Or([Number(3), Some(2, [Number(4), Number(5), Number(6)])]), - ]).evaluate({ - Number(5): {ADDR1}, - Number(6): {ADDR1}, - Number(7): {ADDR1}, - Number(8): {ADDR1}, - }) - ) - - assert False is bool( - Or([ - And([Number(1), Number(2)]), - Or([Number(3), Some(2, [Number(4), Number(5)])]), - ]).evaluate({ - Number(5): {ADDR1}, - Number(6): {ADDR1}, - Number(7): {ADDR1}, - Number(8): {ADDR1}, - }) - ) - - -def test_range(): - # unbounded range, but no matching feature - # since the lower bound is zero, and there are zero matches, ok - assert bool(Range(Number(1)).evaluate({Number(2): {}})) is True # type: ignore - - # unbounded range with matching feature should always match - assert bool(Range(Number(1)).evaluate({Number(1): {}})) is True # type: ignore - assert bool(Range(Number(1)).evaluate({Number(1): {ADDR1}})) is True - - # unbounded max - assert bool(Range(Number(1), min=1).evaluate({Number(1): {ADDR1}})) is True - assert bool(Range(Number(1), min=2).evaluate({Number(1): {ADDR1}})) is False - assert bool(Range(Number(1), min=2).evaluate({Number(1): {ADDR1, ADDR2}})) is True - - # unbounded min - assert bool(Range(Number(1), max=0).evaluate({Number(1): {ADDR1}})) is False - assert bool(Range(Number(1), max=1).evaluate({Number(1): {ADDR1}})) is True - assert bool(Range(Number(1), max=2).evaluate({Number(1): {ADDR1}})) is True - assert bool(Range(Number(1), max=2).evaluate({Number(1): {ADDR1, ADDR2}})) is True - assert bool(Range(Number(1), max=2).evaluate({Number(1): {ADDR1, ADDR2, ADDR3}})) is False - - # we can do an exact match by setting min==max - assert bool(Range(Number(1), min=1, max=1).evaluate({Number(1): {}})) is False # type: ignore - assert bool(Range(Number(1), min=1, max=1).evaluate({Number(1): {ADDR1}})) is True - assert bool(Range(Number(1), min=1, max=1).evaluate({Number(1): {ADDR1, ADDR2}})) is False - - # bounded range - assert bool(Range(Number(1), min=1, max=3).evaluate({Number(1): {}})) is False # type: ignore - assert bool(Range(Number(1), min=1, max=3).evaluate({Number(1): {ADDR1}})) is True - assert bool(Range(Number(1), min=1, max=3).evaluate({Number(1): {ADDR1, ADDR2}})) is True - assert bool(Range(Number(1), min=1, max=3).evaluate({Number(1): {ADDR1, ADDR2, ADDR3}})) is True - assert bool(Range(Number(1), min=1, max=3).evaluate({Number(1): {ADDR1, ADDR2, ADDR3, ADDR4}})) is False - - def test_short_circuit(): assert bool(Or([Number(1), Number(2)]).evaluate({Number(1): {ADDR1}})) is True diff --git a/tests/test_match.py b/tests/test_match.py index f55e54f8..a01b4d72 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -21,7 +21,6 @@ import capa.engine import capa.features.insn import capa.features.common from capa.rules import Scope -from capa.features.common import OS, OS_ANY, OS_WINDOWS, String, MatchedRule def match(rules, features, va, scope=Scope.FUNCTION): @@ -45,495 +44,6 @@ def match(rules, features, va, scope=Scope.FUNCTION): return features1, matches1 -def test_match_simple(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - namespace: testns1/testns2 - features: - - number: 100 - """) - r = capa.rules.Rule.from_yaml(rule) - - features, matches = match([r], {capa.features.insn.Number(100): {1, 2}}, 0x0) - assert "test rule" in matches - assert MatchedRule("test rule") in features - assert MatchedRule("testns1") in features - assert MatchedRule("testns1/testns2") in features - - -def test_match_range_exact(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - count(number(100)): 2 - """) - r = capa.rules.Rule.from_yaml(rule) - - # just enough matches - _, matches = match([r], {capa.features.insn.Number(100): {1, 2}}, 0x0) - assert "test rule" in matches - - # not enough matches - _, matches = match([r], {capa.features.insn.Number(100): {1}}, 0x0) - assert "test rule" not in matches - - # too many matches - _, matches = match([r], {capa.features.insn.Number(100): {1, 2, 3}}, 0x0) - assert "test rule" not in matches - - -def test_match_range_range(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - count(number(100)): (2, 3) - """) - r = capa.rules.Rule.from_yaml(rule) - - # just enough matches - _, matches = match([r], {capa.features.insn.Number(100): {1, 2}}, 0x0) - assert "test rule" in matches - - # enough matches - _, matches = match([r], {capa.features.insn.Number(100): {1, 2, 3}}, 0x0) - assert "test rule" in matches - - # not enough matches - _, matches = match([r], {capa.features.insn.Number(100): {1}}, 0x0) - assert "test rule" not in matches - - # too many matches - _, matches = match([r], {capa.features.insn.Number(100): {1, 2, 3, 4}}, 0x0) - assert "test rule" not in matches - - -def test_match_range_exact_zero(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - count(number(100)): 0 - - # we can't have `count(foo): 0` at the top level, - # since we don't support top level NOT statements. - # so we have this additional trivial feature. - - mnemonic: mov - - """) - r = capa.rules.Rule.from_yaml(rule) - - # feature isn't indexed - good. - _, matches = match([r], {capa.features.insn.Mnemonic("mov"): {}}, 0x0) - assert "test rule" in matches - - # feature is indexed, but no matches. - # i don't think we should ever really have this case, but good to check anyways. - _, matches = match([r], {capa.features.insn.Number(100): {}, capa.features.insn.Mnemonic("mov"): {}}, 0x0) - assert "test rule" in matches - - # too many matches - _, matches = match([r], {capa.features.insn.Number(100): {1}, capa.features.insn.Mnemonic("mov"): {1}}, 0x0) - assert "test rule" not in matches - - -def test_match_range_with_zero(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - count(number(100)): (0, 1) - - # we can't have `count(foo): 0` at the top level, - # since we don't support top level NOT statements. - # so we have this additional trivial feature. - - mnemonic: mov - """) - r = capa.rules.Rule.from_yaml(rule) - - # ok - _, matches = match([r], {capa.features.insn.Mnemonic("mov"): {}}, 0x0) - assert "test rule" in matches - _, matches = match([r], {capa.features.insn.Number(100): {}, capa.features.insn.Mnemonic("mov"): {}}, 0x0) - assert "test rule" in matches - _, matches = match([r], {capa.features.insn.Number(100): {1}, capa.features.insn.Mnemonic("mov"): {1}}, 0x0) - assert "test rule" in matches - - # too many matches - _, matches = match([r], {capa.features.insn.Number(100): {1, 2}, capa.features.insn.Mnemonic("mov"): {1, 2}}, 0x0) - assert "test rule" not in matches - - -def test_match_adds_matched_rule_feature(): - """show that using `match` adds a feature for matched rules.""" - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - number: 100 - """) - r = capa.rules.Rule.from_yaml(rule) - features, _ = match([r], {capa.features.insn.Number(100): {1}}, 0x0) - assert capa.features.common.MatchedRule("test rule") in features - - -def test_match_matched_rules(): - """show that using `match` adds a feature for matched rules.""" - rules = [ - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule1 - scopes: - static: function - dynamic: process - features: - - number: 100 - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule2 - scopes: - static: function - dynamic: process - features: - - match: test rule1 - """) - ), - ] - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.insn.Number(100): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule1") in features - assert capa.features.common.MatchedRule("test rule2") in features - - # the ordering of the rules must not matter, - # the engine should match rules in an appropriate order. - features, _ = match( - capa.rules.topologically_order_rules(list(reversed(rules))), - {capa.features.insn.Number(100): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule1") in features - assert capa.features.common.MatchedRule("test rule2") in features - - -def test_match_namespace(): - rules = [ - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: CreateFile API - scopes: - static: function - dynamic: process - namespace: file/create/CreateFile - features: - - api: CreateFile - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: WriteFile API - scopes: - static: function - dynamic: process - namespace: file/write - features: - - api: WriteFile - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: file-create - scopes: - static: function - dynamic: process - features: - - match: file/create - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: filesystem-any - scopes: - static: function - dynamic: process - features: - - match: file - """) - ), - ] - - features, matches = match( - capa.rules.topologically_order_rules(rules), - {capa.features.insn.API("CreateFile"): {1}}, - 0x0, - ) - assert "CreateFile API" in matches - assert "file-create" in matches - assert "filesystem-any" in matches - assert capa.features.common.MatchedRule("file") in features - assert capa.features.common.MatchedRule("file/create") in features - assert capa.features.common.MatchedRule("file/create/CreateFile") in features - - features, matches = match( - capa.rules.topologically_order_rules(rules), - {capa.features.insn.API("WriteFile"): {1}}, - 0x0, - ) - assert "WriteFile API" in matches - assert "file-create" not in matches - assert "filesystem-any" in matches - - -def test_match_substring(): - rules = [ - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - substring: abc - """) - ), - ] - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("aaaa"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") not in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("abc"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("111abc222"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("111abc"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("abc222"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - -def test_match_regex(): - rules = [ - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - string: /.*bbbb.*/ - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: rule with implied wildcards - scopes: - static: function - dynamic: process - features: - - and: - - string: /bbbb/ - """) - ), - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: rule with anchor - scopes: - static: function - dynamic: process - features: - - and: - - string: /^bbbb/ - """) - ), - ] - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.insn.Number(100): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") not in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("aaaa"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") not in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("aBBBBa"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") not in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("abbbba"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - assert capa.features.common.MatchedRule("rule with implied wildcards") in features - assert capa.features.common.MatchedRule("rule with anchor") not in features - - -def test_match_regex_ignorecase(): - rules = [ - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - string: /.*bbbb.*/i - """) - ), - ] - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("aBBBBa"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - -def test_match_regex_complex(): - rules = [ - capa.rules.Rule.from_yaml( - textwrap.dedent(r""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - or: - - string: /.*HARDWARE\\Key\\key with spaces\\.*/i - """) - ), - ] - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String(r"Hardware\Key\key with spaces\some value"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - -def test_match_regex_values_always_string(): - rules = [ - capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - or: - - string: /123/ - - string: /0x123/ - """) - ), - ] - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("123"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - features, _ = match( - capa.rules.topologically_order_rules(rules), - {capa.features.common.String("0x123"): {1}}, - 0x0, - ) - assert capa.features.common.MatchedRule("test rule") in features - - @pytest.mark.parametrize( "pattern", [ @@ -567,27 +77,6 @@ def test_match_only_not(): assert "test rule" in matches -def test_match_not(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - namespace: testns1/testns2 - features: - - and: - - mnemonic: mov - - not: - - number: 99 - """) - r = capa.rules.Rule.from_yaml(rule) - - _, matches = match([r], {capa.features.insn.Number(100): {1, 2}, capa.features.insn.Mnemonic("mov"): {1, 2}}, 0x0) - assert "test rule" in matches - - @pytest.mark.xfail(reason="can't have nested NOT") def test_match_not_not(): rule = textwrap.dedent(""" @@ -609,155 +98,6 @@ def test_match_not_not(): assert "test rule" in matches -def test_match_operand_number(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - operand[0].number: 0x10 - """) - r = capa.rules.Rule.from_yaml(rule) - - assert capa.features.insn.OperandNumber(0, 0x10) in {capa.features.insn.OperandNumber(0, 0x10)} - - _, matches = match([r], {capa.features.insn.OperandNumber(0, 0x10): {1, 2}}, 0x0) - assert "test rule" in matches - - # mismatching index - _, matches = match([r], {capa.features.insn.OperandNumber(1, 0x10): {1, 2}}, 0x0) - assert "test rule" not in matches - - # mismatching value - _, matches = match([r], {capa.features.insn.OperandNumber(0, 0x11): {1, 2}}, 0x0) - assert "test rule" not in matches - - -def test_match_operand_offset(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - operand[0].offset: 0x10 - """) - r = capa.rules.Rule.from_yaml(rule) - - assert capa.features.insn.OperandOffset(0, 0x10) in {capa.features.insn.OperandOffset(0, 0x10)} - - _, matches = match([r], {capa.features.insn.OperandOffset(0, 0x10): {1, 2}}, 0x0) - assert "test rule" in matches - - # mismatching index - _, matches = match([r], {capa.features.insn.OperandOffset(1, 0x10): {1, 2}}, 0x0) - assert "test rule" not in matches - - # mismatching value - _, matches = match([r], {capa.features.insn.OperandOffset(0, 0x11): {1, 2}}, 0x0) - assert "test rule" not in matches - - -def test_match_property_access(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - property/read: System.IO.FileInfo::Length - """) - r = capa.rules.Rule.from_yaml(rule) - - assert capa.features.insn.Property("System.IO.FileInfo::Length", capa.features.common.FeatureAccess.READ) in { - capa.features.insn.Property("System.IO.FileInfo::Length", capa.features.common.FeatureAccess.READ) - } - - _, matches = match( - [r], - {capa.features.insn.Property("System.IO.FileInfo::Length", capa.features.common.FeatureAccess.READ): {1, 2}}, - 0x0, - ) - assert "test rule" in matches - - # mismatching access - _, matches = match( - [r], - {capa.features.insn.Property("System.IO.FileInfo::Length", capa.features.common.FeatureAccess.WRITE): {1, 2}}, - 0x0, - ) - assert "test rule" not in matches - - # mismatching value - _, matches = match( - [r], - {capa.features.insn.Property("System.IO.FileInfo::Size", capa.features.common.FeatureAccess.READ): {1, 2}}, - 0x0, - ) - assert "test rule" not in matches - - -def test_match_os_any(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - or: - - and: - - or: - - os: windows - - os: linux - - os: macos - - string: "Hello world" - - and: - - os: any - - string: "Goodbye world" - """) - r = capa.rules.Rule.from_yaml(rule) - - _, matches = match( - [r], - {OS(OS_ANY): {1}, String("Hello world"): {1}}, - 0x0, - ) - assert "test rule" in matches - - _, matches = match( - [r], - {OS(OS_WINDOWS): {1}, String("Hello world"): {1}}, - 0x0, - ) - assert "test rule" in matches - - _, matches = match( - [r], - {OS(OS_ANY): {1}, String("Goodbye world"): {1}}, - 0x0, - ) - assert "test rule" in matches - - _, matches = match( - [r], - {OS(OS_WINDOWS): {1}, String("Goodbye world"): {1}}, - 0x0, - ) - assert "test rule" in matches - - # this test demonstrates the behavior of unstable features that may change before the next major release. def test_index_features_and_unstable(): rule = textwrap.dedent(""" diff --git a/tests/test_match_fixtures.py b/tests/test_match_fixtures.py new file mode 100644 index 00000000..23f5c284 --- /dev/null +++ b/tests/test_match_fixtures.py @@ -0,0 +1,711 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import re +from typing import Any, Mapping, Iterable +from pathlib import Path +from dataclasses import dataclass + +import yaml +import pytest + +import capa.rules +import capa.features.file +import capa.features.insn +import capa.features.common +import capa.capabilities.common +import capa.features.basicblock +import capa.capabilities.dynamic +import capa.features.extractors.null +from capa.features.common import Feature +from capa.features.address import ( + NO_ADDRESS, + Address, + ThreadAddress, + DNTokenAddress, + ProcessAddress, + FileOffsetAddress, + DynamicCallAddress, + DNTokenOffsetAddress, + AbsoluteVirtualAddress, + RelativeVirtualAddress, +) +from capa.features.extractors.base_extractor import SampleHashes, FeatureExtractor + +DUMMY_SAMPLE_HASHES = SampleHashes.from_bytes(b"") +PROCESS_HEADER = re.compile(r"^(?P.+) \((?:ppid=(?P\d+), )?pid=(?P\d+)\)$") + + +@dataclass(frozen=True) +class MatchFixture: + path: Path + index: int + name: str + description: str + flavor: str + ruleset: capa.rules.RuleSet + extractor: FeatureExtractor + expected_matches: dict[str, list[Address]] + span_size: int | None + + +class StaticFeatureParser: + def __init__(self, base_address: Address): + self.base_address = base_address + self.global_features: list[Feature] = [] + self.file_features: list[tuple[Address, Feature]] = [] + self.functions: dict[Address, capa.features.extractors.null.FunctionFeatures] = {} + self.current_function: Address | None = None + self.current_basic_block: Address | None = None + + def parse(self, source: Any) -> capa.features.extractors.null.NullStaticFeatureExtractor: + for line in _iter_feature_lines(source): + self.consume(line) + + return capa.features.extractors.null.NullStaticFeatureExtractor( + base_address=self.base_address, + sample_hashes=DUMMY_SAMPLE_HASHES, + global_features=self.global_features, + file_features=self.file_features, + functions=self.functions, + ) + + def consume(self, line: str) -> None: + if line.startswith("global:"): + self.consume_global(line) + elif line.startswith("file:"): + self.consume_file(line) + elif line.startswith("func:"): + self.consume_function(line) + elif line.startswith("bb:"): + self.consume_basic_block(line) + elif line.startswith("insn:"): + self.consume_instruction(line) + else: + raise ValueError(f"unsupported static feature line: {line}") + + def consume_global(self, line: str) -> None: + rest = _strip_prefix(line, "global:") + if rest.startswith("global: "): + rest = rest[len("global: ") :] + self.global_features.append(_parse_feature(rest)) + + def consume_file(self, line: str) -> None: + addr_text, feature_text, target_text = _split_feature_line(_strip_prefix(line, "file:")) + if target_text is not None: + raise ValueError("file feature lines do not support relocated addresses") + self.file_features.append((_parse_static_address(addr_text), _parse_feature(feature_text))) + + def consume_function(self, line: str) -> None: + rest = _strip_prefix(line, "func:") + if ": " not in rest: + function_address = _parse_static_address(rest) + self.ensure_function(function_address) + self.current_function = function_address + self.current_basic_block = None + return + + addr_text, feature_text, target_text = _split_feature_line(rest) + function_address = _parse_static_address(addr_text) + feature_address = _parse_static_address(target_text) if target_text is not None else function_address + self.ensure_function(function_address).features.append((feature_address, _parse_feature(feature_text))) + self.current_function = function_address + self.current_basic_block = None + + def consume_basic_block(self, line: str) -> None: + if self.current_function is None: + raise ValueError(f"basic block line without current function: {line}") + + addr_text, feature_text, target_text = _split_feature_line(_strip_prefix(line, "bb:")) + basic_block_address = _parse_static_address(addr_text) + feature_address = _parse_static_address(target_text) if target_text is not None else basic_block_address + self.ensure_basic_block(self.current_function, basic_block_address).features.append(( + feature_address, + _parse_feature(feature_text), + )) + self.current_basic_block = basic_block_address + + def consume_instruction(self, line: str) -> None: + if self.current_function is None or self.current_basic_block is None: + raise ValueError(f"instruction line without current basic block: {line}") + + addr_text, feature_text, target_text = _split_feature_line(_strip_prefix(line, "insn:")) + instruction_address = _parse_static_address(addr_text) + + feature_address = _parse_static_address(target_text) if target_text is not None else instruction_address + basic_block = self.ensure_basic_block(self.current_function, self.current_basic_block) + instruction = basic_block.instructions.get(instruction_address) + if instruction is None: + instruction = capa.features.extractors.null.InstructionFeatures(features=[]) + basic_block.instructions[instruction_address] = instruction + instruction.features.append((feature_address, _parse_feature(feature_text))) + + def ensure_function(self, address: Address) -> capa.features.extractors.null.FunctionFeatures: + function = self.functions.get(address) + if function is None: + function = capa.features.extractors.null.FunctionFeatures(features=[], basic_blocks={}) + self.functions[address] = function + return function + + def ensure_basic_block( + self, function_address: Address, basic_block_address: Address + ) -> capa.features.extractors.null.BasicBlockFeatures: + function = self.ensure_function(function_address) + basic_block = function.basic_blocks.get(basic_block_address) + if basic_block is None: + basic_block = capa.features.extractors.null.BasicBlockFeatures(features=[], instructions={}) + function.basic_blocks[basic_block_address] = basic_block + return basic_block + + +class DynamicFeatureParser: + def __init__(self): + self.global_features: list[Feature] = [] + self.file_features: list[tuple[Address, Feature]] = [] + self.processes: dict[Address, capa.features.extractors.null.ProcessFeatures] = {} + self.calls_by_id: dict[int, DynamicCallAddress] = {} + self.current_process: ProcessAddress | None = None + self.current_thread: ThreadAddress | None = None + + def parse(self, source: Any) -> capa.features.extractors.null.NullDynamicFeatureExtractor: + for line in _iter_feature_lines(source): + self.consume(line) + + return capa.features.extractors.null.NullDynamicFeatureExtractor( + base_address=NO_ADDRESS, + sample_hashes=DUMMY_SAMPLE_HASHES, + global_features=self.global_features, + file_features=self.file_features, + processes=self.processes, + ) + + def consume(self, line: str) -> None: + if line.startswith("global:"): + self.consume_global(line) + elif line.startswith("file:"): + self.consume_file(line) + elif line.startswith("proc:"): + self.consume_process(line) + elif line.startswith("thread:"): + self.consume_thread(line) + elif line.startswith("call:"): + self.consume_call(line) + else: + raise ValueError(f"unsupported dynamic feature line: {line}") + + def consume_global(self, line: str) -> None: + rest = _strip_prefix(line, "global:") + if rest.startswith("global: "): + rest = rest[len("global: ") :] + self.global_features.append(_parse_feature(rest)) + + def consume_file(self, line: str) -> None: + addr_text, feature_text, target_text = _split_feature_line(_strip_prefix(line, "file:")) + if target_text is not None: + raise ValueError("file feature lines do not support relocated addresses") + self.file_features.append((_parse_address(addr_text), _parse_feature(feature_text))) + + def consume_process(self, line: str) -> None: + rest = _strip_prefix(line, "proc:") + header = PROCESS_HEADER.fullmatch(rest) + if header is not None: + ppid = header.group("ppid") + process_address = ProcessAddress(ppid=int(ppid) if ppid is not None else 0, pid=int(header.group("pid"))) + self.ensure_process(process_address, header.group("name")) + self.current_process = process_address + self.current_thread = None + return + + if self.current_process is None: + raise ValueError(f"process feature line without current process: {line}") + + name, feature_text, target_text = _split_feature_line(rest) + process = self.ensure_process(self.current_process) + if process.name != name: + raise ValueError(f"process feature line does not match current process: {line}") + feature_address = _parse_address(target_text) if target_text is not None else self.current_process + process.features.append((feature_address, _parse_feature(feature_text))) + + def consume_thread(self, line: str) -> None: + if self.current_process is None: + raise ValueError(f"thread line without current process: {line}") + + rest = _strip_prefix(line, "thread:") + if ": " not in rest: + thread_address = ThreadAddress(process=self.current_process, tid=int(rest, 0)) + self.ensure_thread(thread_address) + self.current_thread = thread_address + return + + tid_text, feature_text, target_text = _split_feature_line(rest) + thread_address = ThreadAddress(process=self.current_process, tid=int(tid_text, 0)) + thread = self.ensure_thread(thread_address) + feature_address = _parse_address(target_text) if target_text is not None else thread_address + thread.features.append((feature_address, _parse_feature(feature_text))) + self.current_thread = thread_address + + def consume_call(self, line: str) -> None: + if self.current_thread is None: + raise ValueError(f"call line without current thread: {line}") + + call_id_text, feature_text, target_text = _split_feature_line(_strip_prefix(line, "call:")) + call_address = DynamicCallAddress(thread=self.current_thread, id=int(call_id_text, 0)) + call = self.ensure_call(call_address) + feature_address = _parse_address(target_text) if target_text is not None else call_address + call.features.append((feature_address, _parse_feature(feature_text))) + + def ensure_process( + self, address: ProcessAddress, name: str | None = None + ) -> capa.features.extractors.null.ProcessFeatures: + process = self.processes.get(address) + if process is None: + process = capa.features.extractors.null.ProcessFeatures( + name=name or f"process-{address.pid}", + features=[], + threads={}, + ) + self.processes[address] = process + elif name is not None: + process.name = name + return process + + def ensure_thread(self, address: ThreadAddress) -> capa.features.extractors.null.ThreadFeatures: + process = self.ensure_process(address.process) + thread = process.threads.get(address) + if thread is None: + thread = capa.features.extractors.null.ThreadFeatures(features=[], calls={}) + process.threads[address] = thread + return thread + + def ensure_call(self, address: DynamicCallAddress) -> capa.features.extractors.null.CallFeatures: + existing = self.calls_by_id.get(address.id) + if existing is not None and existing != address: + raise ValueError(f"dynamic fixture call IDs must be unique within a test: {address.id}") + + self.calls_by_id[address.id] = address + + thread = self.ensure_thread(address.thread) + call = thread.calls.get(address) + if call is None: + call = capa.features.extractors.null.CallFeatures(name=f"call-{address.id}", features=[]) + thread.calls[address] = call + return call + + +def load_fixtures(path: Path) -> list[MatchFixture]: + doc = yaml.safe_load(path.read_text()) + fixture_docs = _get_fixture_docs(path, doc) + fixtures: list[MatchFixture] = [] + + for index, fixture_doc in enumerate(fixture_docs, start=1): + flavor = _get_fixture_flavor(path, fixture_doc) + span_size = _load_span_size(fixture_doc) + + extractor: FeatureExtractor + if flavor == "static": + static_parser = StaticFeatureParser(_parse_static_address(fixture_doc.get("base address", 0))) + extractor = static_parser.parse(fixture_doc.get("features", "")) + expected_matches = _load_expected_matches(fixture_doc, flavor) + elif flavor == "dynamic": + dynamic_parser = DynamicFeatureParser() + extractor = dynamic_parser.parse(fixture_doc.get("features", "")) + expected_matches = _load_expected_matches( + fixture_doc, + flavor, + dynamic_parser=dynamic_parser, + ) + else: + raise ValueError(f"unsupported fixture flavor: {flavor}") + + ruleset = _load_ruleset(path, fixture_doc, flavor) + + fixtures.append( + MatchFixture( + path=path, + index=index, + name=str(fixture_doc.get("name", f"{path.stem}-{index}")), + description=str(fixture_doc.get("description", "")), + flavor=flavor, + ruleset=ruleset, + extractor=extractor, + expected_matches=expected_matches, + span_size=span_size, + ) + ) + + return fixtures + + +def render_matches(fixture: MatchFixture, matches: Mapping[str, Any]) -> dict[str, list[Address]]: + return { + rule_name: [address for address, _ in results] + for rule_name, results in matches.items() + if rule_name in fixture.ruleset and not fixture.ruleset[rule_name].is_subscope_rule() + } + + +def _get_fixture_docs(path: Path, doc: Any) -> list[dict[str, Any]]: + if isinstance(doc, list): + fixture_docs = doc + elif isinstance(doc, dict) and isinstance(doc.get("tests"), list): + fixture_docs = doc["tests"] + elif isinstance(doc, dict): + fixture_docs = [doc] + else: + raise ValueError(f"fixture file must contain a mapping or list: {path}") + + for fixture_doc in fixture_docs: + if not isinstance(fixture_doc, dict): + raise ValueError(f"fixture test must be a mapping: {path}") + + return fixture_docs + + +def _get_fixture_flavor(path: Path, doc: dict[str, Any]) -> str: + explicit = doc.get("flavor") + inferred = next( + (part for part in reversed(path.parts) if part in {"static", "dynamic"}), + None, + ) + + if explicit is None: + if inferred is None: + raise ValueError(f"fixture flavor could not be inferred from path: {path}") + return inferred + + if not isinstance(explicit, str): + raise ValueError("fixture flavor must be a string") + + if inferred is not None and explicit != inferred: + raise ValueError(f"fixture flavor {explicit!r} does not match file location {inferred!r}: {path}") + + return explicit + + +def _normalize_rule_doc(rule_doc: dict[str, Any], flavor: str) -> dict[str, Any]: + if "meta" not in rule_doc: + meta: dict[str, Any] = {} + for key in ("name", "namespace", "description", "scopes", "authors", "att&ck", "mbc", "lib"): + if key in rule_doc: + meta[key] = rule_doc.pop(key) + rule_doc["meta"] = meta + meta = rule_doc["meta"] + + if not isinstance(meta, dict): + raise ValueError("rule meta must be a mapping") + + scopes = meta.setdefault("scopes", {}) + if not isinstance(scopes, dict): + raise ValueError("rule scopes must be a mapping") + + if flavor == "static": + scopes.setdefault("dynamic", "unsupported") + elif flavor == "dynamic": + scopes.setdefault("static", "unsupported") + + return rule_doc + + +def _load_ruleset(path: Path, doc: dict[str, Any], flavor: str) -> capa.rules.RuleSet: + rules: list[capa.rules.Rule] = [] + for rule_doc in doc.get("rules", []): + if not isinstance(rule_doc, dict): + raise ValueError(f"rule must be a mapping: {path}") + wrapped = {"rule": _normalize_rule_doc(rule_doc, flavor)} + definition = yaml.safe_dump(wrapped, sort_keys=False) + rules.append(capa.rules.Rule.from_dict(wrapped, definition)) + return capa.rules.RuleSet(rules) + + +def _load_expected_matches( + doc: dict[str, Any], + flavor: str, + dynamic_parser: DynamicFeatureParser | None = None, +) -> dict[str, list[Address]]: + expect = doc.get("expect", {}) + if not isinstance(expect, dict): + raise ValueError("fixture expect must be a mapping") + + matches = expect.get("matches", {}) + if not isinstance(matches, dict): + raise ValueError("fixture expect.matches must be a mapping") + + return { + rule_name: [_parse_expected_address(spec, flavor, dynamic_parser) for spec in locations] + for rule_name, locations in matches.items() + } + + +def _parse_expected_address( + spec: Any, + flavor: str, + dynamic_parser: DynamicFeatureParser | None = None, +) -> Address: + if flavor == "dynamic" and dynamic_parser is not None: + if isinstance(spec, int) and spec in dynamic_parser.calls_by_id: + return dynamic_parser.calls_by_id[spec] + + if isinstance(spec, str): + call_id = re.fullmatch(r"call\((\d+)\)", spec) + if call_id is not None: + call_address = dynamic_parser.calls_by_id.get(int(call_id.group(1))) + if call_address is None: + raise ValueError(f"unknown dynamic fixture call ID: {spec}") + return call_address + + return _parse_address(spec) + + +def _load_span_size(doc: dict[str, Any]) -> int | None: + options = doc.get("options", {}) + if not isinstance(options, dict): + raise ValueError("fixture options must be a mapping") + + span_size = options.get("span size") + if span_size is None: + return None + if not isinstance(span_size, int): + raise ValueError("fixture options.span size must be an integer") + return span_size + + +def _iter_feature_lines(source: Any) -> Iterable[str]: + if isinstance(source, str): + lines = source.splitlines() + elif isinstance(source, list): + lines = source + else: + raise ValueError("fixture features must be a block string or list of strings") + + for line in lines: + if not isinstance(line, str): + raise ValueError("fixture feature lines must be strings") + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + yield stripped + + +def _split_feature_line(text: str) -> tuple[str, str, str | None]: + body, target = _split_target(text) + scope_text, separator, feature_text = body.partition(": ") + if not separator: + raise ValueError(f"expected ': ': {text}") + return scope_text, feature_text, target + + +def _split_target(text: str) -> tuple[str, str | None]: + if " -> " not in text: + return text, None + return text.rsplit(" -> ", 1) # type: ignore[return-value] # rsplit with maxsplit=1 always returns 2 elements + + +def _parse_feature(text: str) -> Feature: + text = text.strip() + if text == "basic block": + return capa.features.basicblock.BasicBlock() + + operand_number = re.fullmatch(r"operand\[(\d+)\]\.number\((.*)\)", text) + if operand_number: + return capa.features.insn.OperandNumber( + int(operand_number.group(1)), + _parse_number_literal(operand_number.group(2)), + ) + + operand_offset = re.fullmatch(r"operand\[(\d+)\]\.offset\((.*)\)", text) + if operand_offset: + return capa.features.insn.OperandOffset( + int(operand_offset.group(1)), + _parse_int_literal(operand_offset.group(2)), + ) + + property_ = re.fullmatch(r"property(?:/(read|write))?\((.*)\)", text) + if property_: + return capa.features.insn.Property( + _strip_quotes(property_.group(2).strip()), + access=property_.group(1), + ) + + feature = re.fullmatch(r"([a-z][a-z0-9\- ]*)\((.*)\)", text) + if feature is None: + raise ValueError(f"unsupported feature syntax: {text}") + + name = feature.group(1) + value = _strip_quotes(feature.group(2).strip()) + + if name == "api": + return capa.features.insn.API(value) + if name == "arch": + return capa.features.common.Arch(value) + if name == "bytes": + return capa.features.common.Bytes(bytes.fromhex(value.replace(" ", ""))) + if name == "characteristic": + return capa.features.common.Characteristic(value) + if name == "class": + return capa.features.common.Class(value) + if name == "export": + return capa.features.file.Export(value) + if name == "format": + return capa.features.common.Format(value) + if name in ("function-name", "function name"): + return capa.features.file.FunctionName(value) + if name == "import": + return capa.features.file.Import(value) + if name == "match": + return capa.features.common.MatchedRule(value) + if name == "mnemonic": + return capa.features.insn.Mnemonic(value) + if name == "namespace": + return capa.features.common.Namespace(value) + if name == "number": + return capa.features.insn.Number(_parse_number_literal(value)) + if name == "offset": + return capa.features.insn.Offset(_parse_int_literal(value)) + if name == "os": + return capa.features.common.OS(value) + if name == "section": + return capa.features.file.Section(value) + if name == "string": + return capa.features.common.String(value) + if name == "substring": + return capa.features.common.Substring(value) + + raise ValueError(f"unsupported feature type: {name}") + + +def _parse_number_literal(value: str) -> int | float: + value = value.strip() + if _looks_like_hex_literal(value): + return int(value, 0) + if any(character in value for character in ".eE"): + return float(value) + return int(value, 0) + + +def _looks_like_hex_literal(value: str) -> bool: + return value.lstrip("+-").lower().startswith("0x") + + +def _parse_int_literal(value: str) -> int: + return int(value, 0) + + +def _parse_static_address(spec: Any) -> Address: + address = _parse_address(spec) + if isinstance(address, (ProcessAddress, ThreadAddress, DynamicCallAddress)): + raise ValueError(f"expected a static address, got {spec!r}") + return address + + +def _parse_address(spec: Any) -> Address: + if spec is None: + return NO_ADDRESS + + if isinstance(spec, int): + return AbsoluteVirtualAddress(spec) + + if not isinstance(spec, str): + raise ValueError(f"unsupported address: {spec!r}") + + if spec in {"global", "no address"}: + return NO_ADDRESS + if spec.startswith("base address+"): + return RelativeVirtualAddress(_coerce_int(spec[len("base address+") :])) + if spec.startswith("file+"): + return FileOffsetAddress(_coerce_int(spec[len("file+") :])) + if token_offset := re.fullmatch(r"token\((.+)\)\+(.+)", spec): + return DNTokenOffsetAddress(_coerce_int(token_offset.group(1)), _coerce_int(token_offset.group(2))) + if token := re.fullmatch(r"token\((.+)\)", spec): + return DNTokenAddress(_coerce_int(token.group(1))) + if process := re.fullmatch(r"process\{ppid:(\d+),pid:(\d+)\}", spec): + return ProcessAddress(ppid=int(process.group(1)), pid=int(process.group(2))) + if process := re.fullmatch(r"process\{pid:(\d+)\}", spec): + return ProcessAddress(pid=int(process.group(1))) + if thread := re.fullmatch(r"process\{ppid:(\d+),pid:(\d+),tid:(\d+)\}", spec): + return ThreadAddress( + process=ProcessAddress(ppid=int(thread.group(1)), pid=int(thread.group(2))), + tid=int(thread.group(3)), + ) + if thread := re.fullmatch(r"process\{pid:(\d+),tid:(\d+)\}", spec): + return ThreadAddress(process=ProcessAddress(pid=int(thread.group(1))), tid=int(thread.group(2))) + if call := re.fullmatch(r"process\{ppid:(\d+),pid:(\d+),tid:(\d+),call:(\d+)\}", spec): + return DynamicCallAddress( + thread=ThreadAddress( + process=ProcessAddress(ppid=int(call.group(1)), pid=int(call.group(2))), + tid=int(call.group(3)), + ), + id=int(call.group(4)), + ) + if call := re.fullmatch(r"process\{pid:(\d+),tid:(\d+),call:(\d+)\}", spec): + return DynamicCallAddress( + thread=ThreadAddress(process=ProcessAddress(pid=int(call.group(1))), tid=int(call.group(2))), + id=int(call.group(3)), + ) + return AbsoluteVirtualAddress(_coerce_int(spec)) + + +def _coerce_int(value: Any) -> int: + if isinstance(value, int): + return value + if isinstance(value, str): + return int(value, 0) + raise ValueError(f"expected integer value: {value!r}") + + +def _strip_prefix(text: str, prefix: str) -> str: + return text[len(prefix) :].strip() + + +def _strip_quotes(value: str) -> str: + if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}: + return value[1:-1] + return value + + +# --------------------------------------------------------------------------- +# Test collection and parametrization +# --------------------------------------------------------------------------- + +FIXTURE_DIR = Path(__file__).parent / "fixtures" / "matcher" +FIXTURE_PATHS = sorted(path for path in FIXTURE_DIR.rglob("*") if path.suffix in {".json", ".yml", ".yaml"}) +FIXTURES = [fixture for path in FIXTURE_PATHS for fixture in load_fixtures(path)] +FIXTURE_IDS = [f"{fixture.path.relative_to(FIXTURE_DIR)}[{fixture.index}]::{fixture.name}" for fixture in FIXTURES] + + +def _enable_paranoid_matching(patch: pytest.MonkeyPatch, ruleset: capa.rules.RuleSet) -> None: + original_match = ruleset.match + + def paranoid_match(scope, features, addr, paranoid=False): + return original_match(scope, features, addr, paranoid=True) + + patch.setattr(ruleset, "match", paranoid_match) + + +@pytest.mark.parametrize("fixture", FIXTURES, ids=FIXTURE_IDS) +def test_match_fixture(fixture: MatchFixture): + with pytest.MonkeyPatch.context() as patch: + if fixture.span_size is not None: + patch.setattr(capa.capabilities.dynamic, "SPAN_SIZE", fixture.span_size) + + _enable_paranoid_matching(patch, fixture.ruleset) + + capabilities = capa.capabilities.common.find_capabilities( + fixture.ruleset, + fixture.extractor, + disable_progress=True, + ) + + assert render_matches(fixture, capabilities.matches) == fixture.expected_matches diff --git a/tests/test_rules.py b/tests/test_rules.py index 6b1be70a..6dd1aff3 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -21,9 +21,8 @@ import capa.rules import capa.engine import capa.features.common import capa.features.address -from capa.engine import Or from capa.features.file import FunctionName -from capa.features.insn import API, Number, Offset, Property +from capa.features.insn import Number, Offset from capa.features.common import ( OS, OS_LINUX, @@ -36,7 +35,6 @@ from capa.features.common import ( Format, String, Substring, - FeatureAccess, ) ADDR1 = capa.features.address.AbsoluteVirtualAddress(0x401001) @@ -45,14 +43,6 @@ ADDR3 = capa.features.address.AbsoluteVirtualAddress(0x401003) ADDR4 = capa.features.address.AbsoluteVirtualAddress(0x401004) -def test_rule_ctor(): - r = capa.rules.Rule( - "test rule", capa.rules.Scopes(capa.rules.Scope.FUNCTION, capa.rules.Scope.FILE), Or([Number(1)]), {} - ) - assert bool(r.evaluate({Number(0): {ADDR1}})) is False - assert bool(r.evaluate({Number(1): {ADDR2}})) is True - - def test_scopes_from_dict(): scopes = capa.rules.Scopes.from_dict({"static": "function", "dynamic": "process"}) assert scopes.static == capa.rules.Scope.FUNCTION @@ -66,56 +56,6 @@ def test_scopes_from_dict(): assert isinstance(sub, SubScopes) -def test_rule_yaml(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - authors: - - user@domain.com - scopes: - static: function - dynamic: process - examples: - - foo1234 - - bar5678 - features: - - and: - - number: 1 - - number: 2 - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({Number(0): {ADDR1}})) is False - assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}})) is False - assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}})) is True - assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}, Number(3): {ADDR1}})) is True - - -def test_rule_yaml_complex(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - or: - - and: - - number: 1 - - number: 2 - - or: - - number: 3 - - 2 or more: - - number: 4 - - number: 5 - - number: 6 - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({Number(5): {ADDR1}, Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}})) is True - assert bool(r.evaluate({Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}})) is False - - def test_rule_descriptions(): rule = textwrap.dedent(""" rule: @@ -207,78 +147,6 @@ def test_get_rules_skips_empty_yaml(tmp_path): assert len(rules) == 1 -def test_rule_yaml_not(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - and: - - number: 1 - - not: - - number: 2 - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({Number(1): {ADDR1}})) is True - assert bool(r.evaluate({Number(1): {ADDR1}, Number(2): {ADDR1}})) is False - - -def test_rule_yaml_count(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - count(number(100)): 1 - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({Number(100): set()})) is False - assert bool(r.evaluate({Number(100): {ADDR1}})) is True - assert bool(r.evaluate({Number(100): {ADDR1, ADDR2}})) is False - - -def test_rule_yaml_count_range(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - count(number(100)): (1, 2) - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({Number(100): set()})) is False - assert bool(r.evaluate({Number(100): {ADDR1}})) is True - assert bool(r.evaluate({Number(100): {ADDR1, ADDR2}})) is True - assert bool(r.evaluate({Number(100): {ADDR1, ADDR2, ADDR3}})) is False - - -def test_rule_yaml_count_string(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - count(string(foo)): 2 - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({String("foo"): set()})) is False - assert bool(r.evaluate({String("foo"): {ADDR1}})) is False - assert bool(r.evaluate({String("foo"): {ADDR1, ADDR2}})) is True - assert bool(r.evaluate({String("foo"): {ADDR1, ADDR2, ADDR3}})) is False - - def test_invalid_rule_feature(): with pytest.raises(capa.rules.InvalidRule): capa.rules.Rule.from_yaml( @@ -852,50 +720,6 @@ def test_number_symbol(): assert (Number(0x100, description="symbol name") in children) is True -def test_count_number_symbol(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - or: - - count(number(2 = symbol name)): 1 - - count(number(0x100 = symbol name)): 2 or more - - count(number(0x11 = (FLAG_A | FLAG_B))): 2 or more - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({Number(2): set()})) is False - assert bool(r.evaluate({Number(2): {ADDR1}})) is True - assert bool(r.evaluate({Number(2): {ADDR1, ADDR2}})) is False - assert bool(r.evaluate({Number(0x100, description="symbol name"): {ADDR1}})) is False - assert bool(r.evaluate({Number(0x100, description="symbol name"): {ADDR1, ADDR2, ADDR3}})) is True - - -def test_count_api(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: thread - features: - - or: - - count(api(kernel32.CreateFileA)): 1 - - count(api(System.Convert::FromBase64String)): 1 - """) - r = capa.rules.Rule.from_yaml(rule) - # apis including their DLL names are not extracted anymore - assert bool(r.evaluate({API("kernel32.CreateFileA"): set()})) is False - assert bool(r.evaluate({API("kernel32.CreateFile"): set()})) is False - assert bool(r.evaluate({API("CreateFile"): {ADDR1}})) is False - assert bool(r.evaluate({API("CreateFileA"): {ADDR1}})) is True - assert bool(r.evaluate({API("System.Convert::FromBase64String"): {ADDR1}})) is True - - def test_invalid_number(): with pytest.raises(capa.rules.InvalidRule): _ = capa.rules.Rule.from_yaml( @@ -965,28 +789,6 @@ def test_offset_symbol(): assert (Offset(0x100, description="symbol name") in children) is True -def test_count_offset_symbol(): - rule = textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - or: - - count(offset(2 = symbol name)): 1 - - count(offset(0x100 = symbol name)): 2 or more - - count(offset(0x11 = (FLAG_A | FLAG_B))): 2 or more - """) - r = capa.rules.Rule.from_yaml(rule) - assert bool(r.evaluate({Offset(2): set()})) is False - assert bool(r.evaluate({Offset(2): {ADDR1}})) is True - assert bool(r.evaluate({Offset(2): {ADDR1, ADDR2}})) is False - assert bool(r.evaluate({Offset(0x100, description="symbol name"): {ADDR1}})) is False - assert bool(r.evaluate({Offset(0x100, description="symbol name"): {ADDR1, ADDR2, ADDR3}})) is True - - def test_invalid_offset(): with pytest.raises(capa.rules.InvalidRule): _ = capa.rules.Rule.from_yaml( @@ -1380,48 +1182,6 @@ def test_arch_features(): assert (Arch(ARCH_I386) not in children) is True -def test_property_access(): - r = capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - property/read: System.IO.FileInfo::Length - """) - ) - assert bool(r.evaluate({Property("System.IO.FileInfo::Length", access=FeatureAccess.READ): {ADDR1}})) is True - - assert bool(r.evaluate({Property("System.IO.FileInfo::Length"): {ADDR1}})) is False - assert bool(r.evaluate({Property("System.IO.FileInfo::Length", access=FeatureAccess.WRITE): {ADDR1}})) is False - - -def test_property_access_symbol(): - r = capa.rules.Rule.from_yaml( - textwrap.dedent(""" - rule: - meta: - name: test rule - scopes: - static: function - dynamic: process - features: - - property/read: System.IO.FileInfo::Length = some property - """) - ) - assert ( - bool( - r.evaluate({ - Property("System.IO.FileInfo::Length", access=FeatureAccess.READ, description="some property"): {ADDR1} - }) - ) - is True - ) - - def test_translate_com_features(): r = capa.rules.Rule.from_yaml( textwrap.dedent(""" From 430de8171131101ea18beb39b2d2fde77f38adb8 Mon Sep 17 00:00:00 2001 From: Capa Bot Date: Fri, 12 Jun 2026 13:10:21 +0000 Subject: [PATCH 18/26] Sync capa rules submodule --- rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules b/rules index e240160d..788a997c 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit e240160da4e38238afc8fd6bbdf47be7a454df2d +Subproject commit 788a997cfaeeb7ac5a68e562c009129fc1637ba0 From 1f317c1d1669917dd841d47f7f0a7ef926f05db2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:43:00 +0000 Subject: [PATCH 19/26] build(deps): bump msgpack from 1.1.2 to 1.2.0 in the vivisect group Bumps the vivisect group with 1 update: [msgpack](https://github.com/msgpack/msgpack-python). Updates `msgpack` from 1.1.2 to 1.2.0 - [Release notes](https://github.com/msgpack/msgpack-python/releases) - [Changelog](https://github.com/msgpack/msgpack-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/msgpack/msgpack-python/compare/v1.1.2...v1.2.0) --- updated-dependencies: - dependency-name: msgpack dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: vivisect ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cb0df34c..0ea294d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ ida-settings==3.4.1 intervaltree==3.2.1 markdown-it-py==4.2.0 mdurl==0.1.2 -msgpack==1.1.2 +msgpack==1.2.0 networkx==3.4.2 pefile==2024.8.26 pip==26.1 From 648d3a384f0041455ad31294739cdd24e7e78b21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:43:11 +0000 Subject: [PATCH 20/26] build(deps-dev): bump pytest from 9.0.3 to 9.1.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.3 to 9.1.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/9.0.3...9.1.0) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.1.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5eebe161..89c7dff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,7 +130,7 @@ dev = [ # These dependencies are not used in production environments # and should not conflict with other libraries/tooling. "pre-commit==4.6.0", - "pytest==9.0.3", + "pytest==9.1.0", "pytest-sugar==1.1.1", "pytest-instafail==0.5.0", "ruff==0.15.0", From 4af7b897f0a0fcd61ef1ccd2604b0aebc187ba22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:43:18 +0000 Subject: [PATCH 21/26] build(deps-dev): bump pyinstaller from 6.20.0 to 6.21.0 Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.20.0 to 6.21.0. - [Release notes](https://github.com/pyinstaller/pyinstaller/releases) - [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst) - [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.20.0...v6.21.0) --- updated-dependencies: - dependency-name: pyinstaller dependency-version: 6.21.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5eebe161..cb4df480 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,7 +151,7 @@ build = [ # we want all developer environments to be consistent. # These dependencies are not used in production environments # and should not conflict with other libraries/tooling. - "pyinstaller==6.20.0", + "pyinstaller==6.21.0", "setuptools==82.0.1", "build==1.5.0" ] From 50668119f9ac8069ba53233c9f0c5bf80057db71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:18:38 +0000 Subject: [PATCH 22/26] build(deps): bump form-data from 4.0.4 to 4.0.6 in /web/explorer Bumps [form-data](https://github.com/form-data/form-data) from 4.0.4 to 4.0.6. - [Release notes](https://github.com/form-data/form-data/releases) - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.4...v4.0.6) --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web/explorer/package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/explorer/package-lock.json b/web/explorer/package-lock.json index d95c7b6b..eec74912 100644 --- a/web/explorer/package-lock.json +++ b/web/explorer/package-lock.json @@ -2269,17 +2269,17 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -2486,9 +2486,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { From f28a150566be632c3b552ce2ff2d3fee24f03757 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:25:05 +0200 Subject: [PATCH 23/26] Merge pull request #3105 from mandiant/dependabot/npm_and_yarn/web/explorer/multi-cee5a6df2e build(deps): bump esbuild, @vitejs/plugin-vue, vite and vite-plugin-singlefile in /web/explorer --- web/explorer/package-lock.json | 1282 +++++++++++++------------------- web/explorer/package.json | 6 +- 2 files changed, 533 insertions(+), 755 deletions(-) diff --git a/web/explorer/package-lock.json b/web/explorer/package-lock.json index d95c7b6b..064eb7ee 100644 --- a/web/explorer/package-lock.json +++ b/web/explorer/package-lock.json @@ -20,15 +20,15 @@ }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", - "@vitejs/plugin-vue": "^5.2.3", + "@vitejs/plugin-vue": "^6.0.7", "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "eslint": "^8.57.0", "eslint-plugin-vue": "^9.23.0", "jsdom": "^24.1.0", "prettier": "^3.2.5", - "vite": "^6.4.2", - "vite-plugin-singlefile": "^2.2.0", + "vite": "^8.0.16", + "vite-plugin-singlefile": "^2.3.3", "vitest": "^4.1.0" } }, @@ -43,429 +43,38 @@ "node": ">=6.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", - "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", - "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", - "cpu": [ - "arm" - ], + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", - "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", - "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", - "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", - "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", - "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", - "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", - "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", - "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", - "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", - "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", - "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", - "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", - "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", - "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", - "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", - "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", - "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", - "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", - "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", - "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", - "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", - "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", - "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -618,6 +227,25 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -659,6 +287,16 @@ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", "dev": true }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -725,24 +363,10 @@ "@primeuix/styled": "^0.0.1" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -751,12 +375,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -765,12 +392,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -779,26 +409,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -807,12 +426,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -821,26 +443,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -849,12 +460,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], @@ -863,40 +477,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -905,54 +494,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -961,12 +511,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -975,12 +528,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -989,26 +545,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -1017,12 +562,34 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -1031,26 +598,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1059,21 +615,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.3", @@ -1088,6 +640,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1120,16 +683,19 @@ "dev": true }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", - "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", "dev": true, "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, @@ -1727,6 +1293,16 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1878,47 +1454,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", - "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.1", - "@esbuild/android-arm": "0.25.1", - "@esbuild/android-arm64": "0.25.1", - "@esbuild/android-x64": "0.25.1", - "@esbuild/darwin-arm64": "0.25.1", - "@esbuild/darwin-x64": "0.25.1", - "@esbuild/freebsd-arm64": "0.25.1", - "@esbuild/freebsd-x64": "0.25.1", - "@esbuild/linux-arm": "0.25.1", - "@esbuild/linux-arm64": "0.25.1", - "@esbuild/linux-ia32": "0.25.1", - "@esbuild/linux-loong64": "0.25.1", - "@esbuild/linux-mips64el": "0.25.1", - "@esbuild/linux-ppc64": "0.25.1", - "@esbuild/linux-riscv64": "0.25.1", - "@esbuild/linux-s390x": "0.25.1", - "@esbuild/linux-x64": "0.25.1", - "@esbuild/netbsd-arm64": "0.25.1", - "@esbuild/netbsd-x64": "0.25.1", - "@esbuild/openbsd-arm64": "0.25.1", - "@esbuild/openbsd-x64": "0.25.1", - "@esbuild/sunos-x64": "0.25.1", - "@esbuild/win32-arm64": "0.25.1", - "@esbuild/win32-ia32": "0.25.1", - "@esbuild/win32-x64": "0.25.1" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2826,6 +2361,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2947,9 +2743,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -3180,9 +2976,9 @@ "integrity": "sha512-FZR9QT60vtE1ocdSIfop+zDIJEoy1lejwOvAjTSy+AmE4GZ//rW1nnIXwCRv4o9ejfzWq++lQMu6FJf9G+NtFg==" }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -3199,7 +2995,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3387,49 +3183,38 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/rrweb-cssom": { @@ -3875,24 +3660,23 @@ "dev": true }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3901,14 +3685,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -3917,15 +3702,18 @@ "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, @@ -3950,9 +3738,9 @@ } }, "node_modules/vite-plugin-singlefile": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.2.0.tgz", - "integrity": "sha512-Ik1wXmJaGzeQtUeIV7JprDUqqy6DlLzXAY27Blei5peE4c9VJF+Kp9xWDJeuX0RJUZmFbIAuw1/RAh06A+Ql7w==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", + "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3962,21 +3750,11 @@ "node": ">18.0.0" }, "peerDependencies": { - "rollup": "^4.35.0", - "vite": "^5.4.11 || ^6.0.0" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" + "rollup": "^4.59.0", + "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "picomatch": { + "rollup": { "optional": true } } diff --git a/web/explorer/package.json b/web/explorer/package.json index da00c2d7..9f24011e 100644 --- a/web/explorer/package.json +++ b/web/explorer/package.json @@ -26,15 +26,15 @@ }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", - "@vitejs/plugin-vue": "^5.2.3", + "@vitejs/plugin-vue": "^6.0.7", "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "eslint": "^8.57.0", "eslint-plugin-vue": "^9.23.0", "jsdom": "^24.1.0", "prettier": "^3.2.5", - "vite": "^6.4.2", - "vite-plugin-singlefile": "^2.2.0", + "vite": "^8.0.16", + "vite-plugin-singlefile": "^2.3.3", "vitest": "^4.1.0" } } From 29575cac194a1dfcaafadc25b6ffbc35d5814336 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:29:20 +0200 Subject: [PATCH 24/26] Merge pull request #3110 from mandiant/dependabot/npm_and_yarn/web/explorer/js-yaml-4.2.0 build(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 in /web/explorer --- web/explorer/package-lock.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/web/explorer/package-lock.json b/web/explorer/package-lock.json index 07b62fe6..5e4ae01a 100644 --- a/web/explorer/package-lock.json +++ b/web/explorer/package-lock.json @@ -2260,10 +2260,20 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" From 45460ca91b96179f59cfbc3eb68fd237a3682e7b Mon Sep 17 00:00:00 2001 From: Capa Bot Date: Fri, 19 Jun 2026 21:19:59 +0000 Subject: [PATCH 25/26] Sync capa rules submodule --- rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules b/rules index 788a997c..aed45e25 160000 --- a/rules +++ b/rules @@ -1 +1 @@ -Subproject commit 788a997cfaeeb7ac5a68e562c009129fc1637ba0 +Subproject commit aed45e2571ebf7d2330e3daddbb5c472cc54966e From 3201e214834f6de9c2385ce9440655abf6ba2aa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:43:11 +0000 Subject: [PATCH 26/26] build(deps): bump bump-my-version from 1.3.0 to 1.4.1 Bumps [bump-my-version](https://github.com/callowayproject/bump-my-version) from 1.3.0 to 1.4.1. - [Release notes](https://github.com/callowayproject/bump-my-version/releases) - [Changelog](https://github.com/callowayproject/bump-my-version/blob/1.4.1/CHANGELOG.md) - [Commits](https://github.com/callowayproject/bump-my-version/compare/v1.3...1.4.1) --- updated-dependencies: - dependency-name: bump-my-version dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2c60a459..cc50267f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,7 @@ dev = [ "mypy==2.1.0", "mypy-protobuf==5.1.0", "PyGithub==2.9.0", - "bump-my-version==1.3.0", + "bump-my-version==1.4.1", # type stubs for mypy "types-colorama==0.4.15.11", "types-PyYAML==6.0.8", diff --git a/requirements.txt b/requirements.txt index 0ea294d3..a9568e7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,4 +46,4 @@ sortedcontainers==2.4.0 viv-utils==0.8.0 vivisect==1.3.2 msgspec==0.21.1 -bump-my-version==1.3.0 +bump-my-version==1.4.1