Merge pull request #1743 from volatilityfoundation/requirement_checks_github_action

Testing: Verify `VersionRequirement`s
This commit is contained in:
ikelos
2025-03-28 22:30:44 +00:00
committed by GitHub
31 changed files with 590 additions and 17 deletions
+25
View File
@@ -0,0 +1,25 @@
name: Check Volatility3 Version Requirements
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.8"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[test]
- name: Testing...
run: |
# Verify completeness of ConfigurableInterface requirements
python ./test/check_configurable_requirements.py
+370
View File
@@ -0,0 +1,370 @@
"""
This script performs syntax analysis on the volatility3 source tree through a combination of AST analysis and import-time introspection of classes.
The current checks it implements are:
1. Ensure that classes derived from `ConfigurableInterface` properly
declare all `VersionableInterface` classes that they make use of in their
`get_requirements()` classmethod.
:WARNING: a notable exception to this are classes defined within factory
functions. Because these classes are not created until the factory function
is called, they therefore do no exist at import time and cannot be checked
by this script. It is important to keep in mind during code review that
this is a best-effort check and does not make guarantees about the
completeness of declared requirements.
"""
import abc
import argparse
import ast
import importlib
import inspect
import logging
import pkgutil
import sys
import types
from typing import Any, Iterator, List, Optional, Tuple, Type, Union
from volatility3.framework import configuration, interfaces
from volatility3.framework.deprecation import PluginRenameClass
logging.basicConfig(format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
class NodeVisitor:
def visit(self, node):
"""Visit a node."""
method = "visit_" + node.__class__.__name__
visitor = getattr(self, method, self.generic_visit)
self.enter(node)
result = visitor(node)
self.leave(node)
return result
def enter(self, node):
"""Called when entering a node."""
method = "enter_" + node.__class__.__name__
visitor = getattr(self, method, self.generic_enter)
return visitor(node)
def leave(self, node):
"""Called when leaving a node."""
method = "leave_" + node.__class__.__name__
visitor = getattr(self, method, self.generic_leave)
return visitor(node)
def generic_visit(self, node):
"""Called if no explicit visitor function exists for a node."""
for _, value in ast.iter_fields(node):
if isinstance(value, list):
for item in value:
if isinstance(item, ast.AST):
self.visit(item)
elif isinstance(value, ast.AST):
self.visit(value)
def generic_enter(self, node):
"""Default enter behavior."""
def generic_leave(self, node):
"""Default leave behavior."""
class CodeViolation(metaclass=abc.ABCMeta):
def __init__(self, module: types.ModuleType, node: ast.AST) -> None:
self.module = module
self.node = node
def __str__(self):
return f"Issue in module {self.module.__name__}: line {self.node.lineno}, col {self.node.col_offset}"
class UnrequiredVersionableUsage(CodeViolation):
def __init__(
self,
module: types.ModuleType,
node: ast.AST,
consuming_class: str,
versionable_item_class: str,
) -> None:
super().__init__(module, node)
self.consuming_class = consuming_class
self.versionable_item_class = versionable_item_class
def __str__(self) -> str:
return (
super().__str__()
+ ": "
+ (
f"Found usage of {self.versionable_item_class} "
f"in class {self.consuming_class} that is not declared "
f"in {self.consuming_class}'s `get_requirements()` classmethod"
)
)
def is_versionable(var):
try:
return (
issubclass(var, interfaces.configuration.VersionableInterface)
and var is not interfaces.configuration.VersionableInterface
and not inspect.isabstract(var)
and not (hasattr(var, "hidden") and getattr(var, "hidden") is True)
)
except TypeError:
return False
def is_configurable(var):
try:
return issubclass(var, interfaces.configuration.ConfigurableInterface)
except TypeError:
return False
class ModuleVisitor(NodeVisitor):
def __init__(self, module: types.ModuleType) -> None:
self._module = module
self._scopes = []
self._violations = []
@property
def violations(self):
return self._violations
def enter_ClassDef(self, node: ast.ClassDef) -> Any:
logger.debug("Entering class %s", node.name)
clazz = None
try:
clazz = vars(self._module)[str(node.name)]
except KeyError:
logger.debug(
"Failed to get %s from module scope: (%s)",
node.name,
self._module.__name__,
)
if self._scopes:
try:
logger.debug(
"Attempting to get class %s from scope of %s",
node.name,
self._scopes[-1].__name__,
)
clazz = getattr(self._scopes[-1], node.name)
except AttributeError:
logger.debug(
"Class not found in scope of %s", self._scopes[-1].__name__
)
if clazz:
self._scopes.append(clazz)
if clazz and is_configurable(clazz):
logger.info("Checking configurable class %s", clazz.__name__)
visitor = ConfigurableClassVisitor(self._module, clazz)
visitor.visit(node)
self._violations += visitor.violations
self.generic_visit(node)
def leave_ClassDef(self, node: ast.ClassDef):
logger.debug("Leaving class %s", node.name)
try:
scoped_class = next(
scope for scope in self._scopes if scope.__name__ == node.name
)
self._scopes.remove(scoped_class)
except StopIteration:
logger.debug("%s not found in scope list", node.name)
class ConfigurableClassVisitor(NodeVisitor):
def __init__(
self,
module: types.ModuleType,
clazz: Optional[Type[interfaces.configuration.ConfigurableInterface]],
) -> None:
self._module = module
self._current_object = None
self._clazz = clazz
self._seen = set()
self._violations: List[CodeViolation] = []
@property
def versioned_classes(self):
return (
[
req._component
for req in self._clazz.get_requirements()
if isinstance(req, configuration.requirements.VersionRequirement)
]
if self._clazz is not None
else []
)
def check_item(self, item: Type, node: Union[ast.Name, ast.Attribute]):
if (
is_versionable(item)
and self._clazz is not None
and item not in self.versioned_classes
and item is not self._clazz
and not issubclass(self._clazz, PluginRenameClass)
):
logger.info(
"Found versionable item %s, checking against %s",
str(item),
str(self.versioned_classes),
)
result = UnrequiredVersionableUsage(
self._module, node, self._clazz.__name__, item.__name__
)
self._violations.append(result)
@property
def violations(self):
return self._violations
def visit_Name(self, node: ast.Name):
try:
logger.debug(
"Checking module %s for name %s", self._module.__name__, node.id
)
item = vars(self._module)[str(node.id)]
logger.debug("Found %s in %s namespace", node.id, self._module.__name__)
except KeyError:
return
self.check_item(item, node)
def visit_Attribute(
self, node: ast.Attribute
) -> Optional[UnrequiredVersionableUsage]:
if self._clazz is None:
self.generic_visit(node)
return
if (node.lineno, node.col_offset) in self._seen:
return
self._seen.add((node.lineno, node.col_offset))
stack = []
root = node
while True:
stack.append(node.attr)
if isinstance(node.value, ast.Attribute):
node = node.value
elif isinstance(node.value, ast.Name):
stack.append(node.value.id)
break
else:
break
current = None
logger.debug("Checking %s", ".".join(stack[::-1]))
for item in stack[::-1]:
try:
current = (
vars(self._module)[item]
if current is None
else getattr(current, item)
)
except (KeyError, AttributeError) as exc:
logger.debug(
"Failed to get attribute %s (%s)%s",
item,
exc.__class__.__name__,
(" on" + str(current)) if current is not None else "",
)
break
self.check_item(current, root)
def report_missing_requirements() -> Iterator[Tuple[str, UnrequiredVersionableUsage]]:
vol3 = importlib.import_module("volatility3")
for _, module_name, _ in pkgutil.walk_packages(
vol3.__path__, vol3.__name__ + ".", onerror=lambda _: None
):
modname = module_name.replace(
"volatility3.framework.plugins", "volatility3.plugins"
)
try:
# import the module that we want to check
plugin_module = importlib.import_module(modname)
except ImportError as exc:
logger.warning("Failed to import %s: %s", modname, str(exc))
continue
except Exception as exc:
logger.warning(
"An unexpected exception occurred while importing %s: %s",
modname,
str(exc),
)
continue
logger.info("Checking module %s", plugin_module.__name__)
if plugin_module.__file__ is None:
logger.warning("Plugin module %s has no source file", modname)
continue
try:
with open(plugin_module.__file__, "rb") as f:
source = f.read()
except OSError:
logger.warning(
"Failed to read file contents for %s", plugin_module.__file__
)
continue
try:
module_ast_root = ast.parse(source)
except (SyntaxError, ValueError) as exc:
logger.warning(
"Failed to parse source for %s: %s", plugin_module.__file__, str(exc)
)
raise
mod_visitor = ModuleVisitor(plugin_module)
mod_visitor.visit(module_ast_root)
if mod_visitor.violations:
yield from (
(plugin_module.__name__, res) for res in iter(mod_visitor.violations)
)
def perform_review():
found = 0
for mod, usage in report_missing_requirements():
found += 1
print(str(usage))
if found:
print(
f"Found {found} issues"
)
sys.exit(1)
print("All configurable classes passed validation!")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", action="count", dest="verbosity", default=0)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
if args.verbosity == 0:
logger.setLevel(logging.WARNING)
elif args.verbosity == 1:
logger.setLevel(logging.INFO)
elif args.verbosity > 1:
logger.setLevel(logging.DEBUG)
perform_review()
+15 -7
View File
@@ -38,6 +38,8 @@ class Volshell(interfaces.plugins.PluginInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
DEFAULT_NUM_DISPLAY_BYTES = 128
def __init__(self, *args, **kwargs):
@@ -52,9 +54,18 @@ class Volshell(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
reqs: List[interfaces.configuration.RequirementInterface] = []
reqs: List[interfaces.configuration.RequirementInterface] = [
requirements.VersionRequirement(
name="regex_scanner",
component=scanners.RegExScanner,
version=(1, 0, 0),
),
]
if cls == Volshell:
reqs = [
reqs += [
requirements.TranslationLayerRequirement(
name="primary", description="Memory layer for the kernel"
),
requirements.URIRequirement(
name="script",
description="File to load and execute at start",
@@ -68,11 +79,8 @@ class Volshell(interfaces.plugins.PluginInterface):
optional=True,
),
]
return reqs + [
requirements.TranslationLayerRequirement(
name="primary", description="Memory layer for the kernel"
),
]
return reqs
def run(
self, additional_locals: Dict[str, Any] = {}
+6 -1
View File
@@ -36,7 +36,12 @@ class Volshell(generic.Volshell):
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
),
]
requirements.VersionRequirement(
name="generic_volshell",
component=generic.Volshell,
version=(1, 0, 0),
),
] + super().get_requirements()
def change_task(self, pid=None):
"""Change the current process and layer, based on a process ID"""
+6 -1
View File
@@ -25,7 +25,12 @@ class Volshell(generic.Volshell):
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
),
]
requirements.VersionRequirement(
name="generic_volshell",
component=generic.Volshell,
version=(1, 0, 0),
),
] + super().get_requirements()
def change_task(self, pid=None):
"""Change the current process and layer, based on a process ID"""
+6 -1
View File
@@ -23,7 +23,12 @@ class Volshell(generic.Volshell):
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
),
]
requirements.VersionRequirement(
name="generic_volshell",
component=generic.Volshell,
version=(1, 0, 0),
),
] + super().get_requirements()
def change_process(self, pid=None):
"""Change the current process and layer, based on a process ID"""
+19 -4
View File
@@ -17,7 +17,7 @@ from volatility3.framework import constants, exceptions, interfaces, layers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import intel, scanners
from volatility3.framework.symbols import native
from volatility3.framework.symbols.windows.pdbutil import PDBUtility
from volatility3.framework.symbols.windows import pdbutil
if __name__ == "__main__":
import sys
@@ -50,6 +50,21 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
max_pdb_size = 0x400000
exclusion_list = ["linux", "mac"]
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.VersionRequirement(
name="pdb_utility",
component=pdbutil.PDBUtility,
version=(1, 0, 1),
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
]
def find_virtual_layers_from_req(
self,
context: interfaces.context.ContextInterface,
@@ -120,7 +135,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
):
raise TypeError("PDB name or GUID not a string value")
PDBUtility.load_windows_symbol_table(
pdbutil.PDBUtility.load_windows_symbol_table(
context=context,
guid=kernel["GUID"],
age=kernel["age"],
@@ -259,7 +274,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
bytes(name + ".pdb", "utf-8")
for name in constants.windows.KERNEL_MODULE_NAMES
]
kernels = PDBUtility.pdbname_scan(
kernels = pdbutil.PDBUtility.pdbname_scan(
ctx=context,
layer_name=layer_to_scan,
start=start_scan_address,
@@ -362,7 +377,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
with contextlib.suppress(exceptions.InvalidAddressException):
if vlayer.read(address, 0x2) == b"MZ":
res = list(
PDBUtility.pdbname_scan(
pdbutil.PDBUtility.pdbname_scan(
ctx=context,
layer_name=vlayer.name,
page_size=vlayer.page_size,
@@ -40,7 +40,12 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
name="SQLiteCache",
component=symbol_cache.SqliteCache,
version=(1, 0, 0),
)
),
requirements.VersionRequirement(
name="multi_string_scanner",
component=scanners.MultiStringScanner,
version=(1, 0, 0),
),
]
@property
+11
View File
@@ -9,6 +9,7 @@ import struct
from typing import Any, Dict, List, Optional, Set, Tuple
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import scanners, segmented
from volatility3.framework.symbols import intermed
@@ -99,6 +100,16 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
context=context, config_path=config_path, name=name, metadata=metadata
)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.VersionRequirement(
name="regex_scanner",
component=scanners.RegExScanner,
version=(1, 0, 0),
),
]
@classmethod
def _check_header(
cls, base_layer: interfaces.layers.DataLayerInterface, name: str = ""
@@ -11,6 +11,8 @@ from volatility3.framework.layers.scanners import multiregexp as multiregexp
class BytesScanner(layers.ScannerInterface):
thread_safe = True
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
def __init__(self, needle: bytes) -> None:
@@ -38,6 +40,8 @@ class RegExScanner(layers.ScannerInterface):
thread_safe = True
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
def __init__(self, pattern: bytes, flags: int = re.DOTALL) -> None:
@@ -57,6 +61,7 @@ class RegExScanner(layers.ScannerInterface):
class MultiStringScanner(layers.ScannerInterface):
thread_safe = True
_version = (1, 0, 0)
_required_framework_version = (2, 0, 0)
def __init__(self, patterns: List[bytes]) -> None:
+6 -1
View File
@@ -22,7 +22,12 @@ class Banners(interfaces.plugins.PluginInterface):
return [
requirements.TranslationLayerRequirement(
name="primary", description="Memory layer to scan"
)
),
requirements.VersionRequirement(
name="regex_scanner",
component=scanners.RegExScanner,
version=(1, 0, 0),
),
]
def _generator(self):
@@ -40,6 +40,16 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
component=timeliner.TimeLinerInterface,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="multi_string_scanner",
component=scanners.MultiStringScanner,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
requirements.ListRequirement(
name="pid",
element_type=int,
@@ -46,6 +46,11 @@ class Check_modules(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.VersionRequirement(
name="modules",
component=linux_utilities_modules.Modules,
version=(3, 0, 1),
),
requirements.VersionRequirement(
name="linux_utilities_modules_module_display_plugin",
component=linux_utilities_modules.ModuleDisplayPlugin,
@@ -93,6 +93,11 @@ class Hidden_modules(plugins.PluginInterface):
component=linux_utilities_modules.ModuleDisplayPlugin,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="linux_utilities_modules",
component=linux_utilities_modules.Modules,
version=(3, 0, 1),
),
] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements()
@staticmethod
@@ -36,6 +36,11 @@ class ModuleExtract(interfaces.plugins.PluginInterface):
description="Base virtual address to reconstruct an ELF file",
optional=False,
),
requirements.VersionRequirement(
name="linux_utilities_module_extract",
version=(1, 0, 0),
component=linux_utilities_module_extract.ModuleExtract,
),
]
def _generator(self):
@@ -41,6 +41,11 @@ class PsScan(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="pslist", component=pslist.PsList, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="multi_string_scanner",
component=scanners.MultiStringScanner,
version=(1, 0, 0),
),
]
def _generator(self):
@@ -46,6 +46,11 @@ class VmaRegExScan(plugins.PluginInterface):
requirements.StringRequirement(
name="pattern", description="RegEx pattern", optional=False
),
requirements.VersionRequirement(
name="regex_scanner",
component=scanners.RegExScanner,
version=(1, 0, 0),
),
requirements.IntRequirement(
name="maxsize",
description="Maximum size in bytes for displayed context",
+10
View File
@@ -38,6 +38,16 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
component=timeliner.TimeLinerInterface,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="multi_string_scanner",
component=scanners.MultiStringScanner,
version=(1, 0, 0),
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
@@ -31,6 +31,11 @@ class List_Files(plugins.PluginInterface):
requirements.VersionRequirement(
name="mount", component=mount.Mount, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="mac_utilities",
component=mac.MacUtilities,
version=(1, 3, 0),
),
]
@classmethod
@@ -39,6 +39,11 @@ class RegExScan(plugins.PluginInterface):
default=cls.MAXSIZE_DEFAULT,
optional=True,
),
requirements.VersionRequirement(
name="regex_scanner",
component=scanners.RegExScanner,
version=(1, 0, 0),
),
]
def _generator(self, regex_pattern):
+7
View File
@@ -26,6 +26,8 @@ class VMCSTest(enum.IntFlag):
class PageStartScanner(interfaces.layers.ScannerInterface):
_version = (1, 0, 0)
def __init__(self, signatures: List[bytes], page_size: int = 0x1000):
super().__init__()
if not len(signatures):
@@ -69,6 +71,11 @@ class Vmscan(plugins.PluginInterface):
requirements.TranslationLayerRequirement(
name="primary", description="Physical base memory layer"
),
requirements.VersionRequirement(
name="page_start_scanner",
component=PageStartScanner,
version=(1, 0, 0),
),
requirements.IntRequirement(
name="log-threshold",
description="Number of criteria failed to log to debug output",
@@ -41,6 +41,11 @@ class CmdScan(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="consoles", component=consoles.Consoles, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="no_registry",
description="Don't search the registry for possible values of CommandHistorySize",
@@ -54,6 +54,11 @@ class Consoles(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="hivelist", component=hivelist.HiveList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="no_registry",
description="Don't search the registry for possible values of CommandHistorySize and HistoryBufferMax",
@@ -37,6 +37,11 @@ class MBRScan(interfaces.plugins.PluginInterface):
default=False,
optional=True,
),
requirements.VersionRequirement(
name="multi_string_scanner",
component=scanners.MultiStringScanner,
version=(1, 0, 0),
),
]
@classmethod
@@ -55,6 +55,8 @@ class PoolConstraint:
class PoolHeaderScanner(interfaces.layers.ScannerInterface):
_version = (1, 0, 0)
def __init__(
self,
module: interfaces.context.ModuleInterface,
@@ -142,6 +144,11 @@ class PoolScanner(plugins.PluginInterface):
requirements.VersionRequirement(
name="handles", component=handles.Handles, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="pool_header_scanner",
component=PoolHeaderScanner,
version=(1, 0, 0),
),
]
def _generator(self):
@@ -63,6 +63,11 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
]
def _check_for_skeleton_key_vad(
@@ -42,6 +42,11 @@ class SvcList(svcscan.SvcScan):
description="Windows kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
]
@classmethod
@@ -56,6 +56,11 @@ class SvcScan(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="hivelist", component=hivelist.HiveList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="bytes_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
]
@classmethod
@@ -41,6 +41,11 @@ class VadRegExScan(plugins.PluginInterface):
element_type=int,
optional=True,
),
requirements.VersionRequirement(
name="regex_scanner",
component=scanners.RegExScanner,
version=(1, 0, 0),
),
requirements.StringRequirement(
name="pattern", description="RegEx pattern", optional=False
),
@@ -48,6 +48,11 @@ class VerInfo(interfaces.plugins.PluginInterface):
requirements.VersionRequirement(
name="modules", component=modules.Modules, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="page_start_scanner",
component=scanners.BytesScanner,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="extensive",
description="Search physical layer for version information",
+6 -1
View File
@@ -118,7 +118,12 @@ class YaraScan(plugins.PluginInterface):
name="primary",
description="Memory layer for the kernel",
architectures=["Intel32", "Intel64"],
)
),
requirements.VersionRequirement(
name="yarascanner",
component=YaraScanner,
version=(2, 1, 1),
),
]
@classmethod