Repository: 0xeb/ida-qscripts Branch: main Commit: e754bfa2de81 Files: 73 Total size: 116.2 KB Directory structure: gitextract_tqzl_grq/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── CLAUDE.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── TODO.md ├── ida-plugin.json ├── ida.h ├── prep-cmake.bat ├── qscripts.cpp ├── script.hpp ├── snippet_manager/ │ ├── README.md │ ├── pseudocode.cpp │ └── snippetmanager.py ├── test_addons/ │ ├── README.md │ ├── loader_template/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── driver.cpp │ │ ├── idasdk.h │ │ └── main.cpp │ ├── plugin_template/ │ │ ├── .claude/ │ │ │ └── agents/ │ │ │ └── ida-cmake.md │ │ ├── .gitignore │ │ ├── CLAUDE.md │ │ ├── CMakeLists.txt │ │ ├── driver.cpp │ │ ├── idasdk.h │ │ └── main.cpp │ ├── plugin_triton/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── README.md │ │ ├── driver.cpp │ │ ├── idasdk.h │ │ ├── junk64.bin.i64 │ │ ├── main.cpp │ │ ├── qscripts_native_triton.py │ │ └── qscripts_native_triton.py.deps.qscripts │ ├── qscripts_native.py │ └── qscripts_native.py.deps.qscripts ├── test_scripts/ │ ├── dependency-test/ │ │ ├── README.md │ │ ├── subdir/ │ │ │ ├── t4.py │ │ │ ├── t4.py.deps.qscripts │ │ │ └── t5.py │ │ ├── t1.py │ │ ├── t1.py.deps.qscripts │ │ ├── t2.py │ │ ├── t2.py.deps.qscripts │ │ └── t3.py │ ├── hello.idc │ ├── hello.py │ ├── notebooks/ │ │ ├── 0000 Imports and Init.py │ │ ├── 0000 Imports and Init.py.deps.qscripts │ │ ├── 0010 List functions.py │ │ ├── 0020 List segments.py │ │ ├── 0030 Show info.py │ │ ├── 0040 Set name.py │ │ ├── 0050 Set name.py │ │ ├── A0000 Init.py │ │ ├── A0000 Init.py.deps.qscripts │ │ ├── A0010 Cell 1.py │ │ ├── A0020 Cell 2.py │ │ ├── B0000 Init.py │ │ ├── B0000 Init.py.deps.qscripts │ │ ├── B0010 Cell 1.py │ │ ├── B0020 Cell 2.py │ │ └── README.md │ ├── pkg-dependency/ │ │ ├── README.md │ │ └── example.py.deps.qscripts │ └── trigger-file/ │ ├── dep.py │ ├── trigger.py │ └── trigger.py.deps.qscripts └── utils_impl.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * -crlf ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: branches: ["**"] tags: ["v*", "V*"] pull_request: branches: ["**"] workflow_dispatch: permissions: contents: write concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: build-linux: name: Build for Linux runs-on: ubuntu-latest env: IDASDK: ${{ github.workspace }}/ida-sdk steps: - name: Checkout project uses: actions/checkout@v4 - name: Prepare IDA SDK run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk - name: Configure (Linux) run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - name: Build (Linux) run: cmake --build build -- -j2 - name: Upload Linux artifact (qscripts.so) uses: actions/upload-artifact@v4 with: name: qscripts-linux path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.so build-macos: name: Build for macOS runs-on: macos-latest env: IDASDK: ${{ github.workspace }}/ida-sdk steps: - name: Checkout project uses: actions/checkout@v4 - name: Prepare IDA SDK run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk - name: Configure (macOS) run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - name: Build (macOS) run: cmake --build build -- -j2 - name: Upload macOS artifact (qscripts.dylib) uses: actions/upload-artifact@v4 with: name: qscripts-macos path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.dylib build-windows: name: Build for Windows runs-on: windows-latest env: IDASDK: ${{ github.workspace }}/ida-sdk steps: - name: Checkout project uses: actions/checkout@v4 - name: Prepare IDA SDK run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk - name: Configure (Windows) run: cmake -S . -B build -A x64 - name: Build (Windows) run: cmake --build build --config Release -- /m - name: Upload Windows artifact (qscripts.dll) uses: actions/upload-artifact@v4 with: name: qscripts-windows path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.dll publish-release: name: Publish Release runs-on: ubuntu-latest needs: [ build-linux, build-macos, build-windows ] if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout project uses: actions/checkout@v4 - name: Download all artifacts uses: actions/download-artifact@v4 with: pattern: qscripts-* path: release merge-multiple: true - name: Copy metadata to release folder run: | cp ida-plugin.json release/ || echo "No ida-plugin.json found" - name: List release files run: | ls -la release || true - name: Create zip package run: | cd release if [ -f ida-plugin.json ]; then zip -9 qscripts-${{ github.ref_name }}.zip qscripts.dll qscripts.so qscripts.dylib ida-plugin.json else zip -9 qscripts-${{ github.ref_name }}.zip qscripts.dll qscripts.so qscripts.dylib fi ls -lh *.zip - name: Create GitHub Release and upload assets uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: QScripts ${{ github.ref_name }} draft: false prerelease: false generate_release_notes: true files: | release/qscripts-${{ github.ref_name }}.zip release/qscripts.dll release/qscripts.so release/qscripts.dylib release/ida-plugin.json ================================================ FILE: .gitignore ================================================ .vscode/ obj/ __pycache__/ build*/ .vs/ Debug/ Release/ x64/ BINARIES/ .claude/settings.local.json .claude/agents/ida-cmake.md ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. # QScripts - IDA Pro Productivity Plugin QScripts is an IDA Pro plugin that enables automatic script execution upon file changes, supporting hot-reload development workflows with Python, IDC, and compiled plugins. ## Build Commands ### Prerequisites - **IDASDK**: Environment variable must be set to IDA SDK path - **ida-cmake**: Must be installed at `$IDASDK/ida-cmake/` - **idacpp**: Optional - will be automatically fetched if not found at `$IDASDK/include/idacpp/` ### Build with ida-cmake agent Always use the ida-cmake agent for building: ``` Task with subagent_type="ida-cmake" ``` ### Manual build (Windows) ```bash # Configure CMake prep-cmake.bat # Build prep-cmake.bat build # Clean build prep-cmake.bat clean ``` ## Architecture ### Core Components **Main Plugin (`qscripts.cpp`)** - Implements `qscripts_chooser_t`: Non-modal chooser UI for script management - File monitoring system with configurable intervals (default 500ms) - Dependency tracking and automatic reloading - Integration with IDA's recent scripts system (shares same list) **Script Management (`script.hpp`)** - `fileinfo_t`: File metadata and modification tracking - `script_info_t`: Script state management (active/inactive/dependency) - `active_script_info_t`: Extends script_info with dependency handling - Dependency resolution with recursive parsing and cycle detection ### Key Features Implementation **Dependency System** - Dependencies defined in `.deps.qscripts` files - Supports `/reload` directive for Python module reloading - `/triggerfile` directive for custom trigger conditions - `/notebook` mode for cell-based execution - Variable expansion: `$basename$`, `$env:VAR$`, `$pkgbase$`, `$ext$` **File Monitoring** - Uses qtimer-based polling (not OS-specific watchers yet) - Monitors active script and all dependencies - Trigger file support for compiled plugin development ## IDA SDK and API Resources When answering SDK/API questions, search and read from: - **SDK Headers**: `$IDASDK/include` - All headers have docstrings - **SDK Examples**: `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module` ## Testing Test scripts are located in `test_scripts/`: - `dependency-test/` - Simple dependency examples - `pkg-dependency/` - Package dependency examples - `notebooks/` - Notebook mode examples - `trigger-native/` - Compiled plugin hot-reload examples ## Plugin States Scripts can be in three states: 1. **Normal**: Shown in regular font 2. **Active** (bold): Currently monitored for changes 3. **Inactive** (italics): Previously active but monitoring disabled ## Important Implementation Notes - Plugin uses idacpp wrapper library for enhanced C++ API - Shares script list with IDA's built-in "Recent Scripts" (Alt-F9) - Maximum 512 scripts in list (IDA_MAX_RECENT_SCRIPTS) - Special unload function: `__quick_unload_script` called before reload - Supports undo via IDA's undo system when enabled in options ## CI/CD and Releases ### Automated Builds - GitHub Actions builds for Linux (.so), macOS (.dylib), and Windows (.dll) - Runs on every push and pull request - CMake automatically fetches idacpp if not available in IDASDK ### Creating Releases To create a new release with pre-built binaries: ```bash git tag v1.0.0 # Create version tag git push origin v1.0.0 # Push tag to trigger release ``` This will automatically: 1. Build qscripts for all 3 platforms 2. Create a GitHub Release page 3. Upload all platform binaries (.dll, .so, .dylib) 4. Generate release notes from commits ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.27) project(qscripts) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Include IDA SDK bootstrap include($ENV{IDASDK}/src/cmake/bootstrap.cmake) find_package(idasdk REQUIRED) # Add idacpp library with fallback # First, try to use idacpp from IDASDK set(IDACPP_SDK_PATH "$ENV{IDASDK}/include/idacpp") set(IDACPP_LOCAL_PATH "${CMAKE_CURRENT_BINARY_DIR}/idacpp") if(EXISTS "${IDACPP_SDK_PATH}/CMakeLists.txt") message(STATUS "Using idacpp from IDASDK: ${IDACPP_SDK_PATH}") add_subdirectory(${IDACPP_SDK_PATH} ${CMAKE_CURRENT_BINARY_DIR}/idacpp) else() message(STATUS "idacpp not found in IDASDK, fetching to build directory...") # Use FetchContent to download idacpp into build directory # NOTE: The header-only idacpp wrapper is in the allthingsida/libidacpp repo include(FetchContent) FetchContent_Declare( idacpp GIT_REPOSITORY https://github.com/allthingsida/libidacpp.git GIT_TAG main SOURCE_DIR ${IDACPP_LOCAL_PATH} ) FetchContent_MakeAvailable(idacpp) endif() # Convert paths to native format set(SAMPLE_IDB "${IDA_CMAKE_DIR}/samples/wizmo32.exe.i64") file(TO_NATIVE_PATH "${SAMPLE_IDB}" SAMPLE_IDB) # Disabled sources list(APPEND DISABLED_SOURCES utils_impl.cpp) set_source_files_properties(${DISABLED_SOURCES} PROPERTIES LANGUAGE "") # Add plugin ida_add_plugin(qscripts SOURCES qscripts.cpp ida.h script.hpp ${DISABLED_SOURCES} DEBUG_ARGS "${SAMPLE_IDB}" ) target_link_libraries(qscripts PRIVATE idacpp::idacpp) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Elias Bachaalany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # What is QScripts? QScripts is productivity tool and an alternative to IDA's "Recent scripts" (Alt-F9) and "Execute Scripts" (Shift-F2) facilities. QScripts allows you to develop and run any supported scripting language (\*.py; \*.idc, etc.) from the comfort of your own favorite text editor as soon as you save the active script, the trigger file or any of its dependencies. QScripts also supports hot-reloading of native plugins (loaders, processor modules, and plugins) using trigger files, enabling rapid development of compiled IDA addons. ![Quick introduction](docs/_resources/qscripts-vid-1.gif) Video tutorials on the [AllThingsIDA](https://www.youtube.com/@allthingsida) YouTube channel: - [Boost your IDA programming efficiency tenfold using the ida-qscripts productivity plugin](https://youtu.be/1UEoLAgEGMc?si=YMieIKHEY0AXgMHU) - [Scripting concepts and productivity tips for IDAPython & IDC](https://youtu.be/RgHmwHN0NLk?si=OCnLMhcAmHAQPgNI) - [An exercise in netnodes with the snippet manager plugin](https://youtu.be/yhVdLYzFJW0?si=z3xMqCEFOU89gAkI) # Usage Invoke QScripts from the plugins menu, or its default hotkey Alt-Shift-F9. When it runs, the scripts list might be empty. Just press `Ins` and select a script to add, or press `Del` to delete a script from the list. QScripts shares the same scripts list as IDA's `Recent Scripts` window. To execute a script, just press `ENTER` or double-click it. After running a script once, it will become the active script (shown in **bold**). An active script will then be monitored for changes. If you modify the script in your favorite text editor and save it, then QScripts will execute the script for you automatically in IDA. To deactivate the script monitor, just press `Ctrl-D` or right-click and choose `Deactivate script monitor` from the QScripts window. When an active script becomes inactive, it will be shown in *italics*. ## Keyboard shortcuts Inside QScripts window: * `Alt-Shift-F9`: Open QScripts window * `ENTER` or double-click: Activate and execute selected script * `Shift-Enter`: Execute selected script without activating it * `Ins`: Add a new script to the list * `Del`: Remove a script from the list * `Ctrl-E`: Open options dialog * `Ctrl-D`: Deactivate script monitor From anywhere in IDA: * `Alt-Shift-X`: Re-execute the last active script or notebook cell ## Configuration options Press `Ctrl+E` or right-click and select `Options` to configure QScripts: * Clear message window before execution: clear the message log before re-running the script. Very handy if you to have a fresh output log each time. * Show file name when execution: display the name of the file that is automatically executed * Execute the unload script function: A special function, if defined in the global scope (usually by your active script), called `__quick_unload_script` will be invoked before reloading the script. This gives your script a chance to do some cleanup (for example to unregister some hotkeys) * Script monitor interval: controls the refresh rate of the script change monitor. Ideally 500ms is a good amount of time to pick up script changes. * Allow QScripts execution to be undo-able: The executed script's side effects can be reverted with IDA's Undo. # Managing Dependencies in QScripts QScripts offers a feature that allows automatic re-execution of the active script when any of its dependent scripts, undergo modifications. ## Setting Up Automatic Dependencies To leverage the automatic dependency tracking feature, create a file named identically to your active script, appending `.deps.qscripts` to its name. This file should contain paths to dependent scripts, along with any necessary reload directives. Alternatively, you can place a `.deps` file (without the `.qscripts` suffix) within a `.qscripts` subfolder, located alongside your active script. **Example locations for `script.py`:** * `script.py.deps.qscripts` (same directory) * `.qscripts/script.py.deps.qscripts` (local folder) * `.qscripts/script.py.deps` (local folder, alternate name) ## Dependency Index File Syntax The dependency index file supports comments and directives: ```txt # Python-style comments ; Semicolon comments // C-style comments /reload import importlib; import $basename$; importlib.reload($basename$); dependency1.py dependency2.py ``` ## Integrating Python Scripts For projects involving Python, QScripts can automatically [reload](https://docs.python.org/3/library/importlib.html#importlib.reload) any changed dependent Python scripts. Include a `/reload` directive in your `.deps.qscripts` file, followed by the appropriate Python reload syntax. ### Example `.deps.qscripts` file for `t1.py`: ```txt /reload import importlib; import $basename$; importlib.reload($basename$); t2.py # This is a comment t3.py ``` The `t1.py.deps.qscripts` configuration enables the following behavior: 1. **Script Auto-Execution**: Changes to `t1.py` trigger its automatic re-execution within the IDA environment. 2. **Dependency Reload**: Modifications to the dependency index file (`t1.py.deps.qscripts`) lead to the reloading of specified dependencies, followed by the re-execution of the active script. 3. **Dependency Script Changes**: Any alteration in a dependency script file causes the active script to re-execute. If a reload directive is present, the modified dependency files are also reloaded. In our cases, if either or both of `t2.py` and `t3.py` are modified, `t1.py` is re-executed and the modified dependencies are reloaded as well. **Note**: If a dependent script possesses its own `.deps.qscripts` file, QScripts recursively integrates all linked dependencies into the active script's dependencies. However, specific directives (e.g., `reload`) within these recursive dependencies are disregarded. See also: * [Simple dependency example](test_scripts/dependency-test/README.md) * [Package dependency example](test_scripts/pkg-dependency/README.md) ## Directive Reference Directives must appear at the beginning of a line and start with `/`. Arguments follow the directive name. ### `/reload ` Executes the specified code snippet when a dependency is modified, before re-executing the main script. The code is executed in the context of the dependency's language interpreter. ```txt /reload import importlib; import $basename$; importlib.reload($basename$); ``` ### `/pkgbase ` Specifies a package base directory for Python package dependencies. This path is used in conjunction with `$pkgmodname$` and `$pkgparentmodname$` variables. ```txt /pkgbase C:\projects\mypackage ``` ### `/triggerfile [/keep] ` Configures QScripts to execute the script when the specified trigger file is created or modified, rather than when the script itself changes. This is particularly useful for hot-reloading compiled native plugins. Options: * `/keep`: Preserve the trigger file after execution (default behavior is to delete it) ```txt /triggerfile /keep C:\temp\build_done.flag ``` ### `/notebook []` Enables notebook mode where a directory of scripts is treated as a collection of cells. When any file matching the cell pattern is saved, that cell is executed. ```txt /notebook My Analysis Notebook ``` ### `/notebook.cells_re <regex>` Specifies a regular expression pattern to identify notebook cell files. The default pattern is `\d{4}.*\.py$`. ```txt /notebook.cells_re ^\d{4}_.+\.py$ ``` ### `/notebook.activate <action>` Controls the behavior when the notebook is activated: * `exec_none`: Display the notebook title but do not execute any scripts * `exec_main`: Execute the main script file * `exec_all`: Execute all notebook cells in order ```txt /notebook.activate exec_all ``` ## Special Variables Variables are expanded when encountered in file paths or reload directives. Use the syntax `$variable$`. * `$basename$`: The base name (without extension) of the current dependency file * `$env:VariableName$`: The value of the environment variable `VariableName` * `$pkgbase$`: The package base directory (set via `/pkgbase`) * `$pkgmodname$`: The module name derived from the dependency file path relative to `$pkgbase$`, with path separators replaced by dots (e.g., `pkg.submodule.file`) * `$pkgparentmodname$`: The parent module name (e.g., `pkg.submodule`) * `$ext$`: The platform-specific plugin extension (e.g., `64.dll`, `.so`, `64.dylib`) ### Examples ```txt # Reload a Python module /reload import importlib; import $basename$; importlib.reload($basename$); # Use environment variable in path $env:SCRIPTS_DIR$/helper.py # Package reloading /pkgbase C:\myproject\src /reload import importlib; import $pkgmodname$; importlib.reload($pkgmodname$); src/utils/helper.py ``` # Using QScripts like a Jupyter notebook QScripts can monitor a directory of script files as if they were notebook cells. When you save any file matching the cell pattern, that cell is executed. **Example notebook configuration for `notebook.py.deps.qscripts`:** ```txt /notebook Data Analysis Notebook /notebook.cells_re ^\d{4}_.+\.py$ /notebook.activate exec_none /reload import importlib; import $basename$; importlib.reload($basename$); shared_utils.py ``` With this configuration: * Files like `0010_load_data.py`, `0020_process.py`, `0030_visualize.py` are recognized as cells * Saving any cell executes only that cell * The notebook title is displayed when activated * Changes to `shared_utils.py` cause the last-executed cell to re-run See also: * [Notebooks dependency example](test_scripts/notebooks/README.md) # Using QScripts with trigger files Sometimes you don't want to trigger QScripts when your scripts are saved, instead you want your own trigger condition. One way to achieve a custom trigger is by using the `/triggerfile` directive: ``` /triggerfile createme.tmp ; Dependencies... dep1.py ``` This tells QScripts to wait until the trigger file `createme.tmp` is created (or modified) before executing your script. Now, any time you want to execute the active script, just create (or modify) the trigger file. You may pass the `/keep` option so QScripts does not delete your trigger file: ``` /triggerfile /keep dont_del_me.info ``` # Using QScripts programmatically QScripts can be controlled programmatically from scripts or other plugins. ## Plugin Arguments ```python # Open QScripts window idaapi.load_and_run_plugin("qscripts", 0) # Execute the last selected script idaapi.load_and_run_plugin("qscripts", 1) # Activate the script monitor idaapi.load_and_run_plugin("qscripts", 2) # Deactivate the script monitor idaapi.load_and_run_plugin("qscripts", 3) ``` ## Action IDs QScripts registers the following actions that can be invoked programmatically: * `qscripts:deactivatemonitor` - Deactivate the script monitor * `qscripts:execselscript` - Execute the selected script without activating it * `qscripts:execscriptwithundo` - Re-execute the last active script or notebook cell * `qscripts:executenotebook` - Execute all cells in the active notebook ```python # Re-execute the active script idaapi.process_ui_action("qscripts:execscriptwithundo") # Execute all notebook cells idaapi.process_ui_action("qscripts:executenotebook") ``` # Hot-Reloading Native Plugins QScripts supports hot-reloading of compiled native plugins (IDA plugins, loaders, and processor modules) using trigger files. This enables rapid iterative development of native IDA addons without restarting IDA. ![Compiled code](docs/_resources/trigger_native.gif) ## Requirements The native plugin must be designed to support unloading: * **Plugins**: Set the `PLUGIN_UNL` flag to allow IDA to unload the plugin after each invocation * **Loaders**: Loaders are naturally unloadable * **Processor modules**: Use appropriate cleanup in module termination ## Workflow The hot-reload workflow uses trigger files to detect when a new binary is available: 1. Create a loader script that invokes your native plugin 2. Configure a dependency file with `/triggerfile` pointing to the compiled binary 3. Build your native plugin 4. QScripts detects the binary change and executes the loader script 5. The loader script invokes the newly compiled plugin ## Example: Hot-Reloading a Plugin For a plugin with the `PLUGIN_UNL` flag (like the IDA SDK `hello` sample): **Step 1**: Create a loader script `load_hello.py`: ```python # Optionally clear the screen idaapi.msg_clear() # Load your plugin and pass any arg value you want idaapi.load_and_run_plugin('hello', 0) # Optionally, do post work, etc. # ... ``` **Step 2**: Create the dependency file `load_hello.py.deps.qscripts`: ``` /triggerfile /keep C:\<ida_dir>\plugins\hello$ext$ ``` **Step 3**: Activate `load_hello.py` in QScripts (press `ENTER` on it) **Step 4**: Build or rebuild the plugin in your IDE The moment the compilation succeeds, the new binary will be detected (since it is the trigger file) and your loader script will use IDA's `load_and_run_plugin()` to run the plugin again. ## Example: Hot-Reloading a Loader For loaders, the workflow is similar but uses `load_file()` instead: **Loader script `test_loader.py`:** ```python idaapi.msg_clear() # Unload the old loader if needed # (IDA handles this automatically for loaders) # Load a test file with your loader idaapi.load_file("C:\\testfiles\\myformat.bin", 0) ``` **Dependency file `test_loader.py.deps.qscripts`:** ``` /triggerfile /keep C:\<ida_dir>\loaders\myloader$ext$ ``` ## Using $ext$ Variable The `$ext$` variable automatically expands to the platform-specific plugin extension: * Windows 64-bit: `64.dll` * Windows 32-bit: `.dll` * Linux 64-bit: `64.so` * Linux 32-bit: `.so` * macOS: `64.dylib` or `.dylib` This allows your dependency files to work across platforms without modification. ## Additional Examples Please check the native addons examples in [test_addons](test_addons/) for complete working examples of hot-reloading plugins, loaders, and processor modules. # Building QScripts uses [idacpp](https://github.com/allthingsida/idacpp) and is built using [ida-cmake](https://github.com/allthingsida/ida-cmake). If you don't want to build from sources, then there are release pre-built for MS Windows. # Installation QScripts is written in C++ with IDA's SDK and therefore it should be deployed like a regular plugin. Copy the plugin binaries to either of those locations: * `<IDA_install_folder>/plugins` * `%APPDATA%\Hex-Rays/plugins` Since the plugin uses IDA's SDK and no other OS specific functions, the plugin should be compilable for macOS and Linux just fine. I only provide MS Windows binaries. Please check the [releases page](https://github.com/allthingsida/ida-qscripts/releases). # BONUS ## Snippet Manager QScripts ships with a simple [Snippet Manager](snippet_manager/README.md) plugin to allow you to manage script snippets. ================================================ FILE: TODO.md ================================================ # Ideas ## Allow dot folders - Check for $(pwd)/.qscripts/<sourcefilename.depsfiles> first before checking the current directory - This allows less pollution in the current folder # TODO - Automatically select the previous active script when the UI launches - Restore the QScripts window layout when closed and re-opened - Use OS specific file change monitors - It is time to re-write this, now that we have all the specs and features # Monitor code and refactoring - Isolate the timer code from the UI logic - Refactor filemonitor into a utility class - Perhaps implement per OS on Win32, and as is for Linux/Mac - For Linux, perhaps call an external process like 'inotify-wait' ? ================================================ FILE: ida-plugin.json ================================================ { "IDAMetadataDescriptorVersion": 1, "plugin": { "name": "QScripts", "version": "1.2.6", "entryPoint": "qscripts", "description": "Develop IDA scripts faster with hot-reload and automatic execution on file changes. Supports Python, IDC, and compiled plugins with dependency tracking.", "urls": { "repository": "https://github.com/allthingsida/qscripts" }, "authors": [ { "name": "Elias Bachaalany", "email": "elias.bachaalany@gmail.com" } ], "categories": [ "api-scripting-and-automation", "collaboration-and-productivity" ], "idaVersions": ">=9.0", "platforms": [ "windows-x86_64", "linux-x86_64", "macos-aarch64" ], "license": "MIT" } } ================================================ FILE: ida.h ================================================ #pragma once #pragma warning(push) #pragma warning(disable: 4267 4244 4146) #include <loader.hpp> #include <idp.hpp> #include <expr.hpp> #include <prodir.h> #include <kernwin.hpp> #include <diskio.hpp> #include <registry.hpp> #include <idacpp/kernwin/kernwin.hpp> using namespace idacpp::kernwin; #pragma warning(pop) // IDA 8.3 #ifndef CH_NOIDB #define CH_NOIDB CH_UNUSED #endif ================================================ FILE: prep-cmake.bat ================================================ @echo off :: checkout the Batchography book setlocal if not defined IDASDK ( echo IDASDK environment variable not set. goto :eof ) if not exist %IDASDK%\src\cmake\bootstrap.cmake ( echo IDA SDK cmake not found at %IDASDK%\src\cmake\bootstrap.cmake goto :eof ) if "%1"=="clean" ( if exist build rmdir /s /q build goto :eof ) if not exist build cmake -B build -S . -A x64 if "%1"=="build" cmake --build build --config Release echo. echo All done! echo. ================================================ FILE: qscripts.cpp ================================================ /* Quick execute script: a plugin to speedup IDA scripts development. This plugin replaces the regular "Recent scripts" and "Execute Script" dialogs and allows you to develop scripts in your favorite editor and execute them directly in IDA. (c) Elias Bachaalany <elias.bachaalany@gmail.com> */ #include <unordered_map> #include <string> #include <regex> #include <filesystem> #include <unordered_set> #include "ida.h" #include "utils_impl.cpp" #include "script.hpp" //------------------------------------------------------------------------- // Some constants static constexpr int IDA_MAX_RECENT_SCRIPTS = 512; static constexpr char IDAREG_RECENT_SCRIPTS[] = "RecentScripts"; //------------------------------------------------------------------------- // Non-modal scripts chooser struct qscripts_chooser_t: public plugmod_t, public chooser_t { using chooser_t::operator delete; using chooser_t::operator new; private: action_manager_t am; bool m_b_filemon_timer_active = false; qtimer_t m_filemon_timer = nullptr; const std::regex RE_EXPANDER = std::regex(R"(\$(.+?)\$)"); int opt_change_interval = 500; int opt_clear_log = 0; int opt_show_filename = 0; int opt_exec_unload_func = 0; int opt_with_undo = 0; active_script_info_t selected_script; script_info_t* action_active_script = nullptr; struct expand_ctx_t { // input qstring script_file; bool main_file; // working qstring base_dir; qstring pkg_base; qstring reload_cmd; }; inline int normalize_filemon_interval(const int change_interval) const { return qmax(300, change_interval); } const char *get_selected_script_file() { return selected_script.file_path.c_str(); } bool make_meta_filename( const char* filename, const char* extension, qstring& out, bool local_only = false) { // Check the .qscripts folder char dir[2048]; if (qdirname(dir, sizeof(dir), filename)) { out.sprnt("%s" SDIRCHAR QSCRIPTS_LOCAL SDIRCHAR "%s.%s", dir, qbasename(filename), extension); if (qfileexist(out.c_str())) return true; } if (local_only) return false; // Check the actual script folder out.sprnt("%s.%s", filename, extension); return qfileexist(out.c_str()); } bool find_deps_file( const char* filename, qstring& out) { return make_meta_filename(filename, "deps", out, true) || make_meta_filename(filename, "deps.qscripts", out); } bool parse_deps_for_script(expand_ctx_t &ctx) { // Parse the dependency index file qstring dep_file; if (!find_deps_file(ctx.script_file.c_str(), dep_file)) return false; FILE *fp = qfopen(dep_file.c_str(), "r"); if (fp == nullptr) return false; // Get the dependency file directory ctx.base_dir.resize(ctx.script_file.size()); qdirname(ctx.base_dir.begin(), ctx.base_dir.size(), ctx.script_file.c_str()); ctx.base_dir.resize(strlen(ctx.base_dir.c_str())); // Add the dependency file to the active script selected_script.add_dep_index(dep_file.c_str()); static auto get_value = [](const char* str, const char* key, int key_len) -> const char * { if (strncmp(str, key, key_len) != 0) return nullptr; // Empty value? if (str[key_len] == '\0') return ""; else return str + key_len + 1; }; // Parse each line for (qstring line = dep_file; qgetline(&line, fp) != -1;) { line.trim2(); // Skip comment lines (';', '//' and '#') if (line.empty() || strncmp(line.c_str(), "//", 2) == 0 || line[0] == '#' || line[0] == ';') continue; // Parse special directives (some apply only for the main selected script) if (auto val = get_value(line.c_str(), "/pkgbase", 8)) { if (ctx.main_file) { ctx.pkg_base = val; expand_file_name(ctx.pkg_base, ctx); make_abs_path(ctx.pkg_base, ctx.base_dir.c_str(), true); } continue; } else if (auto val = get_value(line.c_str(), "/notebook.cells_re", 18)) { if (ctx.main_file) { selected_script.notebook.cells_re = std::regex(val); continue; } } else if (auto val = get_value(line.c_str(), "/notebook.activate", 18)) { if (ctx.main_file) { int act; if (qstrcmp(val, "exec_main") == 0) act = notebook_ctx_t::act_exec_main; else if (qstrcmp(val, "exec_all") == 0) act = notebook_ctx_t::act_exec_all; else act = notebook_ctx_t::act_exec_none; selected_script.notebook.activation_action = act; continue; } } else if (auto val = get_value(line.c_str(), "/notebook", 9)) { if (ctx.main_file) { selected_script.b_is_notebook = true; selected_script.notebook.title = val; continue; } } else if (auto val = get_value(line.c_str(), "/reload", 7)) { if (ctx.main_file) ctx.reload_cmd = val; continue; } else if (auto trigger_file = get_value(line.c_str(), "/triggerfile", 12)) { if (auto keep = get_value(trigger_file, "/keep", 5)) { trigger_file = keep; selected_script.b_keep_trigger_file = true; } if (ctx.main_file) { selected_script.trigger_file.refresh(trigger_file); expand_file_name(selected_script.trigger_file.file_path, ctx); } continue; } // From here on, the *line* variable is an expandable string leading to a script file ctx.script_file = line; expand_file_name(line, ctx); normalize_path_sep(line); // Skip dependency scripts that (do not|no longer) exist script_info_t dep_script; if (!get_file_modification_time(line, &dep_script.modified_time)) continue; // Add script dep_script.file_path = line.c_str(); dep_script.reload_cmd = ctx.reload_cmd; dep_script.pkg_base = ctx.pkg_base; selected_script.dep_scripts[line.c_str()] = std::move(dep_script); expand_ctx_t sub_ctx = ctx; sub_ctx.script_file = line; sub_ctx.main_file = false; parse_deps_for_script(sub_ctx); } qfclose(fp); return true; } void expand_file_name(qstring &filename, const expand_ctx_t &ctx) { expand_string(filename, filename, ctx); make_abs_path(filename, ctx.base_dir.c_str(), true); } void populate_initial_notebook_cells() { auto& cell_files = selected_script.notebook.cell_files; auto current_path = std::filesystem::path(selected_script.file_path.c_str()).parent_path(); selected_script.notebook.base_path = current_path.string(); enumerate_files( current_path, selected_script.notebook.cells_re, [&cell_files](const std::string& filename) { qtime64_t mtime; get_file_modification_time(filename, &mtime); cell_files[filename] = mtime; return true; } ); } void set_selected_script(script_info_t &script) { // Activate a new script selected_script.clear(); selected_script.refresh(script.file_path.c_str()); // Recursively parse the dependencies and the index files expand_ctx_t main_ctx = { script.file_path.c_str(), true }; parse_deps_for_script(main_ctx); // If a notebook is selected, let's capture all the cell files if (selected_script.is_notebook()) populate_initial_notebook_cells(); } void clear_selected_script() { action_active_script = nullptr; selected_script.clear(); // ...and deactivate the monitor activate_monitor(false); } const bool has_selected_script() { return !selected_script.file_path.empty(); } bool is_monitor_active() const { return m_b_filemon_timer_active; } bool is_filemon_timer_installed() const { return m_filemon_timer != nullptr; } std::string expand_pkgmodname(const expand_ctx_t& ctx) { auto dep_file = selected_script.has_dep(ctx.script_file.c_str()); qstring pkg_base = dep_file == nullptr ? selected_script.pkg_base : dep_file->pkg_base; // If the script file is in the package base, then replace the path separators with '.' if (strncmp(ctx.script_file.c_str(), pkg_base.c_str(), pkg_base.length()) == 0) { qstring s = ctx.script_file.c_str() + pkg_base.length() + 1; s.replace(SDIRCHAR, "."); // Drop the extension too auto idx = s.rfind('.'); if (idx != -1) s.resize(idx); return s.c_str(); } return ""; } // Dynamic string expansion Description // ------------------------ ----------- // basename Returns the basename of the input file // env:Variable_Name Expands the 'Variable_Name' // pkgbase Sets the current pkgbase path // pkgmodname Expands the file name using the pkgbase into the form: 'module.submodule1.submodule2' // pkgparentmodname Expands the file name using the pkgbase into the form up to the parent module: 'module.submodule1' // ext Add-on suffix including bitness and extension (example: 64.dll, .so, 64.so, .dylib, etc.) void expand_string( qstring &input, qstring &output, const expand_ctx_t& ctx) { output = std::regex_replace( input.c_str(), RE_EXPANDER, [this, ctx](auto &m) -> std::string { qstring match1 = m.str(1).c_str(); if (strncmp(match1.c_str(), "pkgmodname", 10) == 0) { return expand_pkgmodname(ctx); } else if (strncmp(match1.c_str(), "pkgparentmodname", 16) == 0) { std::string pkgmodname = expand_pkgmodname(ctx); size_t pos = pkgmodname.rfind('.'); return pos == std::string::npos ? pkgmodname : pkgmodname.substr(0, pos); } else if (strncmp(match1.c_str(), "ext", 3) == 0) { static_assert(LOADER_DLL[0] == '*'); return LOADER_DLL + 1; } else if (strncmp(match1.c_str(), "pkgbase", 7) == 0) { return ctx.pkg_base.c_str(); } else if (strncmp(match1.c_str(), "basename", 8) == 0) { char *basename, *ext; qstring wrk_str; get_basename_and_ext(ctx.script_file.c_str(), &basename, &ext, wrk_str); return basename; } else if (strncmp(match1.c_str(), "env:", 4) == 0) { qstring env; if (qgetenv(match1.begin() + 4, &env)) return env.c_str(); } return m.str(1); } ).c_str(); } bool execute_reload_directive( script_info_t &dep_script_file, qstring &err, bool silent=true) { const char *script_file = dep_script_file.file_path.c_str(); do { auto ext = get_file_ext(script_file); extlang_object_t elang(find_extlang_by_ext(ext == nullptr ? "" : ext)); if (elang == nullptr) { err.sprnt("unknown script language detected for '%s'!\n", script_file); break; } qstring reload_cmd; expand_ctx_t ctx; ctx.script_file = script_file; ctx.pkg_base = dep_script_file.pkg_base; expand_string(dep_script_file.reload_cmd, reload_cmd, ctx); if (!elang->eval_snippet(reload_cmd.c_str(), &err)) { err.sprnt( "QScripts failed to reload script file: '%s'\n" "Reload command used: %s\n" "Error: %s\n", script_file, reload_cmd.c_str(), err.c_str()); break; } return true; } while (false); if (!silent) msg("%s", err.c_str()); return false; } bool execute_script(script_info_t *script_info, bool with_undo) { if (with_undo) { action_active_script = script_info; auto r = process_ui_action(ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID); action_active_script = nullptr; return r; } return execute_script_sync(script_info); } // Executes a script file bool execute_script_sync(script_info_t *script_info) { bool exec_ok = false; // Pause the file monitor timer while executing a script bool old_state = activate_monitor(false); do { auto script_file = script_info->file_path.c_str(); // First things first: always take the file's modification time-stamp first so not to visit it again in the file monitor timer if (!get_file_modification_time(script_file, &script_info->modified_time)) { msg("Script file '%s' not found!\n", script_file); break; } const char *script_ext = get_file_ext(script_file); extlang_object_t elang(nullptr); if (script_ext == nullptr || (elang = find_extlang_by_ext(script_ext)) == nullptr) { msg("Unknown script language detected for '%s'!\n", script_file); break; } if (opt_clear_log) msg_clear(); // Silently call the unload script function qstring errbuf; if (opt_exec_unload_func) { idc_value_t result; elang->call_func(&result, UNLOAD_SCRIPT_FUNC_NAME, &result, 0, &errbuf); } if (opt_show_filename) msg("QScripts executing %s...\n", script_file); exec_ok = elang->compile_file( script_file, #if IDA_SDK_VERSION >= 850 nullptr, // requested_namespace #endif &errbuf); if (!exec_ok) { msg("QScripts failed to compile script file: '%s':\n%s", script_file, errbuf.c_str()); break; } // Special case for IDC scripts: we have to call 'main' if (elang->is_idc()) { idc_value_t result; exec_ok = elang->call_func(&result, "main", &result, 0, &errbuf); if (!exec_ok) { msg("QScripts failed to run the IDC main() of file '%s':\n%s", script_file, errbuf.c_str()); break; } } } while (false); activate_monitor(old_state); return exec_ok; } enum { OPTID_INTERVAL = 0x0001, OPTID_CLEARLOG = 0x0002, OPTID_SHOWNAME = 0x0004, OPTID_UNLOADEXEC = 0x0008, OPTID_SELSCRIPT = 0x0010, OPTID_WITHUNDO = 0x0020, OPTID_ONLY_SCRIPT = OPTID_SELSCRIPT, OPTID_ALL_BUT_SCRIPT = 0xffff & ~OPTID_ONLY_SCRIPT, OPTID_ALL = 0xffff, }; // Save or load the options void saveload_options(bool bsave, int what_ids = OPTID_ALL) { enum { QSTR = 1000 }; struct options_t { int id; const char *name; int vtype; void *pval; } int_options [] = { {OPTID_INTERVAL, "QScripts_interval", VT_LONG, &opt_change_interval}, {OPTID_CLEARLOG, "QScripts_clearlog", VT_LONG, &opt_clear_log}, {OPTID_SHOWNAME, "QScripts_showscriptname", VT_LONG, &opt_show_filename}, {OPTID_UNLOADEXEC, "QScripts_exec_unload_func", VT_LONG, &opt_exec_unload_func}, {OPTID_SELSCRIPT, "QScripts_selected_script_name", QSTR, &selected_script.file_path}, {OPTID_WITHUNDO, "QScripts_with_undo", VT_LONG, &opt_with_undo} }; for (auto &opt: int_options) { if ((what_ids & opt.id) == 0) continue; if (opt.vtype == VT_LONG) { if (bsave) reg_write_int(opt.name, *(int *)opt.pval); else *(int *)opt.pval = reg_read_int(opt.name, *(int *)opt.pval); } else if (opt.vtype == VT_STR) { if (bsave) reg_write_string(opt.name, ((qstring *)opt.pval)->c_str()); else reg_read_string(((qstring *)opt.pval), opt.name); } else if (opt.vtype == QSTR) { if (bsave) { reg_write_string(opt.name, ((qstring *)opt.pval)->c_str()); } else { qstring tmp; reg_read_string(&tmp, opt.name); *((qstring *)opt.pval) = tmp.c_str(); } } } if (!bsave) opt_change_interval = normalize_filemon_interval(opt_change_interval); } static int idaapi s_filemon_timer_cb(void *ud) { return ((qscripts_chooser_t *)ud)->filemon_timer_cb(); } // Monitor callback int filemon_timer_cb() { do { // No active script, do nothing if (!is_monitor_active() || !has_selected_script()) break; std::unique_ptr<active_script_info_t> notebook_cell_script; active_script_info_t* work_script = &selected_script; // // Handle dependencies first // // Check if the active script or its dependencies are changed: // 1. Dependency file --> repopulate it and execute active script // 2. Any dependencies --> reload if needed and // // 3. Active script --> execute it again auto& dep_scripts = selected_script.dep_scripts; // Let's check the dependencies index files first auto mod_stat = selected_script.is_any_dep_index_modified(); if (mod_stat == filemod_status_e::modified) { // Force re-parsing of the index file dep_scripts.clear(); set_selected_script(selected_script); // Let's invalidate all the scripts time stamps so we ensure they are re-interpreted again selected_script.invalidate_all_scripts(); // Refresh the UI refresh_chooser(QSCRIPTS_TITLE); // Just leave and come back fast so we get a chance to re-evaluate everything return 1; // (1 ms) } // Dependency index file is gone else if (mod_stat == filemod_status_e::not_found && !dep_scripts.empty()) { // Let's just check the active script dep_scripts.clear(); } // // Check the dependency scripts // bool dep_script_changed = false; bool brk = false; for (auto& kv : dep_scripts) { auto& dep_script = kv.second; if (dep_script.get_modification_status() == filemod_status_e::modified) { qstring err; dep_script_changed = true; if (dep_script.has_reload_directive() && !execute_reload_directive(dep_script, err, false)) { brk = true; break; } } } if (brk) break; // // Notebook mode // if (selected_script.is_notebook()) { auto& last_active_cell = selected_script.notebook.last_active_cell; auto& cell_files = selected_script.notebook.cell_files; auto current_path = std::filesystem::path(selected_script.file_path.c_str()).parent_path(); std::unordered_set<std::string> present_files; std::string active_cell; enumerate_files( current_path, selected_script.notebook.cells_re, [&present_files, &last_active_cell, &active_cell, &cell_files](const std::string& filename) { present_files.insert(filename); auto p = cell_files.find(filename); qtime64_t mtime; get_file_modification_time(filename.c_str(), &mtime); // New file? if (p == cell_files.end()) { cell_files[filename] = mtime; } // File was modified? else if (p->second != mtime) { last_active_cell = active_cell = filename; p->second = mtime; // Stop enumeration; next interval we pick up the rest return false; } return true; } ); // Remove missing files from cell_files for (auto it = cell_files.begin(); it != cell_files.end();) { if (present_files.find(it->first) == present_files.end()) it = cell_files.erase(it); else ++it; } // We have to always execute a script when a dependency changes: // - If a dependency has changed, but no active cells changedthen attempt to use the last active cell. if (dep_script_changed && active_cell.empty()) active_cell = selected_script.notebook.last_active_cell; // If no modified cell files, then do nothing if (!active_cell.empty()) { // ...use the same metadata as the notebook main script, but just execute the given cell notebook_cell_script.reset(new active_script_info_t(selected_script)); work_script = notebook_cell_script.get(); work_script->file_path = active_cell.c_str(); } } // // Trigger mode // // In trigger file mode, just wait for the trigger file to be created else if (selected_script.trigger_based()) { // The monitor waits until the trigger file is created or modified auto trigger_status = selected_script.trigger_file.get_modification_status(true); if (trigger_status != filemod_status_e::modified) break; // Delete the trigger file if (!selected_script.b_keep_trigger_file) qunlink(selected_script.trigger_file.c_str()); // Always execute the main script even if it was not changed selected_script.invalidate(); // ...and proceed with QScript logic } // Check the main script mod_stat = work_script->get_modification_status(); if (mod_stat == filemod_status_e::not_found) { // Script no longer exists msg( "QScripts detected that the active script '%s' no longer exists!\n", work_script->file_path.c_str()); clear_selected_script(); break; } // Script or its dependencies changed? if (dep_script_changed || mod_stat == filemod_status_e::modified) execute_script(work_script, opt_with_undo); } while (false); return opt_change_interval; } protected: static constexpr uint32 flags_ = CH_KEEP | CH_RESTORE | CH_ATTRS | CH_NOIDB | CH_CAN_DEL | CH_CAN_EDIT | CH_CAN_INS | CH_CAN_REFRESH; static constexpr int widths_[2] = { 20, 70 }; static constexpr const char *const header_[2] = { "Script", "Path" }; static constexpr const char *ACTION_DEACTIVATE_MONITOR_ID = "qscripts:deactivatemonitor"; static constexpr const char *ACTION_EXECUTE_SELECTED_SCRIPT_ID = "qscripts:execselscript"; static constexpr const char *ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID = "qscripts:execscriptwithundo"; static constexpr const char *ACTION_EXECUTE_NOTEBOOK_ID = "qscripts:executenotebook"; scripts_info_t m_scripts; ssize_t m_nselected = NO_SELECTION; static bool is_correct_widget(action_update_ctx_t* ctx) { return ctx->widget_title == QSCRIPTS_TITLE; } // Add a new script file and properly populate its script info object // and returns a borrowed reference const script_info_t *add_script( const char *script_file, bool silent = false, bool unique = true) { if (unique) { auto p = m_scripts.find({ script_file }); if (p != m_scripts.end()) return &*p; } qtime64_t mtime; if (!get_file_modification_time(script_file, &mtime)) { if (!silent) msg("Script file not found: '%s'\n", script_file); return nullptr; } auto &si = m_scripts.push_back(); si.file_path = script_file; si.modified_time = mtime; return &si; } bool config_dialog() { static const char form[] = "Options\n" "\n" "<#Controls the refresh rate of the script change monitor#Script monitor ~i~nterval:D:100:10::>\n" "<#Clear the output window before re-running the script#C~l~ear the output window:C>\n" "<#Display the name of the file that is automatically executed#Show ~f~ile name when execution:C>\n" "<#Execute a function called '__quick_unload_script' before reloading the script#Execute the u~n~load script function:C>\n" "<#The executed scripts' side effects can be reverted with IDA's Undo#Allow QScripts execution to be ~u~ndo-able:C>>\n" "\n" "\n"; // Copy values to the dialog union { ushort n; struct { ushort b_clear_log : 1; ushort b_show_filename : 1; ushort b_exec_unload_func : 1; ushort b_with_undo : 1; }; } chk_opts; // Load previous options first (account for multiple instances of IDA) saveload_options(false); chk_opts.n = 0; chk_opts.b_clear_log = opt_clear_log; chk_opts.b_show_filename = opt_show_filename; chk_opts.b_exec_unload_func = opt_exec_unload_func; chk_opts.b_with_undo = opt_with_undo; sval_t interval = opt_change_interval; if (ask_form(form, &interval, &chk_opts.n) > 0) { // Copy values from the dialog opt_change_interval = normalize_filemon_interval(int(interval)); opt_clear_log = chk_opts.b_clear_log; opt_show_filename = chk_opts.b_show_filename; opt_exec_unload_func = chk_opts.b_exec_unload_func; opt_with_undo = chk_opts.b_with_undo; // Save the options directly saveload_options(true); return true; } return false; } const void *get_obj_id(size_t *len) const override { // Allow a single instance *len = sizeof(this); return (const void *)this; } size_t idaapi get_count() const override { return m_scripts.size(); } void idaapi get_row( qstrvec_t *cols, int *icon, chooser_item_attrs_t *attrs, size_t n) const override { auto si = &m_scripts[n]; auto path = si->file_path.c_str(); auto name = strrchr(path, DIRCHAR); cols->at(0) = name == nullptr ? path : name + 1; cols->at(1) = path; if (n == m_nselected) { if (is_monitor_active()) { attrs->flags = CHITEM_BOLD; *icon = selected_script.is_notebook() ? IDAICONS::NOTEPAD_1 : IDAICONS::KEYBOARD_GRAY; } else { attrs->flags = CHITEM_ITALIC; *icon = IDAICONS::RED_DOT; } } else if (is_monitor_active() && selected_script.has_dep(si->file_path) != nullptr) { // Mark as a dependency *icon = IDAICONS::EYE_GLASSES_EDIT; } else { // Mark as an inactive file *icon = IDAICONS::GRAY_X_CIRCLE; } } // Activate a script and execute it cbret_t idaapi enter(size_t n) override { bool exec_ok = false; m_nselected = n; // Set as the selected script and execute it set_selected_script(m_scripts[n]); if ( selected_script.is_notebook() && selected_script.notebook.activation_action == notebook_ctx_t::act_exec_none) { // Notebook, execute nothing on activation msg("Selected notebook: %s\n", selected_script.notebook.title.c_str()); } else if ( selected_script.is_notebook() && selected_script.notebook.activation_action == notebook_ctx_t::act_exec_all) { // Notebook, execute all scripts on activation msg("Executing all scripts for notebook: %s\n", selected_script.notebook.title.c_str()); execute_notebook_cells(&selected_script); } else { exec_ok = execute_script(&selected_script, opt_with_undo); } if (exec_ok) saveload_options(true, OPTID_ONLY_SCRIPT); // ...and activate the monitor even if the script fails activate_monitor(); return cbret_t(n, chooser_base_t::ALL_CHANGED); } // Add a new script cbret_t idaapi ins(ssize_t) override { qstring filter; get_browse_scripts_filter(filter); const char *script_file = ask_file(false, "", "%s", filter.c_str()); if (script_file == nullptr) return {}; reg_update_strlist(IDAREG_RECENT_SCRIPTS, script_file, IDA_MAX_RECENT_SCRIPTS); ssize_t idx = build_scripts_list(script_file); return cbret_t(qmax(idx, 0), chooser_base_t::ALL_CHANGED); } // Remove a script from the list cbret_t idaapi del(size_t n) override { auto &script_file = m_scripts[n].file_path; reg_update_strlist(IDAREG_RECENT_SCRIPTS, nullptr, IDA_MAX_RECENT_SCRIPTS, script_file.c_str()); build_scripts_list(); // Active script removed? if (m_nselected == NO_SELECTION) clear_selected_script(); return adjust_last_item(n); } // Use it to show the configuration dialog cbret_t idaapi edit(size_t n) override { config_dialog(); return cbret_t(n, chooser_base_t::NOTHING_CHANGED); } void idaapi closed() override { saveload_options(true); } static void get_browse_scripts_filter(qstring &filter) { // Collect all installed external languages extlangs_t langs; collect_extlangs(&langs, false); // Build the filter filter = "FILTER Script files|"; for (auto lang: langs) filter.cat_sprnt("*.%s;", lang->fileext); filter.remove_last(); filter.append("|"); // Language specific filters for (auto lang: langs) filter.cat_sprnt("%s scripts|*.%s|", lang->name, lang->fileext); filter.remove_last(); filter.append("\nSelect script file to load"); } void setup_ui() { am.add_action( AMAHF_NONE, ACTION_DEACTIVATE_MONITOR_ID, "QScripts: Deactivate script monitor", "Ctrl-D", FO_ACTION_UPDATE([this], return this->is_correct_widget(ctx) ? AST_ENABLE_FOR_WIDGET : AST_DISABLE_FOR_WIDGET; ), FO_ACTION_ACTIVATE([this]) { if (this->is_monitor_active()) { this->clear_selected_script(); refresh_chooser(QSCRIPTS_TITLE); } return 1; }, nullptr, IDAICONS::DISABLED); am.add_action( AMAHF_NONE, ACTION_EXECUTE_SELECTED_SCRIPT_ID, "QScripts: Execute selected script", "Shift-Enter", FO_ACTION_UPDATE([this], return this->is_correct_widget(ctx) ? AST_ENABLE_FOR_WIDGET : AST_DISABLE_FOR_WIDGET; ), FO_ACTION_ACTIVATE([this]) { if (!ctx->chooser_selection.empty()) this->execute_script_at(ctx->chooser_selection.at(0)); return 1; }, "Execute script without activating it", IDAICONS::FLASH); am.add_action( AMAHF_NONE, ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID, "QScripts: Execute last active script", "Alt-Shift-X", FO_ACTION_UPDATE([this], return AST_ENABLE_ALWAYS; ), FO_ACTION_ACTIVATE([this]) { if (is_monitor_active()) { script_info_t* work_script = nullptr; std::unique_ptr<active_script_info_t> cell_script; if (selected_script.is_notebook()) { if (!selected_script.notebook.last_active_cell.empty()) { cell_script.reset(new active_script_info_t(selected_script)); if (work_script = cell_script.get()) work_script->file_path = selected_script.notebook.last_active_cell.c_str(); } } else if (action_active_script != nullptr) work_script = action_active_script; else if (this->has_selected_script()) work_script = &selected_script; if (work_script != nullptr) this->execute_script_sync(work_script); } return 1; }, "An action to programmatically execute the active script", IDAICONS::FLASH); am.add_action( AMAHF_NONE, ACTION_EXECUTE_NOTEBOOK_ID, "QScripts: Execute all notebook cells", "", FO_ACTION_UPDATE([this], return AST_ENABLE_ALWAYS; ), FO_ACTION_ACTIVATE([this]) { if (this->has_selected_script() && selected_script.is_notebook()) this->execute_notebook_cells(&selected_script); return 1; }, "An action to programmatically execute the active script", IDAICONS::NOTEPAD_1); } public: static constexpr const char *QSCRIPTS_TITLE = "QScripts"; qscripts_chooser_t(const char *title_ = QSCRIPTS_TITLE) : chooser_t(flags_, qnumber(widths_), widths_, header_, title_), am(this) { popup_names[POPUP_EDIT] = "~O~ptions"; setup_ui(); saveload_options(false); } bool activate_monitor(bool activate = true) { bool old = m_b_filemon_timer_active; m_b_filemon_timer_active = activate; return old; } // Rebuilds the scripts list and returns the index of the `find_script` if needed ssize_t build_scripts_list(const char *find_script = nullptr) { // Remember active script and invalidate its index bool b_has_selected_script = has_selected_script(); qstring selected_script; if (b_has_selected_script) selected_script = get_selected_script_file(); // De-selected the current script in the hope of finding it again in the list m_nselected = NO_SELECTION; // Read all scripts qstrvec_t scripts_list; reg_read_strlist(&scripts_list, IDAREG_RECENT_SCRIPTS); // Rebuild the list ssize_t idx = 0, find_idx = NO_SELECTION; m_scripts.qclear(); for (auto &script_file: scripts_list) { // Restore active script if (b_has_selected_script && selected_script == script_file) m_nselected = idx; // Optionally, find the index of a script by name if (find_script != nullptr && streq(script_file.c_str(), find_script)) find_idx = idx; // We skip non-existent scripts if (add_script(script_file.c_str(), true) != nullptr) ++idx; } return find_idx; } void execute_last_selected_script(bool with_undo=false) { if (has_selected_script()) execute_script(&selected_script, with_undo); } void execute_script_at(ssize_t n) { if (n >=0 && n < ssize_t(m_scripts.size())) execute_script(&m_scripts[n], opt_with_undo); } void execute_notebook_cells(active_script_info_t *script) { auto& cell_files = script->notebook.cell_files; active_script_info_t cell_script = *script; enumerate_files( script->notebook.base_path, script->notebook.cells_re, [this, &cell_script, &cell_files](const std::string& filename) { qtime64_t mtime; get_file_modification_time(filename, &mtime); cell_files[filename] = mtime; cell_script.file_path = filename.c_str(); // Execute script and stop enumeration if one cell fails to execute return this->execute_script_sync(&cell_script); } ); } void show() { build_scripts_list(); auto r = choose(m_nselected); TWidget *widget; if (r == 0 && (widget = find_widget(QSCRIPTS_TITLE)) != nullptr) { attach_action_to_popup( widget, nullptr, ACTION_DEACTIVATE_MONITOR_ID); attach_action_to_popup( widget, nullptr, ACTION_EXECUTE_SELECTED_SCRIPT_ID); attach_action_to_popup( widget, nullptr, ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID); attach_action_to_popup( widget, nullptr, ACTION_EXECUTE_NOTEBOOK_ID); } } bool install_filemon_timer() { m_filemon_timer = register_timer( opt_change_interval, s_filemon_timer_cb, this); return is_filemon_timer_installed(); } void uninstall_filemon_timer() { if (is_filemon_timer_installed()) { unregister_timer(m_filemon_timer); m_filemon_timer = nullptr; } activate_monitor(false); } bool idaapi run(size_t arg) override { switch (arg) { // Full UI run case 0: { if (!is_filemon_timer_installed()) { if (!install_filemon_timer()) msg("QScripts: failed to start the file monitor.\n"); else msg("QScripts: file monitor started successfully.\n"); } show(); break; } // Execute the selected script case 1: { execute_last_selected_script(); break; } // Activate the scripts monitor case 2: { activate_monitor(true); refresh_chooser(QSCRIPTS_TITLE); break; } // Deactivate the scripts monitor case 3: { activate_monitor(false); refresh_chooser(QSCRIPTS_TITLE); break; } } return true; } virtual ~qscripts_chooser_t() { uninstall_filemon_timer(); } }; //------------------------------------------------------------------------- plugmod_t *idaapi init(void) { auto plg = new qscripts_chooser_t(); if (!plg->install_filemon_timer()) { if (::debug & IDA_DEBUG_PLUGIN) msg("QScripts: failed to install the file monitor on startup. Please invoke the UI once to attempt try again!\n"); } return plg; } //-------------------------------------------------------------------------- static const char help[] = "An alternative scripts manager that lets you develop in an external editor and run them fast in IDA\n" "\n" "Just press ENTER on the script to activate it and then go back to your editor to continue development.\n" "\n" "Each time you update your script, it will be automatically invoked in IDA\n\n" "\n" "QScripts is developed by Elias Bachaalany. Please see https://github.com/0xeb/ida-qscripts for more information\n" "\n" "\0" __DATE__ " " __TIME__ "\n" "\n"; //-------------------------------------------------------------------------- // // PLUGIN DESCRIPTION BLOCK // //-------------------------------------------------------------------------- plugin_t PLUGIN = { IDP_INTERFACE_VERSION, PLUGIN_MULTI | PLUGIN_FIX, init, nullptr, nullptr, "QScripts: Develop IDA scripts faster in your favorite text editor", help, qscripts_chooser_t::QSCRIPTS_TITLE, #ifdef _DEBUG "Alt-Shift-A" #else "Alt-Shift-F9" #endif }; ================================================ FILE: script.hpp ================================================ #pragma once #define QSCRIPTS_LOCAL ".qscripts" static constexpr char UNLOAD_SCRIPT_FUNC_NAME[] = "__quick_unload_script"; static constexpr auto DEFAULT_CELLS_RE = R"(\d{4}.*\.py$)"; //------------------------------------------------------------------------- // File modification state enum class filemod_status_e { not_found, not_modified, modified }; // Structure to describe a file and its metadata struct fileinfo_t { qstring file_path; qtime64_t modified_time; fileinfo_t(const char* file_path = nullptr): modified_time(0) { if (file_path != nullptr) this->file_path = file_path; } inline const bool empty() const { return file_path.empty(); } inline const char* c_str() { return file_path.c_str(); } bool operator==(const fileinfo_t &rhs) const { return file_path == rhs.file_path; } virtual void clear() { file_path.clear(); modified_time = 0; } bool refresh(const char *file_path = nullptr) { if (file_path != nullptr) this->file_path = file_path; return get_file_modification_time(this->file_path, &modified_time); } // Checks if the current script has been modified // Optionally updates the time stamp to the latest one if modified filemod_status_e get_modification_status(bool update_mtime=true) { qtime64_t cur_mtime; const char *script_file = this->file_path.c_str(); if (!get_file_modification_time(script_file, &cur_mtime)) { if (update_mtime) modified_time = 0; return filemod_status_e::not_found; } // Script is up to date, no need to execute it again if (cur_mtime == modified_time) return filemod_status_e::not_modified; if (update_mtime) modified_time = cur_mtime; return filemod_status_e::modified; } void invalidate() { modified_time = 0; } }; //------------------------------------------------------------------------- // Dependency script info struct script_info_t: fileinfo_t { using fileinfo_t::fileinfo_t; // Each dependency script can have its own reload command qstring reload_cmd; // Base path if this dependency is part of a package qstring pkg_base; const bool has_reload_directive() const { return !reload_cmd.empty(); } }; // Script files using scripts_info_t = qvector<script_info_t>; //------------------------------------------------------------------------- // Notebook context struct notebook_ctx_t { enum activate_action_e { act_exec_none, act_exec_main, act_exec_all }; std::string base_path; std::string title; std::regex cells_re = std::regex(DEFAULT_CELLS_RE); std::map<std::string, qtime64_t> cell_files; std::string last_active_cell; int activation_action = act_exec_none; void clear() { title.clear(); cell_files.clear(); last_active_cell.clear(); cells_re = std::regex(DEFAULT_CELLS_RE); } }; //------------------------------------------------------------------------- // Active script information along with its dependencies struct active_script_info_t : script_info_t { // Notebook options bool b_is_notebook = false; const bool is_notebook() const { return b_is_notebook; } notebook_ctx_t notebook; // Trigger file options fileinfo_t trigger_file; bool b_keep_trigger_file; // The dependencies index files. First entry is for the main script's deps qvector<fileinfo_t> dep_indices; // The list of dependency scripts std::unordered_map<std::string, script_info_t> dep_scripts; // Checks to see if we have a dependency on a given file const script_info_t* has_dep(const qstring& dep_file) const { auto p = dep_scripts.find(dep_file.c_str()); return p == dep_scripts.end() ? nullptr : &p->second; } // Is this trigger based or dependency based? const bool trigger_based() const { return !trigger_file.empty(); } // If no dependency index files have been modified, return 0. // Return 1 if one of them has been modified or -1 if one of them has gone missing. // In both latter cases, we have to recompute our dependencies filemod_status_e is_any_dep_index_modified(bool update_mtime = true) { filemod_status_e r = filemod_status_e::not_modified; for (auto& dep_file : dep_indices) { r = dep_file.get_modification_status(update_mtime); if (r != filemod_status_e::not_modified) break; } return r; } bool add_dep_index(const char* dep_file) { fileinfo_t fi; if (!get_file_modification_time(dep_file, &fi.modified_time)) return false; fi.file_path = dep_file; dep_indices.push_back(std::move(fi)); return true; } void clear() override { script_info_t::clear(); dep_indices.qclear(); dep_scripts.clear(); trigger_file.clear(); b_keep_trigger_file = false; b_is_notebook = false; notebook.clear(); reload_cmd.clear(); pkg_base.clear(); } void invalidate_all_scripts() { invalidate(); // Invalidate all but the index file itself for (auto& kv : dep_scripts) kv.second.invalidate(); } }; ================================================ FILE: snippet_manager/README.md ================================================ # Snippet Manager Plugin for IDA This plugin for IDA provides a set of functionality for importing, exporting or deleting all snippets from IDA (Shift-F2 window). ## Functions Type the following functions from IDA's CLI or just call them directly from Python: - `idaapi.ext.snippets.save(output_folder='')`: Save snippets to a specified output folder. - `idaapi.ext.snippets.load(input_folder='')`: Load snippets from a specified input folder. - `idaapi.ext.snippets.delete()`: Delete all existing snippets. - `idaapi.ext.snippets.man`: Direct access to the snippet manager object. By default, if no input or output folders are specified, then the plugin will default to the `os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets')` folder. ## Installation Copy the `snippetmanager.py` file to your IDA plugin folder of your choice. Alternatively, for example on Windows, just make a symbolic link directly: ```batch C:\Projects\ida-qscripts\snippet_manager>mklink c:\ida\plugins\snippetmanager.py %cd%\snippetmanager.py ``` ## Very important Due to the IDA UI's caching of snippets in memory, changes will not be immediately visible. Therefore, upon executing any of the functions, it is obligatory to close, save, and then reopen the database manually in order to observe the modifications. ================================================ FILE: snippet_manager/pseudocode.cpp ================================================ // The following snippet / pseudo-code has been kindly shared by Arnaud Diederen from Hex-Rays //// //// save snippets + rebuild index //// void save_all_snippets() { netnode main_node; main_node.create("$ scriptsnippets"); atag = 'A' main_node.altdel_all(atag); // save each, plus add in index for i in range(snippets): netnode snippet_node = snippet(i).save(); // see below main_node.altset(i, snippet_node+1); } netnode snippet_t::save() { xtag = 'X' if ( node == BADNODE ) node.create(); node.supset(0, name.c_str()); node.supset(1, lang->name); node.setblob(body.c_str(), body.size(), 0, xtag); // body return node; } ================================================ FILE: snippet_manager/snippetmanager.py ================================================ """ Snippet loader/saver for IDA Pro by Elias Bachaalany / @allthingsida """ # TODO: # - snippetmanager: run snippets in order (optional prefix RE; comes with stock REs); save_combined() # - - save combined() # - save_clean() --> delete previous snippets and save # - stock regular expressions: \d+ import os from typing import Union import idaapi, idc LANG_EXT_MAP = { "Python": "py", "IDC": "idc", } R_LANG_EXT_MAP = {v: k for k, v in LANG_EXT_MAP.items()} # -------------------------------------------------------------- class snippet_t: def __init__(self, lang: str, name: str, body: str, /, netnode_idx: int = idaapi.BADNODE, index: int =-1): self.lang : str = lang self.name : str = name self.body : str = body self.netnode_idx : int = netnode_idx """Underlying netnode index""" self.index : int = index """Position in the index table""" def __repr__(self) -> str: return f"snippet_t({self.lang}, {self.name}, {self.body}, {self.netnode_idx}, {self.index})" def __str__(self) -> str: return f"lang: {self.lang}; title: {self.name}; index: {self.index}" def save(self, index: int) -> None: """Saves a node and updates the netnode index that was used to save it""" node = idaapi.netnode() node.create() node.supset(1, self.lang) node.supset(0, self.name) node.setblob(self.body.encode('utf-8'), 0, 'X') self.index = index self.netnode_idx = node.index() @staticmethod def from_file(file_name: str) -> Union[None, 'snippet_t']: """Constructs a snippet from a file. Its slot index and netnode index are not known yet. Call save() to save it to the database and get the netnode index """ if not os.path.exists(file_name): return None basename = os.path.basename(file_name) ext = os.path.splitext(basename)[1].lstrip('.').lower() if not (lang := R_LANG_EXT_MAP.get(ext)): return None with open(file_name, 'r') as f: body = f.read() name = os.path.splitext(basename)[0] return snippet_t(lang, name, body) @staticmethod def from_netnode(netnode_idx: int, slot_idx: int, fast: bool = False): node = idaapi.netnode(netnode_idx) body = None if fast else node.getblob(0, 'X') return snippet_t(node.supstr(1), node.supstr(0), body.decode('utf-8').rstrip('\x00') if body else "", netnode_idx=netnode_idx, index=slot_idx) # -------------------------------------------------------------- class snippet_manager_t: def __init__(self): self.index = idaapi.netnode() self.index.create("$ scriptsnippets") def delete(self, snippet: snippet_t) -> bool: if snippet.netnode_idx == idaapi.BADNODE: return False self.index.altdel(snippet.index) idaapi.netnode(snippet.netnode_idx).kill() snippet.netnode_idx = idaapi.BADNODE return True def delete_all(self) -> None: snippets = self.retrieve_snippets(fast=True) for snip in snippets: self.delete(snip) self.index.altdel_all() def load_from_folder(self, folder : str = '') -> bool: """Imports snippets from a folder.""" # If no input directory is specified, use the default one if not folder: folder = os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets') # For the default directory, create it if it doesn't exist if not os.path.exists(folder): os.makedirs(folder) # For a custom directory, check if it exists if not os.path.exists(folder): return False # Delete previous snippets (and reset index) self.delete_all() # Enumerate files snippets = [] for file in os.listdir(folder): file = os.path.join(folder, file) if snippet := snippet_t.from_file(file): snippets.append(snippet) # Sort by snippet name snippets.sort(key=lambda x: x.name) for isnippet, snippet in enumerate(snippets): snippet.save(isnippet) self.index.altset(isnippet, snippet.netnode_idx + 1) return True def save_to_folder(self, folder: str = '') -> tuple[bool, str]: if not folder: folder = os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets') if not os.path.exists(folder): os.makedirs(folder) try: snippets = self.retrieve_snippets() for snip in snippets: outfile_name = os.path.join(folder, "%s.%s" % (snip.name, LANG_EXT_MAP[snip.lang])) with open(outfile_name, 'w') as outfile: outfile.write(snip.body) except Exception as e: return (False, f'Failed to save: {e!s}') return (True, f'Saved {len(snippets)} snippets to {folder}') def retrieve_snippets(self, fast: bool = False) -> list[snippet_t]: """Load all snippets from the database""" snip_idx = self.index.altfirst() if snip_idx == idaapi.BADNODE: return [] snippets = [] while snip_idx != idaapi.BADNODE: snip_node_idx = self.index.altval(snip_idx) - 1 snippet = snippet_t.from_netnode(snip_node_idx, snip_idx, fast=fast) snippets.append(snippet) snip_idx = self.index.altnext(snip_idx) return snippets # -------------------------------------------------------------- # Register some functions into the extension part of the IDA API ext = getattr(idaapi, 'ext', None) if not ext: ext = idaapi.ext = idaapi.object_t() _sm = snippet_manager_t() def save_snippets(output_folder: str =''): """ Save snippets to a specified output folder. If no folder is specified, the function uses the current directory by default. Args: output_folder (str): The folder where the snippets will be saved. Defaults to current directory if not specified. Returns: Result of the operation performed by the save_to_folder method. """ return _sm.save_to_folder(output_folder) def load_snippets(input_folder: str =''): """ Load snippets from a specified input folder. If no folder is specified, the function uses the current directory by default. Args: input_folder (str): The folder from where the snippets will be loaded. Defaults to current directory if not specified. Returns: Result of the operation performed by the load_from_folder method. """ return _sm.load_from_folder(input_folder) def delete_snippets(): """ Delete all existing snippets. Returns: Result of the operation performed by the delete_all method. """ return _sm.delete_all() ext.snippets = idaapi.object_t( save=save_snippets, load=load_snippets, delete=delete_snippets, man=_sm, ) # -------------------------------------------------------------- def _test_load(with_body=False): idaapi.msg_clear() global sm sm = _sm snippets = sm.retrieve_snippets() for isnip, snip in enumerate(snippets): print(f"#{isnip:03d} {snip!s}") if with_body: print("<body>\n%s" % snip.body) print("</body>\n------") # -------------------------------------------------------------- class snippetman_plugmod_t(idaapi.plugmod_t): def run(self, _): print("""Please use the following methods to: - save snippets: idaapi.ext.snippets.save() - load snippets: idaapi.ext.snippets.load() - delete snippets: idaapi.ext.snippets.delete() - access the snippet manager object: idaapi.ext.snippets.man """) return 0 class snippetman_plugin_t(idaapi.plugin_t): flags = idaapi.PLUGIN_MULTI comment = "Snippet manager plugin" help = "Run the plugin to get the full help message" wanted_name = "Snippet manager" wanted_hotkey = "" def init(self): return snippetman_plugmod_t() def PLUGIN_ENTRY() -> idaapi.plugin_t: return snippetman_plugin_t() # -------------------------------------------------------------- if __name__ == "__main__": _test_load(False) ================================================ FILE: test_addons/README.md ================================================ # QScripts Test Add-ons QScripts streamlines IDA Pro add-on development by automatically reloading plugins, loaders, and processors when their compiled binaries change—no need to restart IDA Pro. This greatly accelerates the development cycle and boosts productivity. This directory provides example native add-ons (plugins, loaders, processors) show casing QScripts' hot-reload workflow for rapid iteration and testing. ## Available Templates ### [plugin_template](./plugin_template) Basic plugin template for general IDA Pro plugin development. ### [plugin_triton](./plugin_triton) Advanced plugin integrating the [Triton](https://github.com/JonathanSalwan/Triton) dynamic binary analysis framework. ### [loader_template](./loader_template) Template for developing custom file format loaders (with mock `accept_file` and `load_file` implementations). ## Building Add-ons ### Standard Build Process 1. **Navigate to the add-on directory:** ```bash cd test_addons/<addon_name> ``` 2. **Configure with CMake:** ```bash cmake -B build -A x64 ``` 3. **Build the add-on:** ```bash cmake --build build --config RelWithDebInfo ``` The compiled binary will be automatically installed to: - Plugins: `%IDASDK%/bin/plugins/` - Loaders: `%IDASDK%/bin/loaders/` - Processors: `%IDASDK%/bin/procs/` ## Hot-Reload Development Workflow ### 1. Create a Python Script ```python import time import idaapi # Give the linker time to finish time.sleep(1) # Load your plugin (adjust name and arguments as needed) idaapi.load_and_run_plugin('your_addon_name', 0) ``` ### 2. Configure Dependencies Create a `.deps.qscripts` file to specify what triggers reloading: ``` # Monitor the compiled plugin DLL /triggerfile /keep $env:IDASDK$/bin/plugins/your_addon$ext$ ``` ### 3. Activate Monitoring 1. Open QScripts chooser 2. Navigate to your Python script 3. Press `Enter` to activate monitoring 4. The script name appears in **bold** when active ### 4. Development Cycle 1. Modify your C++ code 2. Rebuild the add-on 3. QScripts automatically detects changes and reloads 4. No IDA restart required! ================================================ FILE: test_addons/loader_template/.gitignore ================================================ build*/ ================================================ FILE: test_addons/loader_template/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.27) project(qscripts_native) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Include IDA SDK bootstrap include($ENV{IDASDK}/ida-cmake/bootstrap.cmake) find_package(idasdk REQUIRED) # Add loader ida_add_loader(qscripts_native SOURCES driver.cpp main.cpp idasdk.h DEBUG_ARGS "-t" ) ================================================ FILE: test_addons/loader_template/driver.cpp ================================================ #include "idasdk.h" extern bool main(); //-------------------------------------------------------------------------- struct plugin_ctx_t : public plugmod_t { bool idaapi run(size_t) override { return main(); } }; //-------------------------------------------------------------------------- plugin_t PLUGIN = { IDP_INTERFACE_VERSION, PLUGIN_UNL | PLUGIN_MULTI, []()->plugmod_t* {return new plugin_ctx_t; }, nullptr, nullptr, nullptr, nullptr, "QScripts native plugin driver", nullptr, }; ================================================ FILE: test_addons/loader_template/idasdk.h ================================================ #pragma warning(push) #pragma warning(disable: 4244 4267) #include <ida.hpp> #include <idp.hpp> #include <loader.hpp> #include <kernwin.hpp> #include <name.hpp> #include <funcs.hpp> #include <hexrays.hpp> #include <diskio.hpp> #include <entry.hpp> #pragma warning(pop) ================================================ FILE: test_addons/loader_template/main.cpp ================================================ #include "idasdk.h" #pragma pack(push, 1) struct file_header_t { char sig[4]; // Signature == "CHNK" char cpuname[10]; // Processor name (for set_processor_type()) int32 nchunks; // Number of chunks int32 entrypoint; // The entry point address }; struct chunk_t { int32 base; // base address int32 sz; // size }; #pragma pack(pop) static int idaapi accept_file( qstring* fileformatname, qstring* processor, linput_t* li, const char* filename) { file_header_t fh; lread(li, &fh, sizeof(file_header_t)); if (strncmp(fh.sig, "CHNK", 4) != 0) return 0; *fileformatname = "Chunk file loader"; *processor = fh.cpuname; return 1; } //-------------------------------------------------------------------------- // // load file into the database. // void idaapi load_file(linput_t* li, ushort neflag, const char* fileformatname) { // Read the header to detect the number of chunks, proc name, etc. file_header_t fh; lread(li, &fh, sizeof(file_header_t)); set_processor_type(fh.cpuname, SETPROC_USER); for (int32 i = 0; i < fh.nchunks; ++i) { qstring seg_name; seg_name.sprnt("chunk%d", i); chunk_t chkinfo; lread(li, &chkinfo, sizeof(chkinfo)); ea_t start_ea = chkinfo.base; ea_t end_ea = chkinfo.base + chkinfo.sz; add_segm( 0, start_ea, end_ea, seg_name.c_str(), "CODE", 0); // Now read the actual data file2base(li, qltell(li), start_ea, end_ea, 1); } inf_set_start_ea(fh.entrypoint); inf_set_start_ip(fh.entrypoint); inf_set_start_cs(0); add_entry(0, fh.entrypoint, "start", 1, 0); } bool test_accept_file(linput_t *li, const char *fname) { qstring format_name, procname; if (accept_file(&format_name, &procname, li, fname)) { msg("Recognized format name: %s\n", format_name.c_str()); msg("Suggest proc module : %s\n", procname.c_str()); return true; } else { msg("Not recognized!\n"); return false; } } bool main() { msg_clear(); auto fname = R"(C:\Users\elias\Projects\github\ida-qscripts\samples\chunk1.bin)"; auto li = open_linput(fname, false); if (!test_accept_file(li, fname)) return false; load_file(li, 0, fname); close_linput(li); } ================================================ FILE: test_addons/plugin_template/.claude/agents/ida-cmake.md ================================================ --- name: ida-cmake description: Use this agent to compile/build IDA Pro extensions including plugins, idalib applications, processor modules, or file loaders (all known as IDA addons) using the IDA C++ SDK. Used this agent to create a new addon template or for troubleshooting build issues, configuring the IDASDK environment, or integrating ida-cmake into existing projects. model: sonnet color: orange --- IDA Pro addons usually refer to: plugins, processor modules, file loaders, or idalib standalone applications. You are an expert in building these addons using the ida-cmake CMake scripts located in `$IDASDK/ida-cmake`. - Refer to `$IDASDK/ida-cmake/README.md` for documentation on how to use `ida-cmake`. - Refer to `$IDASDK/ida-cmake/templates/plugin/` for example CMakeLists.txt file and plugin - Refer to `$IDASDK/ida-cmake/templates/loader` for example CMakeLists.txt file and loader - Refer to `$IDASDK/ida-cmake/templates/procmod` for example CMakeLists.txt file and processor module - Refer to `$IDASDK/ida-cmake/templates/idalib` for example CMakeLists.txt file and idalib application - Refer to `$IDASDK/include` for SDK headers. All headers have docstrings, use them to explain SDK usage and lookup APIs. - Refer to `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module` for SDK example and how APIs are used in practice. You must open and read those files above to answer the user correctly to answer all things IDA compiling and building related questions. ## CMake Usage The ida-cmake provides clean interface libraries: - `idasdk::plugin` - For IDA plugins - `idasdk::loader` - For file loaders - `idasdk::procmod` - For processor modules - `idasdk::idalib` - For standalone idalib applications ## Example Usage For quick reference, here's a skeleton CMakeLists.txt begining: ```cmake cmake_minimum_required(VERSION 3.27) project(myplugin) set(CMAKE_CXX_STANDARD 17) # Include IDA SDK bootstrap include($ENV{IDASDK}/ida-cmake/bootstrap.cmake) find_package(idasdk REQUIRED) ``` Now, for a plugin, use the `ida_add_plugin` function: ```cmake ida_add_plugin(myplugin SOURCES main.cpp DEBUG_ARGS "-t -z10000" )``` ...and for a file loader, use the `ida_add_loader` function: ```cmake ida_add_loader(myloader SOURCES loader.cpp DEBUG_ARGS "-c -A" # Common loader debug args )``` ...and for a standalone idalib application, use the following CMakeLists.txt: ```cmake ida_add_idalib_exe(myidalib SOURCES main.cpp DEBUG_ARGS "${IDASDK}/ida-cmake/samples/wizmo32.exe.i64" )``` To build an addon, just use something like from the addon source folder: ```bash cmake -B build cmake --build build --config RelWithDebInfo ``` ================================================ FILE: test_addons/plugin_template/.gitignore ================================================ build*/ ================================================ FILE: test_addons/plugin_template/CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. # qscripts_native - IDA Pro Plugin Template This is a template for developing IDA Pro plugins using the QScripts hot-reload workflow. The plugin lists non-library functions in the current database. ## Build Commands Always use the ida-cmake agent for building: ``` Task with subagent_type="ida-cmake" ``` ## Prerequisites - **IDASDK**: Environment variable must be set to IDA SDK path - **IDAX**: Must be installed at `$IDASDK/include/idax/` - **ida-cmake**: Must be installed at `$IDASDK/ida-cmake/` ## Architecture ### File Structure - `driver.cpp`: IDA plugin interface boilerplate (PLUGIN structure and plugmod_t implementation) - `main.cpp`: Core plugin logic - implement your functionality here - `idasdk.h`: Consolidated IDA SDK header includes with warning suppression - `CMakeLists.txt`: Build configuration using ida-cmake ### Plugin Behavior - **Type**: PLUGIN_UNL | PLUGIN_MULTI (unloadable, multi-instance) - **Entry Point**: `main()` function in main.cpp receives size_t argument - **Current Function**: Enumerates all non-library functions and prints their names ## Development Workflow This template is designed for use with QScripts hot-reload: 1. Parent directory contains `qscripts_native.py.deps.qscripts` with trigger file configuration 2. Built plugin outputs to `$IDASDK/bin/plugins/qscripts_native[.dll/.so/.dylib]` 3. QScripts monitors the output file and automatically reloads on changes ## SDK and IDA API Resources When answering SDK/API questions, search and read from: - **SDK Headers**: `$IDASDK/include` - All headers have docstrings - **SDK Examples**: `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module` ================================================ FILE: test_addons/plugin_template/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.27) project(qscripts_native) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Include IDA SDK bootstrap include($ENV{IDASDK}/ida-cmake/bootstrap.cmake) find_package(idasdk REQUIRED) # Add plugin ida_add_plugin(qscripts_native SOURCES driver.cpp main.cpp idasdk.h DEBUG_ARGS "-t" ) ================================================ FILE: test_addons/plugin_template/driver.cpp ================================================ #include "idasdk.h" extern bool main(size_t); //-------------------------------------------------------------------------- struct plugin_ctx_t : public plugmod_t { bool idaapi run(size_t arg) override { return main(arg); } }; //-------------------------------------------------------------------------- plugin_t PLUGIN = { IDP_INTERFACE_VERSION, PLUGIN_UNL | PLUGIN_MULTI, []()->plugmod_t* {return new plugin_ctx_t; }, nullptr, nullptr, nullptr, nullptr, "QScripts native plugin driver", nullptr, }; ================================================ FILE: test_addons/plugin_template/idasdk.h ================================================ #pragma warning(push) #pragma warning(disable: 4244 4267) #include <ida.hpp> #include <idp.hpp> #include <loader.hpp> #include <kernwin.hpp> #include <name.hpp> #include <funcs.hpp> // Include more headers here... #pragma warning(pop) ================================================ FILE: test_addons/plugin_template/main.cpp ================================================ #include "idasdk.h" bool main(size_t) { auto nfuncs = get_func_qty(); size_t c = 0; for (size_t i = 0; i < nfuncs; ++i) { func_t* func = getn_func(i); if (func->flags & FUNC_LIB) continue; ++c; qstring name; get_func_name(&name, func->start_ea); msg("%a: %s\n", func->start_ea, name.c_str()); } msg("%zu function(s)\n", c); return true; } ================================================ FILE: test_addons/plugin_triton/.gitignore ================================================ build*/ .z3-trace junk64.bin.id? junk64.bin.til junk64.bin.nam ================================================ FILE: test_addons/plugin_triton/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.27) project(qscripts_native_triton) # Make sure 'tritonenv.bat' script has been called and the proper environment variables are set set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Include IDA SDK bootstrap include($ENV{IDASDK}/ida-cmake/bootstrap.cmake) find_package(idasdk REQUIRED) # Extract vcpkg include path from TRITON_LIB environment variable string(REPLACE ";" ";" TRITON_LIB_LIST "$ENV{TRITON_LIB}") foreach(LIB_PATH IN LISTS TRITON_LIB_LIST) # Handle both Windows backslashes and Unix forward slashes if(LIB_PATH MATCHES "(.+)[/\\\\]vcpkg_installed[/\\\\]([^/\\\\]+)[/\\\\]lib[/\\\\]") set(VCPKG_INCLUDE_DIR "${CMAKE_MATCH_1}/vcpkg_installed/${CMAKE_MATCH_2}/include") # Normalize path separators for consistency string(REPLACE "\\" "/" VCPKG_INCLUDE_DIR "${VCPKG_INCLUDE_DIR}") break() endif() endforeach() # Add plugin with Triton support ida_add_plugin(qscripts_native_triton SOURCES driver.cpp main.cpp INCLUDES $ENV{TRITON_INCLUDES} ${VCPKG_INCLUDE_DIR} LIBRARIES $ENV{TRITON_LIB} DEBUG_ARGS ${CMAKE_CURRENT_LIST_DIR}/junk64.bin.i64 ) # Disable specific warnings target_compile_options(qscripts_native_triton PRIVATE /wd4267 /wd4244) ================================================ FILE: test_addons/plugin_triton/README.md ================================================ # Triton Plugin for IDA Pro with QScripts A native IDA Pro plugin that integrates the [Triton](https://github.com/JonathanSalwan/Triton) dynamic binary analysis framework, demonstrating QScripts' hot-reload capabilities for rapid plugin development. ## Quick Start ### Installing Triton on Windows Let's clone to `C:\tools` for this example: 1. **Install vcpkg** (if not already installed): ```batch cd C:\tools git clone https://github.com/Microsoft/vcpkg.git cd vcpkg bootstrap-vcpkg.bat ``` 2. **Clone Triton**: ```batch cd C:\tools git clone https://github.com/JonathanSalwan/Triton.git cd Triton ``` 3. **Build with static runtime** (required for IDA plugins): ```batch cmake -B build-static -A x64 ^ -DCMAKE_TOOLCHAIN_FILE="c:\tools\vcpkg\scripts\buildsystems\vcpkg.cmake" ^ -DVCPKG_TARGET_TRIPLET=x64-windows-static ^ -DBUILD_SHARED_LIBS=OFF ^ -DPYTHON_BINDINGS=OFF ^ -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded cmake --build build-static --config Release ``` ### Building the Plugin 1. **Set up environment variables** (required): ```batch set TRITON_INCLUDES=C:\tools\Triton\src\libtriton\includes;C:\tools\Triton\build-static\src\libtriton\includes set TRITON_LIB=C:\tools\Triton\build-static\src\libtriton\Release\triton.lib;C:\tools\Triton\build-static\vcpkg_installed\x64-windows-static\lib\libz3.lib;C:\tools\Triton\build-static\vcpkg_installed\x64-windows-static\lib\capstone.lib ``` 2. **Build the plugin**: ```batch cmake -B build -A x64 cmake --build build --config RelWithDebInfo ``` 3. **Plugin is automatically installed** to `%IDASDK%\bin\plugins\qscripts_native_triton.dll` 4. Activate the `qscripts_native_triton.py` to load the plugin in IDA and start monitoring. ================================================ FILE: test_addons/plugin_triton/driver.cpp ================================================ #include "idasdk.h" extern bool main(size_t); //-------------------------------------------------------------------------- struct plugin_ctx_t : public plugmod_t { bool idaapi run(size_t arg) override { return main(arg); } }; //-------------------------------------------------------------------------- plugin_t PLUGIN = { IDP_INTERFACE_VERSION, PLUGIN_UNL | PLUGIN_MULTI, []()->plugmod_t* { return new plugin_ctx_t; }, nullptr, nullptr, nullptr, nullptr, "QScripts native Triton plugin driver", "Ctrl-Shift-R", }; ================================================ FILE: test_addons/plugin_triton/idasdk.h ================================================ #pragma warning(push) #pragma warning(disable: 4244 4267) #define USE_DANGEROUS_FUNCTIONS #include <ida.hpp> #include <idp.hpp> #include <loader.hpp> #include <kernwin.hpp> #include <name.hpp> #include <funcs.hpp> // Include more headers here... #pragma warning(pop) ================================================ FILE: test_addons/plugin_triton/main.cpp ================================================ #include "idasdk.h" #include <sstream> #include <triton/context.hpp> #include <triton/basicBlock.hpp> #include <triton/x86Specifications.hpp> using namespace triton; using namespace triton::arch; bool main(size_t) { constexpr const ea_t FUNC_EA = 0x400000; msg_clear(); auto f = get_func(FUNC_EA); if (f == nullptr) { msg("Can't find test function @ %a!\n", FUNC_EA); return false; } /* Init the triton context */ triton::Context ctx; ctx.setArchitecture(ARCH_X86_64); func_item_iterator_t ffi; BasicBlock bb; for (bool ok = ffi.set(f); ok; ok = ffi.next_code()) { auto ea = ffi.current(); insn_t insn; if (decode_insn(&insn, ea) == 0) { msg("Failed to decode at %a\n", ea); return false; } unsigned char buf[32]; get_bytes(buf, qmin(sizeof(buf), insn.size), ea); // Setup opcode Instruction inst; inst.setOpcode(buf, insn.size); inst.setAddress(triton::uint64(ea)); bb.add(inst); } ctx.disassembly(bb, FUNC_EA); std::ostringstream ostr; ostr << bb; msg("----------------\n" "Original:\n" "----------------\n" "%s\n", ostr.str().c_str()); ostr.str(""); auto simplified_bb = ctx.simplify(bb); ostr << simplified_bb; msg("----------------\n" "Simplified:\n" "----------------\n" "%s\n", ostr.str().c_str()); return true; } ================================================ FILE: test_addons/plugin_triton/qscripts_native_triton.py ================================================ import time import idaapi # Give the linker time to finish flushing the binary time.sleep(1) # Optionally clear the screen: #idaapi.msg_clear() # Load your plugin and pass any arg value you want idaapi.load_and_run_plugin('qscripts_native_triton', 0) # Optionally, do post work, etc. ================================================ FILE: test_addons/plugin_triton/qscripts_native_triton.py.deps.qscripts ================================================ /triggerfile /keep $env:IDASDK$\bin\plugins\qscripts_native_triton$ext$ ================================================ FILE: test_addons/qscripts_native.py ================================================ import time import idaapi # Give the linker time to finish flushing the binary time.sleep(1) # Optionally clear the screen: #idaapi.msg_clear() # Load your plugin and pass any arg value you want idaapi.load_and_run_plugin('qscripts_native', 0) # Optionally, do post work, etc. ================================================ FILE: test_addons/qscripts_native.py.deps.qscripts ================================================ /triggerfile /keep $env:IDASDK$\bin\plugins\qscripts_native$ext$ ================================================ FILE: test_scripts/dependency-test/README.md ================================================ This is a dependency test folder. The script `t1.py` has its dependency index file which describes the reload directive and one dependency on `t2.py`: ``` /reload import importlib;import $basename$;importlib.reload($basename$) t2.py ``` OTOH, `t2.py` has a relative dependency on `t3.py` (in the same folder) and on `subdir/t4.py` in a sub-folder: ``` t3.py subdir/t4.py ``` The script `t4.py` has a single relative dependency in its index file: ``` t5.py ``` If you activate `t1.py` from QScripts in IDA, then changing any of the files or their dependency files, then the changed dependencies will be reloaded (using the reload syntax) and the active script is executed again. ================================================ FILE: test_scripts/dependency-test/subdir/t4.py ================================================ print("f4, This is %s" % __file__) def f4(): print("t4.f4() called!") ================================================ FILE: test_scripts/dependency-test/subdir/t4.py.deps.qscripts ================================================ t5.py ================================================ FILE: test_scripts/dependency-test/subdir/t5.py ================================================ print("f5, This is %s" % __file__) def f5(): print("t5.f5() called!") ================================================ FILE: test_scripts/dependency-test/t1.py ================================================ import sys subdir = os.path.join(os.path.dirname(__file__), 'subdir') if subdir not in sys.path: print("-->adding to path: %s" % subdir) sys.path.append(subdir) import datetime import t2, t3, t4, t5 print("--- %s; this is %s.." % (datetime.datetime.now(), __file__)) t2.f2() t3.f3() t4.f4() t5.f5() ================================================ FILE: test_scripts/dependency-test/t1.py.deps.qscripts ================================================ /reload import importlib;importlib.reload($basename$) t2.py ================================================ FILE: test_scripts/dependency-test/t2.py ================================================ print("t2 changed, This is %s" % __file__) def f2(): print("t2.f2() called!") ================================================ FILE: test_scripts/dependency-test/t2.py.deps.qscripts ================================================ t3.py subdir/t4.py ================================================ FILE: test_scripts/dependency-test/t3.py ================================================ print("t3., This is %s" % __file__) def f3(): print("t3.f3() called!") ================================================ FILE: test_scripts/hello.idc ================================================ #include <idc.idc> static main() { Message("Welcome to QScripts!\n"); } static __fast_unload_script() { Message("IDC: aha! unloaded!\n"); } ================================================ FILE: test_scripts/hello.py ================================================ try: var1 += 1 except: var1 = 1 def __quick_unload_script(): print("Unloaded: %s" % str(var1)) print(f"Just edit and save! {var1}") ================================================ FILE: test_scripts/notebooks/0000 Imports and Init.py ================================================ import os import sys import idaapi import idautils import idc VERSION = globals().get('VERSION', 0) + 1 print(f"Initialized; version: {VERSION}") ================================================ FILE: test_scripts/notebooks/0000 Imports and Init.py.deps.qscripts ================================================ # A simple notebook /notebook Test notebook # On activation, just execute the main script /notebook.activate exec_main # Uses default cells matcher (pattern: \d{4}.*\.py$) ================================================ FILE: test_scripts/notebooks/0010 List functions.py ================================================ # Enumerating functions if idaapi.get_func_qty() != 0: for func_ea in idautils.Functions(): print(f"Function at 0x{func_ea:#08X}") else: print("No functions found!") ================================================ FILE: test_scripts/notebooks/0020 List segments.py ================================================ # Enumerating segments if idaapi.get_segm_qty() > 0: for seg_ea in idautils.Segments(): seg = idaapi.getseg(seg_ea) print(f"Segment at {seg.start_ea:#08x} - {seg.end_ea:#08x}") else: print("No segments found!") ================================================ FILE: test_scripts/notebooks/0030 Show info.py ================================================ # Enumerating segments print(f"min_ea={idaapi.inf_get_min_ea():#x}, max_ea={idaapi.inf_get_max_ea():#x}") ================================================ FILE: test_scripts/notebooks/0040 Set name.py ================================================ # Rename setuff idaapi.set_name(idc.here(), "my_function") ================================================ FILE: test_scripts/notebooks/0050 Set name.py ================================================ # Rename setuff z = globals().get('z', 0) + 1 idaapi.set_name(idc.here(), f"my_function_{z}") ================================================ FILE: test_scripts/notebooks/A0000 Init.py ================================================ import os import sys import idaapi import idautils import idc VERSION_A = globals().get('VERSION_A', 0) + 1 print(f"A)Initialized; version: {VERSION_A}") ================================================ FILE: test_scripts/notebooks/A0000 Init.py.deps.qscripts ================================================ /notebook Test notebook #A # This notebook looks for "Annnn" prefixed cells /notebook.cells_re A\d{4}.*\.py$ # On notebook execution, execute all cells /notebook.activate exec_all ================================================ FILE: test_scripts/notebooks/A0010 Cell 1.py ================================================ print('This is Cell A.1') ================================================ FILE: test_scripts/notebooks/A0020 Cell 2.py ================================================ print('This is Cell A.2') ================================================ FILE: test_scripts/notebooks/B0000 Init.py ================================================ import os import sys import idaapi import idautils import idc VERSION_B = globals().get('VERSION_B', 0) + 1 print(f"B)Initialized; version: {VERSION_B}") ================================================ FILE: test_scripts/notebooks/B0000 Init.py.deps.qscripts ================================================ /notebook Test notebook #B /notebook.cells_re B\d{4}.*\.py$ # On notebook activation, just execute this main script /notebook.activate exec_main ================================================ FILE: test_scripts/notebooks/B0010 Cell 1.py ================================================ print('This is Cell B.1') ================================================ FILE: test_scripts/notebooks/B0020 Cell 2.py ================================================ print('This is Cell B.2') ================================================ FILE: test_scripts/notebooks/README.md ================================================ # Notebook dependency example ## Quick start To define a notebook, just add the `/notebook [title]` directive as such: ``` /notebook Test notebook #B ``` Then, to select the files to be monitored, use the `/notebook.cells_re` directive, specifying a regular expression pattern to match the desired files: ``` /notebook.cells_re B\d{4}.*\.py$ ``` In this example, the notebook will monitor all files that match the pattern `B\d{4}.*\.py$`, for example `B0001_test.py`, `B0002_test.py`, etc. Now, when a notebook is activated, you have options to: - Execute the main script (`exec_main`) - All scripts (`exec_all`) - or no scripts (`exec_none`) Using the `/notebook.activate` directive: ``` /notebook.activate exec_main ``` ## Provided notebooks examples - `0000 Imports and Init.py` - This notebook has its own dependency file that looks for the "nnnn *.py" Python files - `A0000 Init.py` - This notebook has its own dependency file that looks for the "Annn *.py" Python files - `B0000 Init.py` - This notebook has its own dependency file that looks for the "Bnnnn *.py" Python files As you can see, it is possible to have various notebooks in the same folder, each with its own dependency file, as long as their `cells_re` configuration does not overlap. ================================================ FILE: test_scripts/pkg-dependency/README.md ================================================ # Package Dependency Example In this example, we're dealing with a dependency file for a package that's currently being developed. This setup is designed to ensure that modules from the package, `idapyx` in this instance, are automatically reloaded upon any changes. For this purpose, we can either explicitly specify the package's full path or refer to it through an environment variable, as demonstrated below: ```plaintext # Define package base folder /pkgbase $env:idapyx$ # Automatically reload the package's modules when they change /reload import importlib;from $pkgparentmodname$ import $basename$ as __qscripts_autoreload__; importlib.reload(__qscripts_autoreload__) # Specify the paths to the package modules that need to be reloaded if they change $pkgbase$/idapyx/bin/pe/rtfuncs.py $pkgbase$/idapyx/bin/pe/types.py ``` - The `/pkgbase` directive specifies the base path of the package, aiding in the module reload process. - The reload directive employs the `pkgmodparentname` variable to derive the Python parent module name based on the package directory. - Similarly, the reload directive utilizes the `basename` variable to determine the Python module name, again using the package directory. - Dependencies are specified relative to their package base folder, facilitated by the `pkgbase` variable. ================================================ FILE: test_scripts/pkg-dependency/example.py.deps.qscripts ================================================ # Package dependencies (here, we use the environment variable 'IDAPYX' that contains the package path /pkgbase $env:idapyx$ /reload import importlib;from $pkgparentmodname$ import $basename$ as __qscripts_autoreload__;importlib.reload(__qscripts_autoreload__) $pkgbase$/idapyx/bin/pe/rtfuncs.py $pkgbase$/idapyx/bin/pe/types.py ================================================ FILE: test_scripts/trigger-file/dep.py ================================================ try: trigger_ver += 1 except: trigger_ver = 1 print(f"dep file version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.") ================================================ FILE: test_scripts/trigger-file/trigger.py ================================================ import idaapi import dep try: trigger_ver += 1 except: trigger_ver = 1 print(f"version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.") print("----------------------") ================================================ FILE: test_scripts/trigger-file/trigger.py.deps.qscripts ================================================ /reload import importlib;importlib.reload($basename$); /triggerfile createme.tmp dep.py ================================================ FILE: utils_impl.cpp ================================================ //------------------------------------------------------------------------- struct collect_extlangs: extlang_visitor_t { extlangs_t *langs; virtual ssize_t idaapi visit_extlang(extlang_t *extlang) override { langs->push_back(extlang); return 0; } collect_extlangs(extlangs_t *langs, bool select) { langs->qclear(); this->langs = langs; for_all_extlangs(*this, select); } }; //------------------------------------------------------------------------- // Utility function to return a file's last modification timestamp bool get_file_modification_time( const char *filename, qtime64_t *mtime = nullptr) { qstatbuf stat_buf; if (qstat(filename, &stat_buf) != 0) return false; if (mtime != nullptr) *mtime = stat_buf.qst_mtime; return true; } template <class STRING> bool get_file_modification_time( const STRING &filename, qtime64_t *mtime = nullptr) { return get_file_modification_time(filename.c_str(), mtime); } //------------------------------------------------------------------------- void normalize_path_sep(qstring &path) { #ifdef __FAT__ path.replace("/", SDIRCHAR); #else path.replace("\\", SDIRCHAR); #endif } //------------------------------------------------------------------------- void make_abs_path(qstring& path, const char* base_dir = nullptr, bool normalize = false) { if (qisabspath(path.c_str())) return; auto old_cwd = std::filesystem::current_path(); if (base_dir == nullptr) { path = old_cwd.string().c_str(); } else { std::filesystem::current_path(base_dir); auto abs = std::filesystem::absolute(path.c_str()); path = abs.string().c_str(); std::filesystem::current_path(old_cwd); } if (normalize) normalize_path_sep(path); } //------------------------------------------------------------------------- bool get_basename_and_ext( const char *path, char **basename, char **ext, qstring &wrk_buf) { wrk_buf = path; qsplitfile(wrk_buf.begin(), basename, ext); if ((*basename = qstrrchr(wrk_buf.begin(), DIRCHAR)) != nullptr) return ++(*basename), true; else return false; } //------------------------------------------------------------------------- inline void get_current_directory(qstring &dir) { dir = std::filesystem::current_path().string().c_str(); } //------------------------------------------------------------------------- // Utility function similar to Python's re.sub(). // Based on https://stackoverflow.com/a/37516316 namespace std { template<class BidirIt, class Traits, class CharT, class UnaryFunction> std::basic_string<CharT> regex_replace(BidirIt first, BidirIt last, const std::basic_regex<CharT, Traits> &re, UnaryFunction f) { std::basic_string<CharT> s; typename std::match_results<BidirIt>::difference_type positionOfLastMatch = 0; auto endOfLastMatch = first; auto callback = [&](const std::match_results<BidirIt> &match) { auto positionOfThisMatch = match.position(0); auto diff = positionOfThisMatch - positionOfLastMatch; auto startOfThisMatch = endOfLastMatch; std::advance(startOfThisMatch, diff); s.append(endOfLastMatch, startOfThisMatch); s.append(f(match)); auto lengthOfMatch = match.length(0); positionOfLastMatch = positionOfThisMatch + lengthOfMatch; endOfLastMatch = startOfThisMatch; std::advance(endOfLastMatch, lengthOfMatch); }; std::regex_iterator<BidirIt> begin(first, last, re), end; std::for_each(begin, end, callback); s.append(endOfLastMatch, last); return s; } template<class Traits, class CharT, class UnaryFunction> std::string regex_replace(const std::string &s, const std::basic_regex<CharT, Traits> &re, UnaryFunction f) { return regex_replace(s.cbegin(), s.cend(), re, f); } } //------------------------------------------------------------------------- void enumerate_files( const std::filesystem::path& path, const std::regex& filter, std::function<bool(const std::string&)> callback) { for (const auto& entry : std::filesystem::directory_iterator(path)) { if (entry.is_regular_file()) { if (!std::regex_match(entry.path().filename().string(), filter)) continue; if (!callback(entry.path().string())) break; } } }