diff --git a/.github/workflows/features-drift.yml b/.github/workflows/features-drift.yml new file mode 100644 index 0000000..bf65e17 --- /dev/null +++ b/.github/workflows/features-drift.yml @@ -0,0 +1,21 @@ +name: features-drift + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + drift: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Check .revengai/features.json is up to date + run: python scripts/emit_features.py --check diff --git a/.revengai/features.json b/.revengai/features.json new file mode 100644 index 0000000..1f5f22e --- /dev/null +++ b/.revengai/features.json @@ -0,0 +1,58 @@ +{ + "features": { + "ai_decompilation_summary": { + "status": "yes" + }, + "analyse_binary": { + "status": "yes" + }, + "apply_existing_analysis": { + "status": "yes" + }, + "binary_auto_analysis": { + "status": "yes" + }, + "binary_upload": { + "note": "No explicit command or feature to upload binaries. Binaries are uploaded on creation of a new analysis only.", + "status": "yes" + }, + "check_analysis_status": { + "status": "yes" + }, + "comment_sync": { + "status": "partial" + }, + "data_types_sync": { + "status": "yes" + }, + "fs_binary_filter": { + "status": "yes" + }, + "fs_collection_filter": { + "status": "yes" + }, + "fs_debug_filter": { + "status": "yes" + }, + "fs_nns_filter": { + "status": "partial" + }, + "fs_similarity_filter": { + "status": "yes" + }, + "function_decompilation": { + "status": "yes" + }, + "rename_from_similar_function": { + "status": "yes" + }, + "search": { + "status": "yes" + }, + "upload_function_names": { + "status": "yes" + } + }, + "plugin": "binary_ninja", + "schema_version": 1 +} diff --git a/docs/FEATURE_STANDARD.md b/docs/FEATURE_STANDARD.md new file mode 100644 index 0000000..b011600 --- /dev/null +++ b/docs/FEATURE_STANDARD.md @@ -0,0 +1,43 @@ +# Feature manifest + +This repo publishes `.revengai/features.json`, a machine-readable manifest listing which +RevEng.AI features this plugin supports. It is generated from code and committed so it is +available at every release tag. + +## Source of truth + +Feature support is declared in [`scripts/emit_features.py`](../scripts/emit_features.py): + +- `FEATURE_CLASSES` maps the plugin's `Feature` classes (registered in + `reai_toolkit/revengai.py`) to a `feature_id` + status. +- `EXTRA` declares features not backed by a single `Feature` class (binary upload, the + function-search filters, status checking, comment/data-type sync, etc.). +- `build_manifest()` merges the two. + +`.revengai/features.json` is generated from this script — never edit the JSON by hand. The +script lives in `scripts/` (not in the `reai_toolkit` package) so it is build tooling and +is not shipped with the plugin. + +## Status values + +`yes`, `partial`, `poc`, `wip`, `planned`, `absent`. A feature may carry an optional `note` +(<=200 chars). + +Feature ids are a shared vocabulary used consistently across the RevEng.AI plugins; reuse +an existing id, or coordinate with the team before introducing a new one. + +## Regenerating + +```sh +python scripts/emit_features.py # rewrite .revengai/features.json +python scripts/emit_features.py --check # exit non-zero if it is stale +``` + +Standard library only (no `binaryninja` imports), so it runs in plain CI without Binary +Ninja. + +## CI + +`.github/workflows/features-drift.yml` runs the `--check` on every pull request and fails +if the committed `.revengai/features.json` does not match the script. When you add or +change a feature: update `scripts/emit_features.py`, run the emitter, and commit both. diff --git a/scripts/emit_features.py b/scripts/emit_features.py new file mode 100644 index 0000000..9367b9a --- /dev/null +++ b/scripts/emit_features.py @@ -0,0 +1,74 @@ +import argparse +import json +import sys +from pathlib import Path + +PLUGIN = "binary_ninja" + +FEATURE_CLASSES = { + "UploadFeature": ("analyse_binary", "yes"), + "AutoUnstripFeature": ("binary_auto_analysis", "yes"), + "ChooseSourceFeature": ("apply_existing_analysis", "yes"), + "MatchCurrentFunctionFeature": ("rename_from_similar_function", "yes"), + "AIDecompilerFeature": ("function_decompilation", "yes"), +} + +EXTRA = { + "binary_upload": { + "status": "yes", + "note": "No explicit command or feature to upload binaries. Binaries are uploaded on creation of a new analysis only.", + }, + "check_analysis_status": {"status": "yes"}, + "fs_collection_filter": {"status": "yes"}, + "fs_binary_filter": {"status": "yes"}, + "fs_debug_filter": {"status": "yes"}, + "fs_nns_filter": {"status": "partial"}, + "fs_similarity_filter": {"status": "yes"}, + "upload_function_names": {"status": "yes"}, + "data_types_sync": {"status": "yes"}, + "comment_sync": {"status": "partial"}, + "search": {"status": "yes"}, + "ai_decompilation_summary": {"status": "yes"}, +} + +MANIFEST_PATH = Path(__file__).resolve().parent.parent / ".revengai" / "features.json" + + +def build_manifest(): + features = {} + for _feature_class, (feature_id, status) in FEATURE_CLASSES.items(): + features.setdefault(feature_id, {"status": status}) + for feature_id, entry in EXTRA.items(): + features.setdefault(feature_id, dict(entry)) + return {"schema_version": 1, "plugin": PLUGIN, "features": features} + + +def serialize(): + return json.dumps(build_manifest(), indent=2, sort_keys=True) + "\n" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + + content = serialize() + if args.check: + current = MANIFEST_PATH.read_text() if MANIFEST_PATH.exists() else "" + if current != content: + print( + "features.json is out of date; run: python scripts/emit_features.py", + file=sys.stderr, + ) + return 1 + print("features.json is up to date.") + return 0 + + MANIFEST_PATH.parent.mkdir(parents=True, exist_ok=True) + MANIFEST_PATH.write_text(content) + print(f"wrote {MANIFEST_PATH}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())