-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
26967b9
commit 0ccbb11
Showing
7 changed files
with
226 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,101 +1,67 @@ | ||
from typing import List | ||
from dataclasses import dataclass, field | ||
import pathlib | ||
from typing import List, Optional, Dict | ||
|
||
import marshmallow.validate | ||
import marshmallow_dataclass | ||
import tomlkit | ||
from tomlkit.items import AoT, Integer, String, Table | ||
from marshmallow_dataclass import dataclass as dataclass_with_schema | ||
|
||
|
||
class PluginDescription: | ||
""" | ||
Describes an angr management plugin. Can be generated from plugin.toml. | ||
""" | ||
|
||
def __init__(self): | ||
# Metadata | ||
self.plugin_metadata_version: int = None | ||
@dataclass_with_schema | ||
class MetadataDescription: | ||
version: int = field(metadata={"validate": marshmallow.validate.OneOf([0])}) | ||
|
||
# Plugin | ||
self.name: str = "" | ||
self.shortname: str = "" | ||
self.version: str = "" | ||
self.description: str = "" | ||
self.long_description: str = "" | ||
self.platforms: List[str] = [] | ||
self.min_angr_vesion: str = "" | ||
self.author = "" | ||
self.entrypoints: List[str] = [] | ||
self.require_workspace: bool = True | ||
self.has_url_actions: bool = False | ||
|
||
# file path | ||
self.plugin_file_path: str = "" | ||
|
||
@classmethod | ||
def load_single_plugin(cls, data: Table) -> "PluginDescription": | ||
desc = PluginDescription() | ||
@dataclass_with_schema | ||
class PackageDescription: | ||
""" | ||
Describes a plugin package. | ||
""" | ||
name: str = field() | ||
version: str = field() | ||
platforms: List[str] = field(default_factory=lambda: ["any"]) | ||
site_packages: Optional[str] = field(default=None) | ||
authors: List[str] = field(default_factory=list) | ||
description: str = field(default="") | ||
long_description: str = field(default="") | ||
|
||
desc.name = data.get("name", None) | ||
if not isinstance(desc.name, String): | ||
raise TypeError(f'"name" must be a String instance, not a {type(desc.name)}') | ||
if not desc.name: | ||
raise TypeError('"name" cannot be empty') | ||
|
||
desc.shortname = data.get("shortname", None) | ||
if not isinstance(desc.shortname, String): | ||
raise TypeError(f'"shortname" must be a String instance, not a {type(desc.shortname)}') | ||
if not desc.shortname: | ||
raise TypeError('"shortname" cannot be empty') | ||
@dataclass_with_schema | ||
class PluginDescription: | ||
""" | ||
Describes an angr management plugin. Can be generated from plugin.toml. | ||
""" | ||
name: str = field() | ||
entrypoint: str = field() | ||
platforms: Optional[List[str]] = field(default=None) | ||
description: str = field(default="") | ||
requires_workspace: bool = field(default=False) | ||
|
||
desc.entrypoints = data.get("entrypoints", "") | ||
if not isinstance(desc.entrypoints, List) or not all( | ||
isinstance(entrypoint, String) for entrypoint in desc.entrypoints | ||
): | ||
raise TypeError('"entrypoints" must be a List of String instances') | ||
if not desc.entrypoints: | ||
raise TypeError('"entrypoints" cannot be empty') | ||
|
||
# optional | ||
desc.version = data.get("version", "") | ||
desc.description = data.get("description", "") | ||
desc.long_description = data.get("long_description", "") | ||
desc.platforms = data.get("platforms", "") | ||
desc.min_angr_vesion = data.get("min_angr_version", "") | ||
desc.author = data.get("author", "") | ||
desc.require_workspace = data.get("require_workspace", True) | ||
desc.has_url_actions = data.get("has_url_actions", False) | ||
@dataclass | ||
class PluginConfigFileDescription: | ||
""" | ||
Describes a plugin config file. | ||
""" | ||
metadata: MetadataDescription = field() | ||
package: PackageDescription = field() | ||
plugins: Dict[str, PluginDescription] = field(default_factory=dict) | ||
|
||
return desc | ||
|
||
@classmethod | ||
def from_toml(cls, file_path: str) -> List["PluginDescription"]: | ||
with open(file_path, encoding="utf-8") as f: | ||
data = tomlkit.load(f) | ||
PluginConfigSchema = marshmallow_dataclass.class_schema(PluginConfigFileDescription)() | ||
|
||
# load metadata | ||
outer_desc = PluginDescription() | ||
if "meta" in data and "plugin_metadata_version" in data["meta"]: | ||
if isinstance(data["meta"]["plugin_metadata_version"], Integer): | ||
outer_desc.plugin_metadata_version = data["meta"]["plugin_metadata_version"].unwrap() | ||
|
||
if outer_desc.plugin_metadata_version is None: | ||
raise TypeError("Cannot find plugin_metadata_version") | ||
if outer_desc.plugin_metadata_version != 0: | ||
raise TypeError(f"Unsupported plugin metadata version {outer_desc.plugin_metadata_version}") | ||
def from_toml_string(toml_string: str) -> PluginConfigFileDescription: | ||
""" | ||
Load a plugin config file from a TOML string. | ||
""" | ||
return PluginConfigSchema.load(tomlkit.parse(toml_string)) | ||
|
||
descs = [] | ||
# load plugin information | ||
if "plugins" in data and isinstance(data["plugins"], AoT): | ||
# multiple plugins to load! | ||
for plugin in data["plugins"]: | ||
desc = PluginDescription.load_single_plugin(plugin) | ||
desc.plugin_metadata_version = outer_desc.plugin_metadata_version | ||
desc.plugin_file_path = file_path | ||
descs.append(desc) | ||
elif "plugin" in data: | ||
desc = PluginDescription.load_single_plugin(data["plugin"]) | ||
desc.plugin_metadata_version = outer_desc.plugin_metadata_version | ||
desc.plugin_file_path = file_path | ||
descs.append(desc) | ||
else: | ||
raise TypeError('Cannot find any "plugin" or "plugins" table.') | ||
|
||
return descs | ||
def from_toml_file(toml_file: pathlib.Path) -> PluginConfigFileDescription: | ||
""" | ||
Load a plugin config file from a TOML file. | ||
""" | ||
with open(toml_file, "r") as f: | ||
return from_toml_string(f.read()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This directory should be adapted into a sphinx project to be hosted on readthedocs with other angr documentation. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
Plugin Metadata Specification | ||
============================= | ||
|
||
Each plugin directory must have a plugin.toml file. A plugin metadata file has | ||
two types of sections: `meta` and `plugin`. Each plugin.toml must contain one | ||
meta section, but may contain | ||
Below are tables of keys for each | ||
section. | ||
|
||
## Meta section | ||
| Key | Type | Required | Notes | | ||
| ------- | ------- | -------- | ------------------- | | ||
| version | integer | yes | Currently version 0 | | ||
|
||
|
||
## Package section | ||
| Key | Type | Required | Default | Notes | | ||
| ---------------- | ---------------- | -------- | ------- | --------- | | ||
| name | string | yes | | | | ||
| version | string | yes | | | | ||
| platforms | list[string] | no | ["any"] | See below | | ||
| site_packages | Optional[string] | no | None | | | ||
| authors | list[string] | no | [] | | | ||
| description | string | no | "" | | | ||
| long-description | string | no | "" | | | ||
|
||
|
||
### Values for `platforms` | ||
angr management treats `"any"` as matching all platfoms. Otherwise, angr | ||
management checks if Python's `sys.platform` starts with any of the listed | ||
strings. See https://docs.python.org/3/library/sys.html#sys.platform to learn | ||
more about the `sys.platform` value in Python. | ||
|
||
|
||
## Plugin section | ||
| Key | Type | Required | Default | Notes | | ||
| ------------------ | ---------------------- | -------- | --------------------- | ------------------------------------------ | | ||
| name | string | yes | | | | ||
| entrypoint | string | yes | | Use file.py::ClassName syntax, like pytest | | ||
| platforms | Optional[list[string]] | no | package.site-packages | overrides package default if configured | | ||
| description | string | no | "" | | | ||
| requires_workspace | bool | no | false | | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# Plugin specification | ||
|
||
angr management supports loading plugins that can extend and adapt the | ||
functionality of angr management itself. In order to create an angr management | ||
plugin, all that is needed is a directory containing two files: a `plugin.toml` | ||
and a Python file where that plugin is implemented. For example, this is valid | ||
plugin package directory layout: | ||
|
||
``` | ||
example-plugin/ | ||
example_plugin.py | ||
plugin.toml | ||
``` | ||
|
||
Inside `example_plugin.py`, a super minimal plugin example looks like: | ||
|
||
```py | ||
import angrmanagement.plugins.BasePlugin | ||
|
||
class ExamplePlugin(BasePlugin): | ||
pass | ||
``` | ||
|
||
A valid `plugin.toml` that would allow this plugin to be loaded would look like | ||
this: | ||
|
||
```toml | ||
[metadata] | ||
version = 0 | ||
|
||
[package] | ||
name = "example-plugin" | ||
version = "1.0" | ||
|
||
[plugin.example] | ||
name = "Example Plugin" | ||
version = "1.0" | ||
entrypoints = ["example_plugin.py::ExamplePlugin"] | ||
``` | ||
|
||
For more information what fields are available in a plugin.toml, se the | ||
[plugin metadata specification](./plugin_metadata_spec.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import unittest | ||
|
||
import tomlkit | ||
|
||
from angrmanagement.plugins.plugin_description import ( | ||
PackageDescription, | ||
PluginDescription, | ||
from_toml_string, | ||
MetadataDescription, | ||
MetadataDescription, | ||
MetadataDescription, | ||
) | ||
|
||
|
||
class TestPluginDescriptionLoading(unittest.TestCase): | ||
def test_metadta_section(self): | ||
test_data = "version = 0" | ||
MetadataDescription.Schema().load(tomlkit.parse(test_data)) | ||
|
||
def test_metadata_section_invalid_version(self): | ||
test_data = "version = 1_000_000" | ||
with self.assertRaises(Exception): | ||
MetadataDescription.Schema().load(tomlkit.parse(test_data)) | ||
|
||
def test_minimal_package(self): | ||
test_data = """ | ||
name = "example" | ||
version = "1.0" | ||
""" | ||
PackageDescription.Schema().load(tomlkit.parse(test_data)) | ||
|
||
def test_minimal_plugin(self): | ||
test_data = """ | ||
name = "Example" | ||
entrypoint = "example.py::ExamplePlugin" | ||
""" | ||
PluginDescription.Schema().load(tomlkit.parse(test_data)) | ||
|
||
def test_minimal(self): | ||
test_data = """ | ||
[metadata] | ||
version = 0 | ||
[package] | ||
name = "example" | ||
version = "1.0" | ||
[plugin.example] | ||
name = "Example" | ||
version = "1.0" | ||
entrypoints = ["example.py::ExamplePlugin"] | ||
""" | ||
from_toml_string(test_data) | ||
|
||
def test_multiple(self): | ||
test_data = """ | ||
[metadata] | ||
version = 0 | ||
[package] | ||
name = "example" | ||
version = "1.0" | ||
platforms = ["any"] | ||
site_pacakges = "site-packages" | ||
authors = ["Example"] | ||
description = "An example plugin package" | ||
long-description = "An example plugin package for testing angr management" | ||
[plugin.example1] | ||
name = "Example 1" | ||
entrypoints = ["example.py::ExamplePlugin1"] | ||
platforms = ["linux"] | ||
description = "An example plugin for testing angr management on linuz" | ||
requires-workspace = false | ||
[plugin.example2] | ||
name = "Example 2" | ||
entrypoints = ["example.py::ExamplePlugin2"] | ||
platforms = ["win32", "cygwin"] | ||
description = "An example plugin for testing angr management on windows" | ||
requires-workspace = true | ||
""" | ||
from_toml_string(test_data) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |