├── .gitattributes ├── .github └── workflows │ ├── gradle.yml │ ├── release.yml │ └── test-kernel-config.json ├── .gitignore ├── .idea ├── fileTemplates │ └── GhidraKotlinScript.kt └── runConfigurations │ └── Ghidra_GUI.xml ├── GhidraJupyterKotlin ├── .gitignore ├── Module.manifest ├── build.gradle ├── extension.properties ├── ghidra_scripts │ ├── Dumpx64dbgLabels.kt │ └── HelloWorldScriptKt.kt ├── gradle.properties ├── lib │ └── .gitkeep └── src │ └── main │ ├── java │ ├── GhidraJupyterKotlin │ │ ├── CellContext.java │ │ ├── ConnectionFile.java │ │ ├── GhidraKotlinKernelLaunchable.java │ │ ├── InterruptKernelAction.kt │ │ ├── JupyterKotlinPlugin.java │ │ ├── KernelThread.java │ │ ├── KotlinQtConsoleThread.java │ │ ├── LineReader.java │ │ ├── NotebookProxy.java │ │ ├── NotebookThread.java │ │ ├── ShutDownKernelAction.kt │ │ └── extensions │ │ │ ├── address │ │ │ └── AddressExtensions.kt │ │ │ ├── data │ │ │ └── DataExtensions.kt │ │ │ └── misc │ │ │ └── MiscExtensions.kt │ └── ghidra │ │ └── app │ │ └── script │ │ ├── KotlinCompilerMessageCollector.kt │ │ └── KotlinScriptProvider.java │ └── resources │ └── images │ ├── README.txt │ ├── notebook.png │ └── qtconsole.png ├── LICENSE ├── README.md ├── ghidra_jupyter ├── kernel │ ├── kernel.css │ ├── kernel.js │ ├── kernel.json │ ├── logo-16x16.png │ ├── logo-64x64.png │ ├── wavyline-orange.gif │ └── wavyline-red.gif ├── requirements.txt ├── setup.py └── src │ └── ghidra_jupyter │ ├── __init__.py │ ├── dispatcher.py │ └── installer.py ├── gradle.properties ├── resources ├── notebook-button.svg ├── notebook-logo.svg ├── options.png ├── qtconsole-button.svg └── readme │ ├── buttons.png │ ├── create_notebook.png │ ├── interrupt_demo.png │ ├── menu.png │ ├── notebook.png │ ├── notebook_view.png │ ├── qtconsole.png │ ├── qtconsole_window.png │ └── waiting.png └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | ghidra_jupyter/kernel/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Test 5 | 6 | on: 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 1.21 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 1.21 21 | - uses: er28-0652/setup-ghidra@master 22 | with: 23 | version: "11.3" 24 | 25 | - name: Build Extension 26 | working-directory: ./GhidraJupyterKotlin 27 | run: gradle buildExtension 28 | 29 | - name: Upload built extension as artifact for debugging 30 | uses: actions/upload-artifact@v4 31 | with: 32 | path: ./GhidraJupyterKotlin/dist/*zip 33 | retention-days: 1 34 | 35 | - name: Install Extension 36 | run: unzip ./GhidraJupyterKotlin/dist/*zip -d $GHIDRA_INSTALL_DIR/Ghidra/Extensions 37 | 38 | - name: Test Kernel Startup # Test if the kernel manages to not crash before the timeout terminates it 39 | run: timeout 5s $GHIDRA_INSTALL_DIR/support/launch.sh fg jdk Ghidra '' '' GhidraJupyterKotlin.GhidraKotlinKernelLaunchable .github/workflows/test-kernel-config.json || exit $(($?-124)) 40 | 41 | - name: Install jupyter-console for testing evaluation 42 | run: sudo apt-get install jupyter-console 43 | 44 | - name: Test basic evaluation 45 | run: | 46 | echo "1+1" | jupyter-console --existing=.github/workflows/test-kernel-config.json & 47 | timeout 10s $GHIDRA_INSTALL_DIR/support/launch.sh fg jdk Ghidra '' '' GhidraJupyterKotlin.GhidraKotlinKernelLaunchable .github/workflows/test-kernel-config.json || exit $(($?-124)) 48 | 49 | - name: Test support for .kt Scripts 50 | run: | 51 | $GHIDRA_INSTALL_DIR/support/analyzeHeadless /tmp tmp -readonly -preScript $GITHUB_WORKSPACE/GhidraJupyterKotlin/ghidra_scripts/HelloWorldScriptKt.kt | grep "HelloWorldScriptKt.kt> Hello in Kotlin from null! (GhidraScript)" 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up JDK 1.21 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 1.21 17 | - uses: er28-0652/setup-ghidra@master 18 | with: 19 | version: "11.3" 20 | 21 | - name: Build with Gradle 22 | working-directory: ./GhidraJupyterKotlin 23 | run: gradle buildExtension 24 | 25 | - name: Debug github.ref 26 | run: echo ${{github.ref}} 27 | 28 | - name: Release 29 | uses: softprops/action-gh-release@v2 30 | with: 31 | files: ./GhidraJupyterKotlin/dist/*zip 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/test-kernel-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "shell_port": 38477, 3 | "iopub_port": 58089, 4 | "stdin_port": 33185, 5 | "control_port": 35869, 6 | "hb_port": 51201, 7 | "ip": "127.0.0.1", 8 | "key": "42dcd222-2548fa07db7fec5eaf72e22b", 9 | "transport": "tcp", 10 | "signature_scheme": "hmac-sha256", 11 | "kernel_name": "ghidra-kotlin" 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | gradle/ 3 | *.class 4 | META-INF 5 | ghidra_jupyter/build/ 6 | ghidra_jupyter/dist/ 7 | -------------------------------------------------------------------------------- /.idea/fileTemplates/GhidraKotlinScript.kt: -------------------------------------------------------------------------------- 1 | // SCRIPT DESCRIPTION 2 | //@category Examples 3 | //@toolbar world.png 4 | 5 | import ghidra.app.script.GhidraScript 6 | import GhidraJupyterKotlin.extensions.address.* 7 | import GhidraJupyterKotlin.extensions.data.* 8 | import GhidraJupyterKotlin.extensions.misc.* 9 | 10 | @Suppress("unused") 11 | class ${NAME} : GhidraScript() { 12 | @Throws(Exception::class) 13 | override fun run() { 14 | TODO("Script code goes here") 15 | } 16 | } -------------------------------------------------------------------------------- /.idea/runConfigurations/Ghidra_GUI.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | build/ 3 | dist/ 4 | .gradle/ 5 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/Module.manifest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/Module.manifest -------------------------------------------------------------------------------- /GhidraJupyterKotlin/build.gradle: -------------------------------------------------------------------------------- 1 | // Builds a Ghidra Extension for a given Ghidra installation. 2 | // 3 | // An absolute path to the Ghidra installation directory must be supplied either by setting the 4 | // GHIDRA_INSTALL_DIR environment variable or Gradle project property: 5 | // 6 | // > export GHIDRA_INSTALL_DIR= 7 | // > gradle 8 | // 9 | // or 10 | // 11 | // > gradle -PGHIDRA_INSTALL_DIR= 12 | // 13 | // Gradle should be invoked from the directory of the project to build. Please see the 14 | // application.gradle.version property in /Ghidra/application.properties 15 | // for the correction version of Gradle to use for the Ghidra installation you specify. 16 | 17 | plugins { 18 | id 'org.jetbrains.kotlin.jvm' version "$kotlinVersion" 19 | id 'idea' 20 | } 21 | 22 | 23 | compileKotlin { 24 | kotlinOptions.jvmTarget = "21" 25 | } 26 | 27 | kotlin { 28 | jvmToolchain(21) 29 | } 30 | 31 | configurations { 32 | requiredLibs //.extendsFrom implementation 33 | } 34 | 35 | 36 | repositories { 37 | flatDir { 38 | dirs 'lib' 39 | } 40 | mavenCentral() 41 | 42 | } 43 | 44 | //----------------------START "DO NOT MODIFY" SECTION------------------------------ 45 | def ghidraInstallDir 46 | 47 | if (System.env.GHIDRA_INSTALL_DIR) { 48 | ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR 49 | } 50 | else if (project.hasProperty("GHIDRA_INSTALL_DIR")) { 51 | ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR") 52 | } 53 | 54 | if (ghidraInstallDir) { 55 | apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle" 56 | } 57 | else { 58 | throw new GradleException("GHIDRA_INSTALL_DIR is not defined!") 59 | } 60 | //----------------------END "DO NOT MODIFY" SECTION------------------------------- 61 | 62 | dependencies { 63 | api('org.jetbrains.kotlinx:kotlin-jupyter-kernel:0.12.0-356'){ 64 | // exclude group: "org.slf4j", module: "slf4j-api" 65 | } 66 | // Needed for compiling 67 | // after building they are already included as part of the recursive dependencies of kotlin-jupyter-kernel 68 | api group: 'org.jetbrains.kotlin', name: 'kotlin-compiler-embeddable', version: "$kotlinVersion" 69 | // api group: 'org.json', name: 'json', version: '20210307' 70 | api group: 'org.zeromq', name: 'jeromq', version: '0.6.0' 71 | // implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1" 72 | } 73 | compileKotlin.dependsOn(copyDependencies) 74 | buildExtension.dependsOn(copyDependencies) 75 | 76 | sourceSets { 77 | main { 78 | kotlin { 79 | srcDirs 'ghidra_scripts' 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/extension.properties: -------------------------------------------------------------------------------- 1 | name=@extname@ 2 | description=Kotlin Jupyter kernel for Ghidra. 3 | author=GhidraJupyter 4 | createdOn=2020-12-06 5 | version=@extversion@ 6 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/ghidra_scripts/Dumpx64dbgLabels.kt: -------------------------------------------------------------------------------- 1 | // Generate a x64dbg script based on the currentProgram that labels all the functions in x64dbg and stores it in the clipboard 2 | //@category Debugger 3 | 4 | import ghidra.app.script.GhidraScript 5 | import GhidraJupyterKotlin.extensions.misc.* 6 | import java.awt.Toolkit 7 | import java.awt.datatransfer.StringSelection 8 | 9 | 10 | @Suppress("unused") 11 | class Dumpx64dbgLabels : GhidraScript() { 12 | @Throws(Exception::class) 13 | override fun run() { 14 | currentProgram.functions 15 | .map { f -> "lblset 0x${f.entryPoint.offset}, ${f.name}"} 16 | .joinToString("\n") 17 | .let { 18 | val sel = StringSelection(it) 19 | Toolkit.getDefaultToolkit().systemClipboard.setContents(sel, sel) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /GhidraJupyterKotlin/ghidra_scripts/HelloWorldScriptKt.kt: -------------------------------------------------------------------------------- 1 | /* ### 2 | * IP: GHIDRA 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | //Writes "Hello, Kotlin!" to console. 17 | //@category Examples 18 | //@menupath Help.Examples.Hello World (Kotlin) 19 | //@toolbar world.png 20 | 21 | import ghidra.app.script.GhidraScript 22 | 23 | @Suppress("unused") 24 | class HelloWorldScriptKt : GhidraScript() { 25 | @Throws(Exception::class) 26 | override fun run() { 27 | println("Hello in Kotlin from $currentProgram!") 28 | } 29 | } -------------------------------------------------------------------------------- /GhidraJupyterKotlin/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlinVersion=1.9.23 2 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/lib/.gitkeep -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/CellContext.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | import ghidra.app.script.GhidraScript; 4 | import ghidra.program.model.address.Address; 5 | import ghidra.program.model.listing.Function; 6 | import ghidra.program.util.ProgramLocation; 7 | import ghidra.program.util.ProgramSelection; 8 | 9 | public class CellContext extends GhidraScript { 10 | 11 | ProgramLocation currentContextLocation; 12 | 13 | public CellContext(GhidraScript script) { 14 | super(); 15 | this.set(script.getState(), 16 | script.getMonitor(), 17 | null 18 | ); 19 | } 20 | 21 | public CellContext() { 22 | super(); 23 | } 24 | 25 | 26 | 27 | @Override 28 | protected void run() throws Exception { 29 | throw new Exception("This is not supposed to be run as a script!"); 30 | } 31 | 32 | @Override 33 | public void print(String message) { 34 | super.print(message); 35 | System.out.print(message); 36 | } 37 | 38 | @Override 39 | public void println(String message) { 40 | super.println(message); 41 | System.out.println(message); 42 | } 43 | 44 | // Expose protected variables with a getter 45 | // Kotlin will allow accessing them without the get prefix 46 | // e.g. currentAddress in the Jupyter console will call this function and not return currentAddress directly 47 | // This also allows adding some extra useful helpers like currentFunction 48 | 49 | public Address getCurrentAddress() { 50 | return currentAddress; 51 | } 52 | 53 | // This is not the same as the currentLocation in the Jython Shell or Ghidra Scripts 54 | // This uses the object provided to GhidraJupyterKotlin.JupyterKotlinPlugin.locationChanged directly 55 | // This contains more information like the exact token referenced in the decompiler or disassembly listing 56 | // The normal currentLocation in the Jython shell seems to be always simply the current address which is less useful 57 | public ProgramLocation getCurrentLocation() { 58 | return currentContextLocation; 59 | } 60 | 61 | public ProgramSelection getCurrentSelection() { 62 | return currentSelection; 63 | } 64 | 65 | public ProgramSelection getCurrentHighlight() { 66 | return currentHighlight; 67 | } 68 | 69 | public Function getCurrentFunction() { 70 | return currentProgram.getFunctionManager().getFunctionContaining(currentAddress); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/ConnectionFile.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | 4 | import ghidra.util.Msg; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.BufferedWriter; 8 | import java.io.File; 9 | import java.io.FileWriter; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.net.InetSocketAddress; 13 | import java.net.Socket; 14 | import java.nio.file.Files; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.UUID; 18 | 19 | public class ConnectionFile { 20 | 21 | public static File create() { 22 | return writeConfigFile(); 23 | 24 | } 25 | 26 | private static List getPorts(Integer n) { 27 | ArrayList sockets = new ArrayList<>(); 28 | try { 29 | for (int i = 0; i < n; i++) { 30 | Socket s = new Socket(); 31 | s.setSoLinger(false, 0); 32 | s.bind(new InetSocketAddress("", 0)); 33 | sockets.add(s); 34 | } 35 | } catch (IOException e) { 36 | e.printStackTrace(); 37 | return null; 38 | } 39 | ArrayList ports = new ArrayList<>(); 40 | for (Socket s : sockets) { 41 | ports.add(s.getLocalPort()); 42 | try { 43 | s.close(); 44 | } catch (IOException e) { 45 | e.printStackTrace(); 46 | } 47 | } 48 | 49 | return ports; 50 | } 51 | 52 | 53 | private static String getJupyterRuntime() { 54 | ProcessBuilder builder = new ProcessBuilder("jupyter", "--runtime-dir"); 55 | try { 56 | 57 | Process process = builder.start(); 58 | process.waitFor(); 59 | 60 | BufferedReader processOutputReader = new BufferedReader( 61 | new InputStreamReader(process.getInputStream())); 62 | 63 | return processOutputReader.readLine().strip(); 64 | } catch (InterruptedException | IOException e) { 65 | throw new RuntimeException("Could not get jupyter runtime directory, please report this issue",e); 66 | } 67 | } 68 | 69 | 70 | private static String formatConnectionFile(String key, List ports) { 71 | return String.format("{\n" + 72 | " \"control_port\": %d,\n" + 73 | " \"shell_port\": %d,\n" + 74 | " \"transport\": \"tcp\",\n" + 75 | " \"signature_scheme\": \"hmac-sha256\",\n" + 76 | " \"stdin_port\": %d,\n" + 77 | " \"hb_port\": %d,\n" + 78 | " \"ip\": \"127.0.0.1\",\n" + 79 | " \"iopub_port\": %d,\n" + 80 | " \"key\": \"%s\"\n" + 81 | "}", ports.get(0), ports.get(1), ports.get(2), ports.get(3), ports.get(4), key); 82 | } 83 | 84 | private static File writeConfigFile() { 85 | String key = UUID.randomUUID().toString(); 86 | List ports = getPorts(5); 87 | if (ports == null) { 88 | return null; 89 | } 90 | String runtimeDir = getJupyterRuntime(); 91 | File kernelFile = new File(runtimeDir, String.format("kernel-%s.json", key)); 92 | // Make sure that the directory actually exists 93 | // this is not guaranteed by only invoking `jupyter --runtime-dir` 94 | // on new machines that never did anything with jupyter the directory won't exist 95 | // and writing the file will fail 96 | Msg.info(ConnectionFile.class, "Trying to write Kernel file to: " + kernelFile.getAbsolutePath()); 97 | if (kernelFile.getParentFile() == null) { 98 | throw new RuntimeException("Parent directory of kernel file %s is unexpectedly null, please report this issue".formatted(kernelFile.toString())); 99 | } 100 | try { 101 | Files.createDirectories(kernelFile.getParentFile().toPath()); 102 | } catch (IOException e) { 103 | e.printStackTrace(); 104 | throw new RuntimeException(e); 105 | } 106 | 107 | String connectionFile = formatConnectionFile(key, ports); 108 | 109 | try { 110 | BufferedWriter writer = new BufferedWriter(new FileWriter(kernelFile)); 111 | writer.write(connectionFile); 112 | writer.flush(); 113 | writer.close(); 114 | } catch (IOException e) { 115 | e.printStackTrace(); 116 | return null; 117 | } 118 | 119 | return kernelFile; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/GhidraKotlinKernelLaunchable.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | import ghidra.GhidraApplicationLayout; 4 | import ghidra.GhidraLaunchable; 5 | import ghidra.util.Msg; 6 | import org.jetbrains.kotlinx.jupyter.IkotlinKt; 7 | import org.jetbrains.kotlinx.jupyter.libraries.EmptyResolutionInfoProvider; 8 | 9 | import java.io.File; 10 | import java.util.Collections; 11 | 12 | public class GhidraKotlinKernelLaunchable implements GhidraLaunchable { 13 | @Override 14 | public void launch(GhidraApplicationLayout ghidraApplicationLayout, String[] args) throws Exception { 15 | var connectionFile = new File(args[0]); 16 | Msg.info(this, connectionFile.toString()); 17 | IkotlinKt.embedKernel( 18 | connectionFile, 19 | null, 20 | null); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/InterruptKernelAction.kt: -------------------------------------------------------------------------------- 1 | //package GhidraJupyterKotlin 2 | // 3 | //import docking.ActionContext 4 | //import docking.action.DockingAction 5 | //import org.jetbrains.kotlinx.jupyter.* 6 | //import org.jetbrains.kotlinx.jupyter.messaging.* 7 | //import org.jetbrains.kotlinx.jupyter.startup.KernelJupyterParams 8 | //import org.zeromq.ZMQ 9 | //import java.util.* 10 | // 11 | // 12 | //class InterruptKernelAction(var parentPlugin: JupyterKotlinPlugin): DockingAction("Interrupt Kernel", parentPlugin.name) { 13 | // override fun actionPerformed(context: ActionContext?) { 14 | // // This creates a ZMQ socket, to send an interrupt request to the running kernel 15 | // // This is needed because Jupyter QT Console doesn't seem to provide this feature and just prints 16 | // // "Cannot interrupt a kernel I did not start" 17 | // // when using the "Interrupt Kernel" menu entry 18 | // 19 | // val config: KernelJupyterParams = KernelJupyterParams.fromFile(parentPlugin.connectionFile) 20 | // 21 | // val ctx = ZMQ.context(1) 22 | // val control = ctx.socket(JupyterSockets.CONTROL.zmqClientType) 23 | // control.connect("${config.transport}://*:${config.ports[JupyterSockets.CONTROL.ordinal]}") 24 | // if (config.sigScheme == "hmac-sha256"){ 25 | // val hmac = HMAC("HmacSHA256", config.key!!) 26 | // control.sendMessage( 27 | // Message(id = listOf(byteArrayOf(1)), 28 | // MessageData( 29 | // header = makeHeader( 30 | // MessageType.INTERRUPT_REQUEST, 31 | // sessionId = UUID.randomUUID().toString()), 32 | // content = InterruptRequest())), 33 | // hmac) 34 | // } 35 | // 36 | // 37 | // } 38 | //} -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/JupyterKotlinPlugin.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | import docking.ActionContext; 4 | import docking.action.DockingAction; 5 | import docking.action.MenuData; 6 | import docking.action.ToolBarData; 7 | import docking.widgets.OptionDialog; 8 | import ghidra.app.plugin.PluginCategoryNames; 9 | import ghidra.app.plugin.ProgramPlugin; 10 | import ghidra.app.script.GhidraState; 11 | import ghidra.framework.options.OptionType; 12 | import ghidra.framework.options.Options; 13 | import ghidra.framework.plugintool.PluginInfo; 14 | import ghidra.framework.plugintool.PluginTool; 15 | import ghidra.framework.plugintool.util.PluginStatus; 16 | import ghidra.program.model.listing.Program; 17 | import ghidra.program.util.ProgramLocation; 18 | import ghidra.program.util.ProgramSelection; 19 | import ghidra.util.Msg; 20 | import ghidra.util.task.RunManager; 21 | import ghidra.util.task.TaskMonitor; 22 | import org.apache.commons.lang3.ArrayUtils; 23 | import resources.ResourceManager; 24 | 25 | import javax.swing.*; 26 | import java.awt.*; 27 | import java.io.BufferedReader; 28 | import java.io.File; 29 | import java.io.IOException; 30 | import java.io.InputStreamReader; 31 | import java.net.URI; 32 | import java.net.URISyntaxException; 33 | 34 | //@formatter:off 35 | @PluginInfo( 36 | status = PluginStatus.RELEASED, 37 | packageName = "GhidraJupyterKotlin", 38 | category = PluginCategoryNames.COMMON, 39 | shortDescription = "Kotlin Jupyter kernel for Ghidra.", 40 | description = "Kotlin Jupyter kernel for Ghidra." 41 | ) 42 | //@formatter:on 43 | public class JupyterKotlinPlugin extends ProgramPlugin { 44 | private static final String OPTION_LAST_URI = "LAST_URI"; 45 | private static final String DEFAULT_URI = "http://localhost:8888/tree"; 46 | private static final String OPTION_CONSOLE_CMD = "CONSOLE_CMD"; 47 | private static final String DEFAULT_CONSOLE_CMD = "jupyter-qtconsole --existing"; 48 | private static final String PLUGIN_NAME = "JupyterKotlinPlugin"; 49 | private final RunManager runManager = new RunManager(); 50 | private final CellContext cellContext = new CellContext(); 51 | private Options programOptions; 52 | private Options toolOptions; 53 | 54 | public File getConnectionFile() { 55 | return (currentKernel != null) ? currentKernel.getConnectionFile() : null; 56 | } 57 | 58 | private KernelThread currentKernel = null; 59 | /** 60 | * Plugin constructor. 61 | * 62 | * @param tool The plugin tool that this plugin is added to. 63 | */ 64 | public JupyterKotlinPlugin(PluginTool tool) { 65 | super(tool); 66 | toolOptions = tool.getOptions(PLUGIN_NAME); 67 | toolOptions.registerOption(OPTION_CONSOLE_CMD, OptionType.STRING_TYPE, DEFAULT_CONSOLE_CMD, null, 68 | "Default Console command to execute (connection file will be appended)"); 69 | toolOptions.registerOption(OPTION_LAST_URI, OptionType.STRING_TYPE, DEFAULT_URI, null, 70 | "Default URI to open when using the GUI shortcut. " + 71 | "This can be set to the full path to a specific notebook " + 72 | "that should open directly after the kernel starts waiting"); 73 | registerActions(); 74 | } 75 | 76 | public void clearKernel() { 77 | currentKernel = null; 78 | } 79 | 80 | 81 | private void registerActions(){ 82 | DockingAction action = new DockingAction("Kotlin QtConsole", getName()) { 83 | @Override 84 | public void actionPerformed(ActionContext context) { 85 | if (getConnectionFile() == null) { 86 | currentKernel = new KotlinQtConsoleThread(cellContext, ConnectionFile.create()); 87 | runManager.runNow(currentKernel, "Kotlin kernel"); 88 | } 89 | launchQtConsole(); 90 | } 91 | }; 92 | ImageIcon qtconsoleIcon = ResourceManager.loadImage("images/qtconsole.png"); 93 | action.setToolBarData(new ToolBarData(qtconsoleIcon, null)); 94 | action.setMenuBarData( 95 | new MenuData(new String[] { "Jupyter", "Open QTConsole" }, qtconsoleIcon, null)); 96 | action.setEnabled(true); 97 | action.markHelpUnnecessary(); 98 | tool.addAction(action); 99 | 100 | DockingAction notebookAction = new DockingAction("Kotlin Notebook", getName()) { 101 | @Override 102 | public void actionPerformed(ActionContext context) { 103 | currentKernel = new NotebookThread(cellContext, tool); 104 | runManager.runNow(currentKernel, "Notebook"); 105 | } 106 | }; 107 | ImageIcon kernelIcon = ResourceManager.loadImage("images/notebook.png"); 108 | notebookAction.setToolBarData(new ToolBarData(kernelIcon, null)); 109 | notebookAction.setMenuBarData( 110 | new MenuData(new String[] { "Jupyter", "Start Kotlin Kernel for Notebook/Lab" }, kernelIcon, null)); 111 | notebookAction.setEnabled(true); 112 | notebookAction.markHelpUnnecessary(); 113 | tool.addAction(notebookAction); 114 | 115 | 116 | DockingAction serverAction = new DockingAction("Jupyter Server", getName()) { 117 | @Override 118 | public void actionPerformed(ActionContext context) { 119 | openNotebookServer(); 120 | } 121 | }; 122 | 123 | serverAction.setMenuBarData( 124 | new MenuData(new String[] { "Jupyter", "Open Jupyter Notebook Server" }, null, null)); 125 | serverAction.setDescription("Tries to open existing server or offers to start a new one"); 126 | tool.addAction(serverAction); 127 | 128 | DockingAction defaultNotebookAction = new DockingAction("Default Notebook", getName()) { 129 | @Override 130 | public void actionPerformed(ActionContext context) { 131 | openDefaultNotebook(); 132 | } 133 | }; 134 | 135 | defaultNotebookAction.setMenuBarData( 136 | new MenuData(new String[] { "Jupyter", "Open Default Notebook" }, null, null)); 137 | defaultNotebookAction.setDescription("Open Default Notebook"); 138 | tool.addAction(defaultNotebookAction); 139 | 140 | // DockingAction interruptAction = new InterruptKernelAction(this); 141 | // interruptAction.setMenuBarData( 142 | // new MenuData(new String[] { "Jupyter", "Interrupt Execution" }, null, null)); 143 | // interruptAction.setDescription("Interrupts the currently running kernel if it is executing something"); 144 | // tool.addAction(interruptAction); 145 | 146 | // DockingAction shutdownAction = new ShutDownKernelAction(this); 147 | // shutdownAction.setMenuBarData( 148 | // new MenuData(new String[] { "Jupyter", "Shutdown Kernel" }, null, null)); 149 | // shutdownAction.setDescription("Terminates the currently running kernel if it isn't busy"); 150 | // tool.addAction(shutdownAction); 151 | } 152 | 153 | private void launchQtConsole() { 154 | String[] console = toolOptions.getString(OPTION_CONSOLE_CMD, 155 | DEFAULT_CONSOLE_CMD).split(" "); 156 | String[] command = ArrayUtils.add(console, currentKernel.getConnectionFile().toString()); 157 | try { 158 | Runtime.getRuntime().exec(command); 159 | } catch (IOException e) { 160 | Msg.showError(this, null, "QT Console process failed", 161 | "The console command failed to start because of an IOException.\n" + 162 | "Most likely jupyter-qtconsole is not available in your PATH because it wasn't installed\n" + 163 | "or your custom command has some issues\n" + 164 | "You can manually run the following command to debug this: \n" + 165 | String.join(" ", command) + 166 | "\nThe kernel*.json path is optional. Leaving it out will reconnect to your most recent running kernel, which is most likely the correct one.\n" + 167 | "You can also run 'jupyter-console --existing' for a terminal based console which is typically already included with a Jupyter install", 168 | e); 169 | } 170 | } 171 | 172 | private URI checkForRunningNotebookServer(){ 173 | Runtime rt = Runtime.getRuntime(); 174 | String[] commands = {"jupyter", "notebook", "list", "--json"}; 175 | 176 | Process proc = null; 177 | try { 178 | proc = rt.exec(commands); 179 | 180 | BufferedReader stdInput = new BufferedReader(new 181 | InputStreamReader(proc.getInputStream())); 182 | 183 | String s = stdInput.readLine(); 184 | 185 | // if (s != null) { 186 | // JSONObject obj = new JSONObject(s); 187 | // return new URI((String) obj.get("url")); 188 | // } 189 | } catch (IOException e) { 190 | e.printStackTrace(); 191 | // } catch (URISyntaxException e) { 192 | // e.printStackTrace(); 193 | } 194 | return null; 195 | } 196 | 197 | private void openURI(URI uri){ 198 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { 199 | try { 200 | Desktop.getDesktop().browse(uri); 201 | } catch (IOException e) { 202 | e.printStackTrace(); 203 | } 204 | } else { 205 | Msg.error(this, 206 | "Notebook couldn't be opened because environment doesn't support opening URLs"); 207 | } 208 | } 209 | private void openNotebookServer() { 210 | URI uri = checkForRunningNotebookServer(); 211 | 212 | if (uri !=null){ 213 | openURI(uri); 214 | } else { 215 | if (OptionDialog.showYesNoDialog(null, 216 | "Start new Jupyter server?", 217 | "No running Jupyter Notebook server could be detected, would you like to start a new one?\n" + 218 | "This server will use the default options by simply invoking 'jupyter-notebook'" + 219 | "and persist after closing Ghidra") 220 | == OptionDialog.OPTION_ONE){ 221 | Runtime rt = Runtime.getRuntime(); 222 | String[] commands = {"jupyter-notebook" }; 223 | try { 224 | rt.exec(commands); 225 | } catch (IOException e) { 226 | Msg.showError(this, null, 227 | "Failed to start Jupyter Server", 228 | "The Jupyter Server could not be started", e); 229 | } 230 | } 231 | 232 | } 233 | } 234 | 235 | private void openDefaultNotebook(){ 236 | if (currentKernel != null) { 237 | Msg.info(this, "There is already a kernel running"); 238 | return; 239 | } 240 | var programValue = programOptions.getString(OPTION_LAST_URI, ""); 241 | var toolValue = toolOptions.getString(OPTION_LAST_URI, ""); 242 | var value = ""; 243 | if (programValue.equals("") && toolValue.equals("")){ 244 | var msg = String.format("The URI option was not set, please go to\n" + 245 | "'Edit'-> 'Options for %s' -> %s\n" + 246 | "and set the option to the full URL your default browser should navigate to", currentProgram.getName(), PLUGIN_NAME); 247 | Msg.showError(this, null,"No URI set in options", msg); 248 | return; 249 | } 250 | else if (programValue.equals("")){ 251 | Msg.info(this, "No program specific notebook configured, but default option is available"); 252 | value = toolValue; 253 | } 254 | else { 255 | value = programValue; 256 | } 257 | try { 258 | var uri = new URI(value); 259 | if (uri.getScheme().equals("http")) { 260 | currentKernel = new NotebookThread(cellContext, tool); 261 | runManager.runNow(currentKernel, "Notebook"); 262 | openURI(uri); 263 | } 264 | else { 265 | Msg.showError(this, null, "Invalid URI", "Scheme of the URI option isn't http, this seems wrong"); 266 | } 267 | } catch (URISyntaxException e) { 268 | Msg.showError(this, null, "Last URI Option is invalid", 269 | "Last URI in options was not a valid URI and parsing it threw an exception", e); 270 | } 271 | 272 | 273 | } 274 | @Override 275 | protected void programActivated(Program activatedProgram) { 276 | Msg.info(this, "Program activated"); 277 | var state = new GhidraState(tool, tool.getProject(), 278 | currentProgram, currentLocation, currentSelection, currentHighlight); 279 | cellContext.set(state, TaskMonitor.DUMMY, null); 280 | 281 | programOptions = activatedProgram.getOptions(PLUGIN_NAME); 282 | programOptions.registerOption(OPTION_LAST_URI, OptionType.STRING_TYPE, "", null, 283 | "Saved URI"); 284 | } 285 | 286 | protected void programClosed(Program program) { 287 | Msg.info(this, "Program closed"); 288 | if (cellContext.getCurrentProgram() == program) { 289 | var state = new GhidraState(tool, tool.getProject(), 290 | null, null, null, null); 291 | cellContext.set(state, TaskMonitor.DUMMY, null); 292 | } 293 | } 294 | 295 | /** 296 | * Subclass should override this method if it is interested in 297 | * program location events. 298 | * @param loc location could be null 299 | */ 300 | protected void locationChanged(ProgramLocation loc) { 301 | if (currentLocation != null) { 302 | cellContext.setCurrentLocation(currentLocation.getAddress()); 303 | cellContext.currentContextLocation = loc; 304 | } 305 | } 306 | 307 | /** 308 | * Subclass should override this method if it is interested in 309 | * program selection events. 310 | * @param sel selection could be null 311 | */ 312 | protected void selectionChanged(ProgramSelection sel) { 313 | cellContext.setCurrentSelection(currentSelection); 314 | } 315 | 316 | /** 317 | * Subclass should override this method if it is interested in 318 | * program highlight events. 319 | * @param hl highlight could be null 320 | */ 321 | protected void highlightChanged(ProgramSelection hl) { 322 | if (hl != null) { 323 | cellContext.setCurrentHighlight(hl); 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/KernelThread.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | import ghidra.util.task.MonitoredRunnable; 4 | 5 | import java.io.File; 6 | 7 | public interface KernelThread extends MonitoredRunnable { 8 | File getConnectionFile(); 9 | } 10 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/KotlinQtConsoleThread.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | import ghidra.util.Msg; 4 | import ghidra.util.task.MonitoredRunnable; 5 | import ghidra.util.task.TaskMonitor; 6 | import org.jetbrains.kotlinx.jupyter.IkotlinKt; 7 | import org.jetbrains.kotlinx.jupyter.libraries.EmptyResolutionInfoProvider; 8 | import org.zeromq.ZMQException; 9 | 10 | import java.io.*; 11 | import java.util.*; 12 | 13 | public class KotlinQtConsoleThread implements KernelThread { 14 | 15 | private final CellContext context; 16 | private final File connectionFile; 17 | 18 | public KotlinQtConsoleThread(CellContext ctx, File connectionFile) { 19 | this.context = ctx; 20 | this.connectionFile = connectionFile; 21 | } 22 | 23 | @Override 24 | public void monitoredRun(TaskMonitor monitor) { 25 | Msg.info(this, connectionFile.toString()); 26 | try { 27 | IkotlinKt.embedKernel( 28 | connectionFile, 29 | null, 30 | Collections.singletonList(context)); 31 | } catch( ZMQException e){ 32 | Msg.warn(this,"Kernel terminated, probably because of shutdown request?", e); 33 | } 34 | } 35 | 36 | @Override 37 | public File getConnectionFile() { 38 | return connectionFile; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/LineReader.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | import ghidra.util.exception.CancelledException; 4 | import ghidra.util.task.TaskMonitor; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.File; 8 | import java.io.FileNotFoundException; 9 | import java.io.FileReader; 10 | import java.io.IOException; 11 | 12 | class LineReader { 13 | BufferedReader reader; 14 | 15 | public LineReader(File file) throws FileNotFoundException { 16 | reader = new BufferedReader(new FileReader(file)); 17 | } 18 | 19 | public String readLine(TaskMonitor monitor) throws IOException, CancelledException { 20 | while (!reader.ready()) { 21 | try { 22 | if (monitor.isCancelled()){ 23 | throw new CancelledException(); 24 | } 25 | Thread.sleep(1000); 26 | } catch (InterruptedException e) { 27 | e.printStackTrace(); 28 | } 29 | } 30 | return reader.readLine(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/NotebookProxy.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | import ghidra.util.exception.CancelledException; 4 | import ghidra.util.task.TaskMonitor; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.StandardOpenOption; 11 | 12 | public class NotebookProxy { 13 | File proxyFile; 14 | File pidFile; 15 | 16 | public NotebookProxy(Path proxyBase) { 17 | this.proxyFile = new File(proxyBase.toString() + ".path"); 18 | this.pidFile = new File(proxyBase.toString() + ".pid"); 19 | } 20 | 21 | public File waitForConnection(TaskMonitor monitor) throws IOException, CancelledException { 22 | // Ensure that the directory exists (create it if needed) 23 | var parent = proxyFile.getParentFile(); 24 | if (!parent.exists()) { 25 | if (!parent.mkdirs()){ 26 | throw new IOException("Failed to create directory: " + parent); 27 | } 28 | } 29 | 30 | // Write the PID to file so that the 31 | pidFile.delete(); 32 | pidFile.createNewFile(); 33 | pidFile.deleteOnExit(); 34 | 35 | long pid = ProcessHandle.current().pid(); 36 | Files.write(Path.of(pidFile.getPath()), Long.toString(pid).getBytes(), StandardOpenOption.APPEND); 37 | 38 | // Create a proxy file and wait for a line on it 39 | proxyFile.delete(); 40 | proxyFile.createNewFile(); 41 | proxyFile.deleteOnExit(); 42 | 43 | var lineReader = new LineReader(proxyFile); 44 | 45 | return new File(lineReader.readLine(monitor)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/NotebookThread.java: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin; 2 | 3 | 4 | import ghidra.framework.plugintool.PluginTool; 5 | import ghidra.util.Msg; 6 | import ghidra.util.exception.CancelledException; 7 | import ghidra.util.task.MonitoredRunnable; 8 | import ghidra.util.task.Task; 9 | import ghidra.util.task.TaskMonitor; 10 | import org.jetbrains.kotlinx.jupyter.IkotlinKt; 11 | import org.jetbrains.kotlinx.jupyter.libraries.EmptyResolutionInfoProvider; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.nio.file.Path; 16 | import java.util.Collections; 17 | import java.util.Optional; 18 | 19 | public class NotebookThread implements KernelThread { 20 | /** 21 | * Steps: 22 | * 1. Create proxy file 23 | * 2. Wait on proxy file 24 | * 3. Read connection file path 25 | * 4. Embed kernel 26 | */ 27 | 28 | private final CellContext context; 29 | private final PluginTool tool; 30 | 31 | 32 | @Override 33 | public File getConnectionFile() { 34 | return connectionFile; 35 | } 36 | 37 | private File connectionFile = null; 38 | 39 | public NotebookThread(CellContext ctx, PluginTool tool) { 40 | this.context = ctx; 41 | this.tool = tool; 42 | } 43 | 44 | private class WaitTask extends Task { 45 | public WaitTask() { 46 | super("Waiting for Jupyter Notebook connection", 47 | true, 48 | false, 49 | true, 50 | true); 51 | } 52 | 53 | @Override 54 | public void run(TaskMonitor taskMonitor) throws CancelledException { 55 | var proxyPath = Path.of(Optional.ofNullable(System.getenv("GHIDRA_JUPYTER_PROXY")) 56 | .orElse( 57 | Path.of(System.getProperty("user.home")) 58 | .resolve(".ghidra") 59 | .resolve("notebook_proxy") 60 | .toString() 61 | )); 62 | try { 63 | connectionFile = new NotebookProxy(proxyPath).waitForConnection(taskMonitor); 64 | } catch (IOException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | } 69 | 70 | @Override 71 | public void monitoredRun(TaskMonitor monitor) { 72 | if (connectionFile == null) { 73 | tool.execute(new WaitTask()); 74 | } 75 | 76 | Msg.info(this, connectionFile.toString()); 77 | IkotlinKt.embedKernel( 78 | connectionFile, 79 | null, 80 | Collections.singletonList(context)); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/ShutDownKernelAction.kt: -------------------------------------------------------------------------------- 1 | //package GhidraJupyterKotlin 2 | // 3 | //import docking.ActionContext 4 | //import docking.action.DockingAction 5 | //import org.jetbrains.kotlinx.jupyter.startup.KernelJupyterParams 6 | ////import org.jetbrains.kotlinx.jupyter.* 7 | ////import org.jetbrains.kotlinx.jupyter.messaging.* 8 | //import org.zeromq.ZMQ 9 | //import java.util.* 10 | // 11 | // 12 | //class ShutDownKernelAction(var parentPlugin: JupyterKotlinPlugin): DockingAction("Interrupt Kernel", parentPlugin.name) { 13 | // override fun actionPerformed(context: ActionContext?) { 14 | // // This creates a ZMQ socket, to send a shutdown request to the running kernel 15 | // // This is basically a duplicate of InterruptKernelAction and could be merged in the future 16 | // val config: KernelJupyterParams = KernelJupyterParams.fromFile(parentPlugin.connectionFile) 17 | // 18 | // val ctx = ZMQ.context(1) 19 | // val control = ctx.socket(JupyterSockets.CONTROL.zmqClientType) 20 | // control.connect("${config.transport}://*:${config.ports[JupyterSockets.CONTROL.ordinal]}") 21 | // if (config.sigScheme == "hmac-sha256"){ 22 | // val hmac = HMAC("HmacSHA256", config.key!!) 23 | // control.sendMessage( 24 | // Message(id = listOf(byteArrayOf(1)), 25 | // MessageData( 26 | // header = makeHeader( 27 | // MessageType.SHUTDOWN_REQUEST, 28 | // sessionId = UUID.randomUUID().toString()), 29 | // content = ShutdownRequest(false))), 30 | // hmac) 31 | // parentPlugin.clearKernel() 32 | // } 33 | // 34 | // 35 | // } 36 | //} -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions/address/AddressExtensions.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | //They are imported in some script or the kernel itself by the user 3 | // use them by adding the following import to script 4 | // import GhidraJupyterKotlin.extensions.address.* 5 | package GhidraJupyterKotlin.extensions.address 6 | 7 | import ghidra.program.model.address.Address 8 | import ghidra.program.model.address.AddressRange 9 | import ghidra.program.model.address.AddressRangeImpl 10 | 11 | 12 | 13 | /** 14 | * `currentAddress+10` returns a new Address 15 | */ 16 | operator fun Address.plus(rhs: Long): Address { 17 | return this.addNoWrap(rhs) 18 | } 19 | operator fun Address.plus(rhs: Int): Address { 20 | return this.addNoWrap(rhs.toLong()) 21 | } 22 | 23 | /** 24 | * `currentAddress-10` returns a new Address 25 | */ 26 | operator fun Address.minus(rhs: Long): Address { 27 | return this.subtractNoWrap(rhs) 28 | } 29 | operator fun Address.minus(rhs: Int): Address { 30 | return this.subtractNoWrap(rhs.toLong()) 31 | } 32 | 33 | operator fun Address.minus(rhs: Address): Long { 34 | return this.subtract(rhs) 35 | } 36 | 37 | /** 38 | * `currentAddress..otherAddress` gives an AddressRange with currentAddress as start, and otherAddress as end 39 | */ 40 | operator fun Address.rangeTo(rhs: Address): AddressRange { 41 | return AddressRangeImpl(this, rhs) 42 | } 43 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions/data/DataExtensions.kt: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin.extensions.data 2 | 3 | import ghidra.program.model.data.Structure 4 | import ghidra.program.model.listing.Data 5 | 6 | 7 | 8 | // For a Data object that supports component (like arrays or structs) you can use 9 | // `data[i]` instead of `data.getComponent(i)` 10 | operator fun Data.get(i: Int): Data? { 11 | return this.getComponent(i) 12 | } 13 | 14 | // For a Data object that represents a struct you can use 15 | // `data[fieldName]` 16 | 17 | operator fun Data.get(name: String): Data? { 18 | if (this.dataType is Structure){ 19 | val s = (this.dataType as Structure) 20 | val idx = s.components.firstOrNull { it.fieldName == name }?.ordinal 21 | return idx?.let(this::getComponent) 22 | } 23 | return null 24 | } -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions/misc/MiscExtensions.kt: -------------------------------------------------------------------------------- 1 | package GhidraJupyterKotlin.extensions.misc 2 | 3 | import ghidra.framework.model.DomainObject 4 | import ghidra.program.model.listing.FunctionIterator 5 | import ghidra.program.model.listing.FunctionManager 6 | import ghidra.program.model.listing.Program 7 | 8 | val FunctionManager.functions: FunctionIterator 9 | get() = this.getFunctions(true) 10 | 11 | 12 | val Program.functions: FunctionIterator 13 | get() = this.functionManager.getFunctions(true) 14 | 15 | 16 | fun DomainObject.runTransaction(description: String, transaction: () -> Unit) { 17 | val transactionID: Int = this.startTransaction(description) 18 | try { 19 | transaction() 20 | this.endTransaction(transactionID, true) 21 | } 22 | catch (e: Throwable) { 23 | this.endTransaction(transactionID, false) 24 | throw e 25 | } 26 | } 27 | 28 | fun DomainObject.runTransaction(transaction: () -> Unit){ 29 | val transactionID: Int = this.startTransaction("Kotlin Lambda Transaction") 30 | try { 31 | transaction() 32 | this.endTransaction(transactionID, true) 33 | } 34 | catch (e: Throwable) { 35 | this.endTransaction(transactionID, false) 36 | throw e 37 | } 38 | } -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/ghidra/app/script/KotlinCompilerMessageCollector.kt: -------------------------------------------------------------------------------- 1 | package ghidra.app.script 2 | 3 | import generic.jar.ResourceFile 4 | import ghidra.util.Msg 5 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 6 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation 7 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 8 | 9 | class KotlinCompilerMessageCollector(private val sourceFile: ResourceFile) : MessageCollector { 10 | override fun clear() { 11 | return 12 | } 13 | 14 | override fun hasErrors(): Boolean { 15 | return false 16 | } 17 | 18 | override fun toString(): String { 19 | return "MessageCollector for compilation of $sourceFile" 20 | } 21 | override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) { 22 | when (severity){ 23 | CompilerMessageSeverity.EXCEPTION -> Msg.error(this, message) 24 | CompilerMessageSeverity.ERROR -> Msg.error(this, message) 25 | CompilerMessageSeverity.STRONG_WARNING -> Msg.warn(this, message) 26 | CompilerMessageSeverity.WARNING -> Msg.warn(this, message) 27 | CompilerMessageSeverity.INFO -> Msg.info(this, message) 28 | CompilerMessageSeverity.LOGGING -> Msg.debug(this, message) 29 | CompilerMessageSeverity.OUTPUT -> Msg.out(message) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/java/ghidra/app/script/KotlinScriptProvider.java: -------------------------------------------------------------------------------- 1 | /* ### 2 | * IP: GHIDRA 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ghidra.app.script; 17 | 18 | import generic.io.NullPrintWriter; 19 | import generic.jar.ResourceFile; 20 | import ghidra.app.util.headless.HeadlessScript; 21 | import ghidra.util.Msg; 22 | import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys; 23 | import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments; 24 | import org.jetbrains.kotlin.cli.common.config.ContentRootsKt; 25 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity; 26 | import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler; 27 | import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles; 28 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; 29 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler; 30 | import org.jetbrains.kotlin.cli.jvm.config.JvmContentRootsKt; 31 | import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer; 32 | import org.jetbrains.kotlin.config.CommonConfigurationKeys; 33 | import org.jetbrains.kotlin.config.CompilerConfiguration; 34 | import org.jetbrains.kotlin.config.JVMConfigurationKeys; 35 | import org.jetbrains.kotlin.utils.PathUtil; 36 | 37 | import java.io.File; 38 | import java.io.FileWriter; 39 | import java.io.IOException; 40 | import java.io.PrintWriter; 41 | import java.lang.reflect.InvocationTargetException; 42 | import java.net.MalformedURLException; 43 | import java.net.URL; 44 | import java.net.URLClassLoader; 45 | import java.util.ArrayList; 46 | import java.util.Arrays; 47 | import java.util.List; 48 | import java.util.stream.Collectors; 49 | 50 | 51 | @SuppressWarnings("unused") 52 | // This is an ExtensionPoint, so the class loader automatically searches for classes ending in "ScriptProvider" 53 | // and sets them up 54 | public class KotlinScriptProvider extends GhidraScriptProvider { 55 | 56 | @Override 57 | public String getDescription() { 58 | return "Kotlin"; 59 | } 60 | 61 | @Override 62 | public String getExtension() { 63 | return ".kt"; 64 | } 65 | 66 | @Override 67 | public boolean deleteScript(ResourceFile scriptSource) { 68 | // Assuming script is in default java package, so using script's base name as class name. 69 | File clazzFile = getClassFile(scriptSource, GhidraScriptUtil.getBaseName(scriptSource)); 70 | //noinspection ResultOfMethodCallIgnored 71 | clazzFile.delete(); 72 | return super.deleteScript(scriptSource); 73 | } 74 | 75 | @Override 76 | public GhidraScript getScriptInstance(ResourceFile sourceFile, PrintWriter writer) 77 | throws GhidraScriptLoadException { 78 | 79 | if (writer == null) { 80 | writer = new NullPrintWriter(); 81 | } 82 | 83 | // Assuming script is in default java package, so using script's base name as class name. 84 | File clazzFile = getClassFile(sourceFile, GhidraScriptUtil.getBaseName(sourceFile)); 85 | try { 86 | compile(sourceFile, writer); // may throw an exception 87 | } catch (ClassNotFoundException e) { 88 | throw new GhidraScriptLoadException("The class could not be found. " + 89 | "It must be the public class of the .java file: " + e.getMessage(), e); 90 | } 91 | 92 | 93 | Class clazz; 94 | try { 95 | clazz = getScriptClass(sourceFile); 96 | } 97 | catch (GhidraScriptUnsupportedClassVersionError e) { 98 | // Unusual Code Alert!: This implies the script was compiled in a newer 99 | // version of Java. So, just delete the class file and try again. 100 | ResourceFile classFile = e.getClassFile(); 101 | classFile.delete(); 102 | return getScriptInstance(sourceFile, writer); 103 | } 104 | 105 | Object object = null; 106 | try { 107 | // If clazz is null for some reason crashing with it might make it more obvious where the issue lies 108 | //noinspection ConstantConditions 109 | object = clazz.getDeclaredConstructor().newInstance(); 110 | } catch (InvocationTargetException | NoSuchMethodException e) { 111 | throw new GhidraScriptLoadException(e); 112 | } catch (InstantiationException | IllegalAccessException e) { 113 | throw new GhidraScriptLoadException(e); 114 | } 115 | if (object instanceof GhidraScript) { 116 | GhidraScript script = (GhidraScript) object; 117 | script.setSourceFile(sourceFile); 118 | return script; 119 | } 120 | 121 | String message = "Not a valid Ghidra script: " + sourceFile.getName(); 122 | writer.println(message); 123 | Msg.error(this, message); // the writer may not be the same as Msg, so log it too 124 | return null; // class is not a script 125 | } 126 | 127 | 128 | /** 129 | * Gets the class file corresponding to the given source file and class name. 130 | * If the class is in a package, the class name should include the full 131 | * package name. 132 | * 133 | * @param sourceFile The class's source file. 134 | * @param className The class's name (including package if applicable). 135 | * @return The class file corresponding to the given source file and class name. 136 | */ 137 | protected File getClassFile(ResourceFile sourceFile, String className) { 138 | ResourceFile resourceFile = 139 | getClassFileByResourceFile(sourceFile, className); 140 | 141 | return resourceFile.getFile(false); 142 | } 143 | 144 | static ResourceFile getClassFileByResourceFile(ResourceFile sourceFile, String rawName) { 145 | String javaAbsolutePath = sourceFile.getAbsolutePath(); 146 | String classAbsolutePath = javaAbsolutePath.replace(".java", ".class"); 147 | 148 | return new ResourceFile(classAbsolutePath); 149 | } 150 | 151 | protected boolean needsCompile(ResourceFile sourceFile, File classFile) { 152 | 153 | // Need to compile if there is no class file. 154 | if (!classFile.exists()) { 155 | return true; 156 | } 157 | 158 | // Need to compile if the script's source file is newer than its corresponding class file. 159 | if (sourceFile.lastModified() > classFile.lastModified()) { 160 | return true; 161 | } 162 | 163 | // Need to compile if parent classes are not up to date. 164 | return !areAllParentClassesUpToDate(sourceFile); 165 | } 166 | 167 | 168 | private boolean areAllParentClassesUpToDate(ResourceFile sourceFile) { 169 | 170 | List> parentClasses = getParentClasses(sourceFile); 171 | if (parentClasses == null) { 172 | // some class is missing! 173 | return false; 174 | } 175 | 176 | if (parentClasses.isEmpty()) { 177 | // nothing to do--no parent class to re-compile 178 | return true; 179 | } 180 | 181 | // check each parent for modification 182 | for (Class clazz : parentClasses) { 183 | ResourceFile parentFile = getSourceFile(clazz); 184 | if (parentFile == null) { 185 | continue; // not sure if this can happen (inner-class, maybe?) 186 | } 187 | 188 | // Parent class might have a non-default java package, so use class's full name. 189 | File clazzFile = getClassFile(parentFile, clazz.getName()); 190 | 191 | if (parentFile.lastModified() > clazzFile.lastModified()) { 192 | return false; 193 | } 194 | } 195 | 196 | return true; 197 | } 198 | 199 | protected void compile(ResourceFile sourceFile, final PrintWriter writer) 200 | throws ClassNotFoundException { 201 | if (!doEmbeddedCompile(sourceFile, writer)) { 202 | writer.flush(); // force any error messages out 203 | throw new ClassNotFoundException("Unable to compile class: " + sourceFile.getName()); 204 | } 205 | writer.println("Successfully compiled: " + sourceFile.getName()); 206 | } 207 | 208 | private K2JVMCompilerArguments getCompilerArgs(K2JVMCompiler compiler){ 209 | var arguments = compiler.createArguments(); 210 | var cp = getClassPath(); 211 | 212 | arguments.setClasspath(cp); 213 | 214 | return arguments; 215 | } 216 | private boolean doEmbeddedCompile(ResourceFile sourceFile, final PrintWriter writer) { 217 | Msg.info(this, "Compiling sourceFile: " + sourceFile.getAbsolutePath()); 218 | 219 | if (System.getProperty("os.name").startsWith("Windows")) { 220 | System.getProperties().setProperty("idea.io.use.nio2", java.lang.Boolean.TRUE.toString()); 221 | } 222 | 223 | var rootDisposable = Disposer.newDisposable(); 224 | var compiler = new K2JVMCompiler(); 225 | var args = getCompilerArgs(compiler); 226 | var compilerConfiguration = new CompilerConfiguration(); 227 | // TODO: What is a good module name here? 228 | compilerConfiguration.put(CommonConfigurationKeys.MODULE_NAME, "SOME_MODULE_NAME"); 229 | var collector = new KotlinCompilerMessageCollector(sourceFile); 230 | compilerConfiguration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, collector); 231 | JvmContentRootsKt.addJvmClasspathRoots(compilerConfiguration, PathUtil.getJdkClassesRootsFromCurrentJre()); 232 | JvmContentRootsKt.addJvmClasspathRoots(compilerConfiguration, getClassPathAsFiles()); 233 | ContentRootsKt.addKotlinSourceRoot(compilerConfiguration, sourceFile.toString()); 234 | compilerConfiguration.put(JVMConfigurationKeys.OUTPUT_DIRECTORY, outputDir(sourceFile).getFile(false)); 235 | 236 | // This shouldn't be needed and is a workaround for a bug in the Kotlin compiler 237 | // https://youtrack.jetbrains.com/issue/KT-20167/JDK-9-unresolved-supertypes-Object-when-working-with-Kotlin-Scripting-API 238 | compilerConfiguration.put(JVMConfigurationKeys.JDK_HOME, new File(System.getProperty("java.home"))); 239 | 240 | var disposable = Disposer.newDisposable(); 241 | 242 | KotlinCoreEnvironment env = KotlinCoreEnvironment.createForProduction( 243 | disposable, compilerConfiguration, EnvironmentConfigFiles.JVM_CONFIG_FILES); 244 | 245 | return KotlinToJVMBytecodeCompiler.INSTANCE.compileBunchOfSources(env); 246 | } 247 | 248 | private List getClassPathAsFiles(){ 249 | return Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator)) 250 | .map(File::new) 251 | // There might be files like "ExtensionPoint.manifest" as a classpath entry 252 | // the Kotlin compiler tries to open them as .jars (ZIP) and fails, so filter them out 253 | .filter(it -> it.getName().endsWith(".jar") || it.isDirectory()) 254 | .collect(Collectors.toList()); 255 | } 256 | 257 | private ResourceFile outputDir(ResourceFile sourceFile) { 258 | return sourceFile.getParentFile(); 259 | } 260 | 261 | private List> getParentClasses(ResourceFile scriptSourceFile) { 262 | 263 | Class scriptClass = getScriptClass(scriptSourceFile); 264 | if (scriptClass == null) { 265 | return null; // special signal that there was a problem 266 | } 267 | 268 | List> parentClasses = new ArrayList<>(); 269 | Class superClass = scriptClass.getSuperclass(); 270 | while (superClass != null) { 271 | if (superClass.equals(GhidraScript.class)) { 272 | break; // not interested in the built-in classes 273 | } else if (superClass.equals(HeadlessScript.class)) { 274 | break; // not interested in the built-in classes 275 | } 276 | parentClasses.add(superClass); 277 | superClass = superClass.getSuperclass(); 278 | } 279 | return parentClasses; 280 | } 281 | 282 | private Class getScriptClass(ResourceFile scriptSourceFile) { 283 | String clazzName = GhidraScriptUtil.getBaseName(scriptSourceFile); 284 | try { 285 | URL classURL = outputDir(scriptSourceFile).getFile(false).toURI().toURL(); 286 | ClassLoader cl = new URLClassLoader(new URL[] {classURL}); 287 | return cl.loadClass(clazzName); 288 | } 289 | catch (NoClassDefFoundError | ClassNotFoundException e) { 290 | Msg.error(this, "Unable to find class file for script file: " + scriptSourceFile, e); 291 | 292 | } 293 | catch (MalformedURLException e) { 294 | Msg.error(this, "Malformed URL exception:", e); 295 | } 296 | return null; 297 | } 298 | 299 | private ResourceFile getSourceFile(Class c) { 300 | // check all script paths for a dir named 301 | String classname = c.getName(); 302 | String filename = classname.replace('.', '/') + ".kt"; 303 | 304 | List scriptDirs = GhidraScriptUtil.getScriptSourceDirectories(); 305 | for (ResourceFile dir : scriptDirs) { 306 | ResourceFile possibleFile = new ResourceFile(dir, filename); 307 | if (possibleFile.exists()) { 308 | return possibleFile; 309 | } 310 | } 311 | 312 | return null; 313 | } 314 | 315 | private String getClassPath() { 316 | return System.getProperty("java.class.path"); 317 | } 318 | 319 | @Override 320 | public void createNewScript(ResourceFile newScript, String category) throws IOException { 321 | String scriptName = newScript.getName(); 322 | String className = scriptName; 323 | int dotPos = scriptName.lastIndexOf('.'); 324 | if (dotPos >= 0) { 325 | className = scriptName.substring(0, dotPos); 326 | } 327 | PrintWriter writer = new PrintWriter(new FileWriter(newScript.getFile(false))); 328 | 329 | writeHeader(writer, category); 330 | 331 | writer.println("import ghidra.app.script.GhidraScript"); 332 | 333 | for (Package pkg : Package.getPackages()) { 334 | if (pkg.getName().startsWith("ghidra.program.model.")) { 335 | writer.println("import " + pkg.getName() + ".*"); 336 | } 337 | } 338 | 339 | writer.println(""); 340 | 341 | writer.println("class " + className + " : GhidraScript() {"); 342 | 343 | writer.println(" @Throws(Exception::class)"); 344 | writer.println(" override fun run() {"); 345 | 346 | writeBody(writer); 347 | 348 | writer.println(" }"); 349 | writer.println(""); 350 | writer.println("}"); 351 | writer.println(""); 352 | writer.close(); 353 | } 354 | 355 | @Override 356 | public String getCommentCharacter() { 357 | return "//"; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/resources/images/README.txt: -------------------------------------------------------------------------------- 1 | The "src/resources/images" directory is intended to hold all image/icon files used by 2 | this module. 3 | -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/resources/images/notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/src/main/resources/images/notebook.png -------------------------------------------------------------------------------- /GhidraJupyterKotlin/src/main/resources/images/qtconsole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/src/main/resources/images/qtconsole.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GhidraJupyter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ghidra-Jupyter 2 | 3 | ## Automatic Installation 4 | 5 | 1. Use pip to install the kernel and the management utility 6 | 7 | ```bash 8 | pip install ghidra-jupyter 9 | ``` 10 | 11 | 2. Use the management utility to install the extension. 12 | Make sure `$GHIDRA_INSTALL_DIR` is defined, 13 | as it points the utility to the right path. 14 | 15 | ```bash 16 | ghidra-jupyter install-extension 17 | ``` 18 | 19 | 3. If you have multiple installations of Ghidra, 20 | you can point the installer to the right one. 21 | 22 | ```bash 23 | ghidra-jupyter install-extension --ghidra 24 | ``` 25 | 26 | ## Manual Installation 27 | 28 | 1. Use pip to install the kernel and the management utility 29 | 30 | ```bash 31 | pip install ghidra-jupyter 32 | ``` 33 | 34 | 2. Download the [latest release](https://github.com/GhidraJupyter/ghidra-jupyter-kotlin/releases/latest) zip from our releases page 35 | 3. Place the zip under `$GHIDRA_INSTALL_DIR/Ghidra/Extensions/` or select it via the GUI dialog to install extensions 36 | 37 | ## Usage 38 | 39 | After installation, you should be prompted about a new plugin when opening the CodeBrowser. Confirm the installation and activate it via "File -> Configure..." and ticking the checkbox for the "Miscellaneous" Group. 40 | 41 | Directly after you'll see 2 new buttons and a new menu inside Ghidra. 42 | 43 | ![Ghidra Buttons](resources/readme/buttons.png) 44 | 45 | ![Ghidra Menu](resources/readme/menu.png) 46 | 47 | 48 | The third action is only available in the menu and provides a shortcut 49 | to open an already running `juptyter-notebook` server or to start a new one. 50 | 51 | 52 | ### Kotlin QtConsole 53 | 54 | This feature requires the Jupyter QT Console to be installed and `jupyter-qtconsole` to be available in your `PATH`. This is a separate package on PyPI and in most distros, so you typically need to explicitly install it. 55 | 56 | Click the ![QtConsole] button to open a QtConsole. 57 | 58 | Once you click, a Jupyter Kernel will be initialized in the current Ghidra program 59 | and the Jupyter QtConsole will launch. If there is already a notebook kernel running, 60 | the console will use the same kernel (i.e. share the variables, functions, etc.) 61 | 62 | ![QtConsole Window](resources/readme/qtconsole_window.png) 63 | 64 | #### Caveats 65 | 66 | If you want to interrupt the code you executed, the menu action "Interrupt Current Kernel" or "Ctrl+C" will NOT work. It will simply print `Cannot interrupt a kernel I did not start.` 67 | 68 | This is a limitation of the Jupyter QT console. To work around this issue, the plugin provides an action `Interrupt Execution` in the `Jupyter` submenu. This will interrupt the currently executed cell: 69 | 70 | ![Interrupt Demo](resources/readme/interrupt_demo.png) 71 | 72 | ### Jupyter Terminal Console 73 | 74 | If you want to use the terminal based `jupyter-console` instead, 75 | open a CodeBrowser instance and in the top bar navigate to 76 | 77 | `Edit -> Tool Options ... -> JupyterKotlinPlugin` 78 | 79 | ![Options](resources/options.png) 80 | 81 | Here you can set the `CONSOLE_CMD` to whatever command opens your preferred terminal and runs 82 | `jupyter-console --existing` in it. The connection file will be appended to it, so the full command called later will be 83 | something like: 84 | 85 | ```sh 86 | kitty -- jupyter-console --existing /home/user/.local/share/jupyter/runtime/kernel-3af276c1-ac24-4368-bbb4-94cdf082fa7a.json 87 | ``` 88 | 89 | ### Jupyter Notebook 90 | 91 | 1. Start Jupyter Notebook or Jupyter Lab 92 | 93 | ```bash 94 | jupyter notebook 95 | ``` 96 | 97 | or by using the menu action. 98 | 99 | 2. Click the ![Notebook] button in Ghidra to accept a notebook connection 100 | 101 | The following popup will show, indicating that Ghidra is actively waiting 102 | 103 | ![Awaiting Connection](resources/readme/waiting.png) 104 | 105 | 3. In the Jupyter Notebook home page, create a Ghidra(Kotlin) notebook 106 | 107 | ![Create Notebook](resources/readme/create_notebook.png) 108 | 109 | Once you do, the notebook will connect to your waiting Ghidra instance. 110 | 111 | ![Jupyter Notebook](resources/readme/notebook_view.png) 112 | 113 | [QtConsole]:resources/readme/qtconsole.png 114 | [Notebook]:resources/readme/notebook.png 115 | 116 | ## Demo Snippets 117 | 118 | These snippets can be pasted directly in the QT console or a notebook cell. 119 | 120 | ### Kotlin Extensions 121 | 122 | [Extensions](https://kotlinlang.org/docs/extensions.html#extensions-are-resolved-statically) are a Kotlin feature which allows extending existing classes with new methods, properties or operators. This allows various convenience features, especially combined with other Kotlin Features like operator overloading and easily providing lambdas. They need to be explicitly imported in your script/kernel before using them: 123 | 124 | ```kotlin 125 | // Import all extensions in the GhidraJupyterKotlin.extensions.address package 126 | import GhidraJupyterKotlin.extensions.address.* 127 | ``` 128 | 129 | If you end up writing any kind of extension method/property/operator we would be happy to receive a PR. 130 | Not all extension provided are documented in the README.md, check the [extensions folder](./GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions) for all of them. Nearly all of them are fairly simple (a few lines at most) so they can also serve as good examples how to write your own. 131 | 132 | #### Explicit Database Transactions 133 | 134 | Unlike the Jython REPL, the Kotlin Kernel does NOT wrap each cell in an implicit Database transaction. Any attempt to modify the Database will result in `NoTransactionException: Transaction has not been started`. 135 | 136 | Instead, there is an extension method on the `UndoableDomainObject` interface, that makes Database transactions explicit with minimal syntactic overhead: 137 | 138 | ```kotlin 139 | import GhidraJupyterKotlin.extensions.misc.* 140 | 141 | currentProgram.runTransaction { 142 | /* your code modifying the DB */ 143 | currentProgram.name = "NewName" 144 | } 145 | ``` 146 | 147 | If the code throws any kind of Exception the transaction will be aborted and the changes will be discarded. 148 | 149 | This method can also be called with a transaction description: 150 | ```kotlin 151 | currentProgram.runTransaction("Transaction Description") { 152 | currentProgram.name = "NewName" 153 | } 154 | ``` 155 | 156 | For comparison, the regular Ghidra API for transactions: 157 | ```kotlin 158 | val transactionID = currentProgram.startTransaction("Transaction Description") 159 | /* your code modifying the DB */ 160 | currentProgram.name = "NewName" 161 | currentProgram.endTransaction(transactionID, true) // true means the changes should be committed to the DB 162 | ``` 163 | 164 | 165 | 166 | #### Address Arithmetic with Operators 167 | 168 | Unlike Java, Kotlin supports [operator overloading](https://kotlinlang.org/docs/operator-overloading.html). This can be used to make calculations involving addresses more comfortable: 169 | 170 | ```kotlin 171 | import GhidraJupyterKotlin.extensions.address.* 172 | import ghidra.program.model.address.Address 173 | import ghidra.program.model.address.AddressRange 174 | 175 | val x: Address = currentAddress + 0x10 // Address + Offset (Int or Long) 176 | val y: Address = currentAddress - 0x10 // Address - Offset (Int or Long) 177 | val z: Address = x - y // Difference between Addresses 178 | 179 | val range: AddressRange = y..x // The range of addresses between currentAddress-0x10 and currentAddress+0x10 180 | 181 | ``` 182 | 183 | ### Defining your own Imports and Helpers for Quick Access 184 | 185 | You can load a JSON file e.g. `decompiler.json` 186 | 187 | ```json 188 | { 189 | "link": "https://github.com/GhidraJupyter/ghidra-jupyter-kotlin", 190 | "description": "Shortcut to load features in Kotlin Kernel", 191 | "imports": [ 192 | "GhidraJupyterKotlin.extensions.address.*", 193 | "GhidraJupyterKotlin.extensions.data.*", 194 | "GhidraJupyterKotlin.extensions.misc.*", 195 | "ghidra.app.decompiler.*" 196 | ], 197 | "init" : [ 198 | "val ClangNode.parent: ClangTokenGroup; get() = this.Parent() as ClangTokenGroup", 199 | "val ClangNode.children: List; get() = (0..this.numChildren()-1).map(this::Child)", 200 | "val currentToken: ClangToken?; get() = (currentLocation as? DecompilerLocation)?.token" 201 | ] 202 | } 203 | ``` 204 | 205 | and then activate it: 206 | ``` 207 | %use /PATH/TO/FILE/decompiler 208 | ``` 209 | 210 | If the file is placed under `$HOME/.jupyter_kotlin/libraries`, you only need to use the name, e.g. `%use decompiler` 211 | 212 | When activated, the `imports` will be made available just like with a manual import 213 | and each line in `init` will be run once. This means that the extension methods defined in the imports are available too. 214 | You can also import other packages to easily access classes, e.g. for `(currentLocation as DecompilerLocation).token` 215 | 216 | The `init` block in this example defines three [Extension Properties](https://kotlinlang.org/docs/extensions.html#extension-properties). 217 | 1. Work around the annoying name of the `ClangNode.Parent()` function and 218 | the quirk that this should always be a `ClangTokenGroup`, but isn't typed as such in Ghidra itself. 219 | 2. A function that collects all the children of a node into a list (which then works with `.map` etc.) 220 | 3. Add a value/property to directly access the currently selected token. 221 | 222 | Combined this means you can now open a console via the GUI, run `%use decompiler`, click on a token in the decompiler, 223 | and run 224 | ``` 225 | In [1]: %use decompilerKotlin 226 | In [2]: currentToken.parent.children 227 | Out[2]: [pcVar2, , =, , "startUpdatingLocation"] 228 | ``` 229 | 230 | For a full list of possibilities check the [official documentation](https://github.com/Kotlin/kotlin-jupyter/blob/master/docs/libraries.md#creating-library-descriptor) 231 | 232 | 233 | ## Building the Ghidra Plugin 234 | 235 | (requires at least Ghidra 10.1 to be this easy, earlier versions require copying libraries around) 236 | 1. Run `gradlew buildExtension -PGHIDRA_INSTALL_DIR=/path/to/ghidra_10.1_PUBLIC` and gradle should take care of 237 | pulling the dependencies, copying them to the `./lib` folder, and building the extension zip to 238 | `./GhidraJupyterKotlin/dist/*zip` 239 | 2. Install the plugin using the ghidra-jupyter installer 240 | ```bash 241 | ghidra-jupyter install-extension --extension-path GhidraJupyterKotlin/dist/ 242 | ``` 243 | 244 | ### Development (Ghidra Extension itself, Kotlin GhidraScripts and Kotlin Extension Methods) 245 | 246 | Developing this extension is only tested and supported with IntelliJ, which isn't officially supported as an IDE for 247 | Ghidra Extensions. 248 | 249 | First make sure that your `GHIDRA_INSTALL_DIR` variable is set in some way that gradle recognizes, 250 | e.g. by adding the line `GHIDRA_INSTALL_DIR=/path/to/ghidra_10.1_PUBLIC/` 251 | to the global gradle config in `$HOME/.gradle/gradle.properties` 252 | 253 | Now it should be enough to import the `settings.gradle` file via the 254 | IntelliJ IDEA "Project From Existing Sources" menu. 255 | IntelliJ will import the gradle project, fetch dependencies and 256 | afterwards you can click the `Build` button (Hammer in the top right menu) and it should compile successfully. 257 | If there is an error during importing or building please open an issue here on GitHub. 258 | 259 | The repo also includes a run configuration under `.idea/runConfigurations/Ghidra_GUI.xml` that IntelliJ should 260 | automatically pick up after the project was created, which will add a run configuration called `Ghidra GUI`. 261 | This run configuration will launch Ghidra with the current 262 | development code of the plugin. It will look fairly ugly because none of the VM options that influence that are included. 263 | To remedy this, generate the VM options for your system and set them as the VM options in the run configuration. 264 | 265 | ```sh 266 | cd $GHIDRA_INSTALL_DIR 267 | java -cp ./support/LaunchSupport.jar LaunchSupport ./support/.. -vmargs 268 | ``` 269 | 270 | 271 | ## Licenses 272 | 273 | This project is released under the MIT license. 274 | 275 | The project uses components that are released under different licenses: 276 | 277 | - [kotlin-jupyter](https://github.com/Kotlin/kotlin-jupyter) is released under the Apache-2.0 License 278 | - The Kotlin runtime and libraries are released under the Apache-2.0 License 279 | -------------------------------------------------------------------------------- /ghidra_jupyter/kernel/kernel.css: -------------------------------------------------------------------------------- 1 | /* 2 | This file includes code of JupyterLab project (https://github.com/jupyterlab/jupyterlab) 3 | which is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2015 Project Jupyter Contributors 7 | 8 | All rights reserved. 9 | 10 | Full license text is available in additional-licenses/LICENSE_BSD_3 file 11 | */ 12 | 13 | :root { 14 | /* Elevation 15 | * 16 | * We style box-shadows using Material Design's idea of elevation. These particular numbers are taken from here: 17 | * 18 | * https://github.com/material-components/material-components-web 19 | * https://material-components-web.appspot.com/elevation.html 20 | */ 21 | 22 | --md-grey-900: #212121; 23 | --md-grey-400: #bdbdbd; 24 | --md-grey-200: #eeeeee; 25 | --md-blue-500: #2196f3; 26 | 27 | 28 | /* The dark theme shadows need a bit of work, but this will probably also require work on the core layout 29 | * colors used in the theme as well. */ 30 | --jp-shadow-base-lightness: 32; 31 | --jp-shadow-umbra-color: rgba( 32 | var(--jp-shadow-base-lightness), 33 | var(--jp-shadow-base-lightness), 34 | var(--jp-shadow-base-lightness), 35 | 0.2 36 | ); 37 | --jp-shadow-penumbra-color: rgba( 38 | var(--jp-shadow-base-lightness), 39 | var(--jp-shadow-base-lightness), 40 | var(--jp-shadow-base-lightness), 41 | 0.14 42 | ); 43 | --jp-shadow-ambient-color: rgba( 44 | var(--jp-shadow-base-lightness), 45 | var(--jp-shadow-base-lightness), 46 | var(--jp-shadow-base-lightness), 47 | 0.12 48 | ); 49 | --jp-elevation-z0: none; 50 | --jp-elevation-z1: 0px 2px 1px -1px var(--jp-shadow-umbra-color), 51 | 0px 1px 1px 0px var(--jp-shadow-penumbra-color), 52 | 0px 1px 3px 0px var(--jp-shadow-ambient-color); 53 | --jp-elevation-z2: 0px 3px 1px -2px var(--jp-shadow-umbra-color), 54 | 0px 2px 2px 0px var(--jp-shadow-penumbra-color), 55 | 0px 1px 5px 0px var(--jp-shadow-ambient-color); 56 | --jp-elevation-z4: 0px 2px 4px -1px var(--jp-shadow-umbra-color), 57 | 0px 4px 5px 0px var(--jp-shadow-penumbra-color), 58 | 0px 1px 10px 0px var(--jp-shadow-ambient-color); 59 | --jp-elevation-z6: 0px 3px 5px -1px var(--jp-shadow-umbra-color), 60 | 0px 6px 10px 0px var(--jp-shadow-penumbra-color), 61 | 0px 1px 18px 0px var(--jp-shadow-ambient-color); 62 | --jp-elevation-z8: 0px 5px 5px -3px var(--jp-shadow-umbra-color), 63 | 0px 8px 10px 1px var(--jp-shadow-penumbra-color), 64 | 0px 3px 14px 2px var(--jp-shadow-ambient-color); 65 | --jp-elevation-z12: 0px 7px 8px -4px var(--jp-shadow-umbra-color), 66 | 0px 12px 17px 2px var(--jp-shadow-penumbra-color), 67 | 0px 5px 22px 4px var(--jp-shadow-ambient-color); 68 | --jp-elevation-z16: 0px 8px 10px -5px var(--jp-shadow-umbra-color), 69 | 0px 16px 24px 2px var(--jp-shadow-penumbra-color), 70 | 0px 6px 30px 5px var(--jp-shadow-ambient-color); 71 | --jp-elevation-z20: 0px 10px 13px -6px var(--jp-shadow-umbra-color), 72 | 0px 20px 31px 3px var(--jp-shadow-penumbra-color), 73 | 0px 8px 38px 7px var(--jp-shadow-ambient-color); 74 | --jp-elevation-z24: 0px 11px 15px -7px var(--jp-shadow-umbra-color), 75 | 0px 24px 38px 3px var(--jp-shadow-penumbra-color), 76 | 0px 9px 46px 8px var(--jp-shadow-ambient-color); 77 | 78 | /* Borders 79 | * 80 | * The following variables, specify the visual styling of borders in JupyterLab. 81 | */ 82 | 83 | --jp-border-width: 1px; 84 | --jp-border-radius: 2px; 85 | 86 | /* UI Fonts 87 | * 88 | * The UI font CSS variables are used for the typography all of the JupyterLab 89 | * user interface elements that are not directly user generated content. 90 | * 91 | * The font sizing here is done assuming that the body font size of --jp-ui-font-size1 92 | * is applied to a parent element. When children elements, such as headings, are sized 93 | * in em all things will be computed relative to that body size. 94 | */ 95 | 96 | --jp-ui-font-scale-factor: 1.2; 97 | --jp-ui-font-size0: 0.83333em; 98 | --jp-ui-font-size1: 13px; /* Base font size */ 99 | --jp-ui-font-size2: 1.2em; 100 | --jp-ui-font-size3: 1.44em; 101 | 102 | --jp-ui-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 103 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 104 | 105 | /* 106 | * Use these font colors against the corresponding main layout colors. 107 | * In a light theme, these go from dark to light. 108 | */ 109 | 110 | /* Defaults use Material Design specification */ 111 | --jp-ui-font-color0: rgba(0, 0, 0, 1); 112 | --jp-ui-font-color1: rgba(0, 0, 0, 0.87); 113 | --jp-ui-font-color2: rgba(0, 0, 0, 0.54); 114 | --jp-ui-font-color3: rgba(0, 0, 0, 0.38); 115 | 116 | /* 117 | * Use these against the brand/accent/warn/error colors. 118 | * These will typically go from light to darker, in both a dark and light theme. 119 | */ 120 | 121 | --jp-ui-inverse-font-color0: rgba(0, 0, 0, 1); 122 | --jp-ui-inverse-font-color1: rgba(0, 0, 0, 0.8); 123 | --jp-ui-inverse-font-color2: rgba(0, 0, 0, 0.5); 124 | --jp-ui-inverse-font-color3: rgba(0, 0, 0, 0.3); 125 | 126 | /* Content Fonts 127 | * 128 | * Content font variables are used for typography of user generated content. 129 | * 130 | * The font sizing here is done assuming that the body font size of --jp-content-font-size1 131 | * is applied to a parent element. When children elements, such as headings, are sized 132 | * in em all things will be computed relative to that body size. 133 | */ 134 | 135 | --jp-content-line-height: 1.6; 136 | --jp-content-font-scale-factor: 1.2; 137 | --jp-content-font-size0: 0.83333em; 138 | --jp-content-font-size1: 14px; /* Base font size */ 139 | --jp-content-font-size2: 1.2em; 140 | --jp-content-font-size3: 1.44em; 141 | --jp-content-font-size4: 1.728em; 142 | --jp-content-font-size5: 2.0736em; 143 | 144 | /* This gives a magnification of about 125% in presentation mode over normal. */ 145 | --jp-content-presentation-font-size1: 17px; 146 | 147 | --jp-content-heading-line-height: 1; 148 | --jp-content-heading-margin-top: 1.2em; 149 | --jp-content-heading-margin-bottom: 0.8em; 150 | --jp-content-heading-font-weight: 500; 151 | 152 | /* Defaults use Material Design specification */ 153 | --jp-content-font-color0: rgba(0, 0, 0, 1); 154 | --jp-content-font-color1: rgba(0, 0, 0, 0.87); 155 | --jp-content-font-color2: rgba(0, 0, 0, 0.54); 156 | --jp-content-font-color3: rgba(0, 0, 0, 0.38); 157 | 158 | 159 | --jp-content-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 160 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 161 | 'Segoe UI Symbol'; 162 | 163 | /* 164 | * Code Fonts 165 | * 166 | * Code font variables are used for typography of code and other monospaces content. 167 | */ 168 | 169 | --jp-code-font-size: 13px; 170 | --jp-code-line-height: 1.3077; /* 17px for 13px base */ 171 | --jp-code-padding: 5px; /* 5px for 13px base, codemirror highlighting needs integer px value */ 172 | --jp-code-font-family-default: Menlo, Consolas, 'DejaVu Sans Mono', monospace; 173 | --jp-code-font-family: var(--jp-code-font-family-default); 174 | 175 | /* This gives a magnification of about 125% in presentation mode over normal. */ 176 | --jp-code-presentation-font-size: 16px; 177 | 178 | /* may need to tweak cursor width if you change font size */ 179 | --jp-code-cursor-width0: 1.4px; 180 | --jp-code-cursor-width1: 2px; 181 | --jp-code-cursor-width2: 4px; 182 | 183 | /* Layout 184 | * 185 | * The following are the main layout colors use in JupyterLab. In a light 186 | * theme these would go from light to dark. 187 | */ 188 | 189 | --jp-layout-color0: #111111; 190 | 191 | /* Inverse Layout 192 | * 193 | * The following are the inverse layout colors use in JupyterLab. In a light 194 | * theme these would go from dark to light. 195 | */ 196 | 197 | --jp-inverse-layout-color0: white; 198 | --jp-inverse-layout-color1: white; 199 | 200 | /* Brand/accent */ 201 | 202 | 203 | /* State colors (warn, error, success, info) */ 204 | 205 | 206 | /* Cell specific styles */ 207 | 208 | --jp-cell-padding: 5px; 209 | 210 | --jp-cell-collapser-width: 8px; 211 | --jp-cell-collapser-min-height: 20px; 212 | --jp-cell-collapser-not-active-hover-opacity: 0.6; 213 | 214 | --jp-cell-editor-active-background: var(--jp-layout-color0); 215 | 216 | --jp-cell-prompt-width: 64px; 217 | --jp-cell-prompt-font-family: 'Source Code Pro', monospace; 218 | --jp-cell-prompt-letter-spacing: 0px; 219 | --jp-cell-prompt-opacity: 1; 220 | --jp-cell-prompt-not-active-opacity: 1; 221 | 222 | /* A custom blend of MD grey and blue 600 223 | * See https://meyerweb.com/eric/tools/color-blend/#546E7A:1E88E5:5:hex */ 224 | --jp-cell-inprompt-font-color: #307fc1; 225 | /* A custom blend of MD grey and orange 600 226 | * https://meyerweb.com/eric/tools/color-blend/#546E7A:F4511E:5:hex */ 227 | --jp-cell-outprompt-font-color: #bf5b3d; 228 | 229 | /* Notebook specific styles */ 230 | 231 | --jp-notebook-padding: 10px; 232 | --jp-notebook-multiselected-color: rgba(33, 150, 243, 0.24); 233 | 234 | /* The scroll padding is calculated to fill enough space at the bottom of the 235 | notebook to show one single-line cell (with appropriate padding) at the top 236 | when the notebook is scrolled all the way to the bottom. We also subtract one 237 | pixel so that no scrollbar appears if we have just one single-line cell in the 238 | notebook. This padding is to enable a 'scroll past end' feature in a notebook. 239 | */ 240 | --jp-notebook-scroll-padding: calc( 241 | 100% - var(--jp-code-font-size) * var(--jp-code-line-height) - 242 | var(--jp-code-padding) - var(--jp-cell-padding) - 1px 243 | ); 244 | 245 | /* Rendermime styles */ 246 | 247 | --jp-rendermime-error-background: rgba(244, 67, 54, 0.28); 248 | --jp-rendermime-table-row-hover-background: rgba(3, 169, 244, 0.2); 249 | 250 | /* Dialog specific styles */ 251 | 252 | --jp-dialog-background: rgba(0, 0, 0, 0.6); 253 | 254 | /* Console specific styles */ 255 | 256 | --jp-console-padding: 10px; 257 | 258 | /* Toolbar specific styles */ 259 | 260 | --jp-toolbar-micro-height: 8px; 261 | --jp-toolbar-box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.8); 262 | --jp-toolbar-header-margin: 4px 4px 0px 4px; 263 | --jp-toolbar-active-background: var(--jp-layout-color0); 264 | 265 | /* Input field styles */ 266 | 267 | --jp-input-active-background: var(--jp-layout-color0); 268 | --jp-input-active-box-shadow-color: rgba(19, 124, 189, 0.3); 269 | 270 | /* General editor styles */ 271 | 272 | --jp-editor-selected-focused-background: rgba(33, 150, 243, 0.24); 273 | --jp-editor-cursor-color: var(--jp-ui-font-color0); 274 | 275 | /* Code mirror specific styles */ 276 | 277 | --jp-mirror-editor-operator-color: #aa22ff; 278 | --jp-mirror-editor-comment-color: #408080; 279 | --jp-mirror-editor-string-color: #ba2121; 280 | --jp-mirror-editor-meta-color: #aa22ff; 281 | --jp-mirror-editor-qualifier-color: #555; 282 | --jp-mirror-editor-bracket-color: #997; 283 | --jp-mirror-editor-error-color: #f00; 284 | --jp-mirror-editor-hr-color: #999; 285 | 286 | /* Sidebar-related styles */ 287 | 288 | --jp-sidebar-min-width: 180px; 289 | 290 | /* Search-related styles */ 291 | 292 | --jp-search-toggle-off-opacity: 0.6; 293 | --jp-search-toggle-hover-opacity: 0.8; 294 | --jp-search-toggle-on-opacity: 1; 295 | --jp-search-selected-match-background-color: rgb(255, 225, 0); 296 | --jp-search-selected-match-color: black; 297 | --jp-search-unselected-match-background-color: var( 298 | --jp-inverse-layout-color0 299 | ); 300 | --jp-search-unselected-match-color: var(--jp-ui-inverse-font-color0); 301 | 302 | /* scrollbar related styles. Supports every browser except Edge. */ 303 | 304 | /* colors based on JetBrain's Darcula theme */ 305 | 306 | --jp-scrollbar-background-color: #3f4244; 307 | --jp-scrollbar-thumb-color: 88, 96, 97; /* need to specify thumb color as an RGB triplet */ 308 | 309 | --jp-scrollbar-endpad: 3px; /* the minimum gap between the thumb and the ends of a scrollbar */ 310 | 311 | /* hacks for setting the thumb shape. These do nothing in Firefox */ 312 | 313 | --jp-scrollbar-thumb-margin: 3.5px; /* the space in between the sides of the thumb and the track */ 314 | --jp-scrollbar-thumb-radius: 9px; /* set to a large-ish value for rounded endcaps on the thumb */ 315 | 316 | 317 | --jp-layout-color1: white; 318 | --jp-border-color1: var(--md-grey-400); 319 | --jp-layout-color2: var(--md-grey-200); 320 | --jp-brand-color1: var(--md-blue-500); 321 | 322 | --jp-private-completer-item-height: 22px; 323 | /* Shift the baseline of the type character to align with the match text */ 324 | --jp-private-completer-type-offset: 2px; 325 | } 326 | 327 | #site { 328 | position: relative !important; 329 | } 330 | 331 | .completions { 332 | /*position: relative !important;*/ 333 | } 334 | 335 | .jp-Completer { 336 | box-shadow: var(--jp-elevation-z6); 337 | background: var(--jp-layout-color1); 338 | color: var(--jp-content-font-color1); 339 | border: var(--jp-border-width) solid var(--jp-border-color1); 340 | list-style-type: none; 341 | overflow-y: scroll; 342 | overflow-x: auto; 343 | padding: 0; 344 | /* Position the completer relative to the text editor, align the '.' */ 345 | margin: 0 0 0 0; 346 | max-height: calc( 347 | (10 * var(--jp-private-completer-item-height)) + 348 | (2 * var(--jp-border-width)) 349 | ); 350 | min-height: calc( 351 | var(--jp-private-completer-item-height) + (2 * var(--jp-border-width)) 352 | ); 353 | z-index: 10001; 354 | } 355 | 356 | .jp-Completer-item { 357 | display: table-row; 358 | box-sizing: border-box; 359 | margin: 0; 360 | padding: 0; 361 | height: var(--jp-private-completer-item-height); 362 | min-width: 150px; 363 | } 364 | 365 | .jp-Completer-item .jp-Completer-match { 366 | display: table-cell; 367 | box-sizing: border-box; 368 | margin: 0; 369 | padding: 0 8px 0 6px; 370 | height: var(--jp-private-completer-item-height); 371 | font-family: var(--jp-code-font-family); 372 | font-size: var(--jp-code-font-size); 373 | line-height: var(--jp-private-completer-item-height); 374 | } 375 | 376 | .jp-Completer-item .jp-Completer-match.jp-Completer-deprecated { 377 | text-decoration: line-through; 378 | } 379 | 380 | .jp-Completer-item .jp-Completer-type { 381 | display: table-cell; 382 | box-sizing: border-box; 383 | height: var(--jp-private-completer-item-height); 384 | text-align: center; 385 | background: transparent; 386 | color: white; 387 | width: var(--jp-private-completer-item-height); 388 | font-family: var(--jp-ui-font-family); 389 | font-size: var(--jp-ui-font-size1); 390 | line-height: calc( 391 | var(--jp-private-completer-item-height) - 392 | var(--jp-private-completer-type-offset) 393 | ); 394 | padding-bottom: var(--jp-private-completer-type-offset); 395 | } 396 | 397 | .jp-Completer-item .jp-Completer-typeExtended { 398 | display: table-cell; 399 | box-sizing: border-box; 400 | height: var(--jp-private-completer-item-height); 401 | text-align: right; 402 | background: transparent; 403 | color: var(--jp-ui-font-color2); 404 | font-family: var(--jp-code-font-family); 405 | font-size: var(--jp-code-font-size); 406 | line-height: var(--jp-private-completer-item-height); 407 | padding-right: 8px; 408 | } 409 | 410 | .jp-Completer-item:hover { 411 | background: var(--jp-layout-color2); 412 | opacity: 0.8; 413 | } 414 | 415 | .jp-Completer-item.jp-mod-active { 416 | background: var(--jp-brand-color1); 417 | color: white; 418 | } 419 | 420 | .jp-Completer-item .jp-Completer-match mark { 421 | font-weight: bold; 422 | background: inherit; 423 | color: inherit; 424 | } 425 | 426 | .jp-Completer-type[data-color-index='0'] { 427 | background: transparent; 428 | } 429 | 430 | .jp-Completer-type[data-color-index='1'] { 431 | background: #1f77b4; 432 | } 433 | 434 | .jp-Completer-type[data-color-index='2'] { 435 | background: #ff7f0e; 436 | } 437 | 438 | .jp-Completer-type[data-color-index='3'] { 439 | background: #2ca02c; 440 | } 441 | 442 | .jp-Completer-type[data-color-index='4'] { 443 | background: #d62728; 444 | } 445 | 446 | .jp-Completer-type[data-color-index='5'] { 447 | background: #9467bd; 448 | } 449 | 450 | .jp-Completer-type[data-color-index='6'] { 451 | background: #8c564b; 452 | } 453 | 454 | .jp-Completer-type[data-color-index='7'] { 455 | background: #e377c2; 456 | } 457 | 458 | .jp-Completer-type[data-color-index='8'] { 459 | background: #7f7f7f; 460 | } 461 | 462 | .jp-Completer-type[data-color-index='9'] { 463 | background: #bcbd22; 464 | } 465 | 466 | .jp-Completer-type[data-color-index='10'] { 467 | background: #17becf; 468 | } 469 | 470 | .cm__red_wavy_line { 471 | background: url("./wavyline-red.gif") repeat-x 100% 100%; 472 | padding-bottom: 2px; 473 | } 474 | 475 | .cm__orange_wavy_line { 476 | background: url("./wavyline-orange.gif") repeat-x 100% 100%; 477 | padding-bottom: 2px; 478 | } 479 | 480 | .kotlin-error-tooltip { 481 | border: 1px solid silver; 482 | border-radius: 3px; 483 | color: #444; 484 | padding: 2px 5px; 485 | font-size: 90%; 486 | font-family: monospace; 487 | background-color: white; 488 | white-space: pre-wrap; 489 | 490 | max-width: 40em; 491 | position: absolute; 492 | z-index: 10; 493 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 494 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 495 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 496 | 497 | transition: opacity 1s; 498 | -moz-transition: opacity 1s; 499 | -webkit-transition: opacity 1s; 500 | -o-transition: opacity 1s; 501 | -ms-transition: opacity 1s; 502 | } 503 | -------------------------------------------------------------------------------- /ghidra_jupyter/kernel/kernel.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file includes code of Jupyter notebook project (https://github.com/jupyter/notebook) 3 | which is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2001-2015, IPython Development Team 7 | - Copyright (c) 2015-, Jupyter Development Team 8 | 9 | All rights reserved. 10 | 11 | Full license text is available in additional-licenses/LICENSE_BSD_3 file 12 | */ 13 | 14 | define(function(){ 15 | function onload() { 16 | if (!Element.prototype.scrollIntoViewIfNeeded) { 17 | Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { 18 | centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; 19 | 20 | var parent = this.parentNode, 21 | parentComputedStyle = window.getComputedStyle(parent, null), 22 | parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), 23 | parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), 24 | overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, 25 | overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight), 26 | overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft, 27 | overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth), 28 | alignWithTop = overTop && !overBottom; 29 | 30 | if ((overTop || overBottom) && centerIfNeeded) { 31 | parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; 32 | } 33 | 34 | if ((overLeft || overRight) && centerIfNeeded) { 35 | parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; 36 | } 37 | 38 | if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { 39 | this.scrollIntoView(alignWithTop); 40 | } 41 | }; 42 | } 43 | 44 | var utils = require('base/js/utils'); 45 | var keyboard = require('base/js/keyboard'); 46 | var $ = require('jquery'); 47 | var CodeMirror = require('codemirror/lib/codemirror'); 48 | var Completer = requirejs("notebook/js/completer").Completer; 49 | var Cell = requirejs("notebook/js/cell").Cell; 50 | var CodeCell = requirejs("notebook/js/codecell").CodeCell; 51 | var Kernel = requirejs("services/kernels/kernel").Kernel; 52 | 53 | var error_class = "cm__red_wavy_line"; 54 | var warning_class = "cm__orange_wavy_line"; 55 | var additionalClasses = [error_class, warning_class]; 56 | var diag_class = { 57 | "ERROR": error_class, 58 | "WARNING": warning_class 59 | }; 60 | 61 | function rectIntersect(a, b) { 62 | return (a.left <= b.right && 63 | b.left <= a.right && 64 | a.top <= b.bottom && 65 | b.top <= a.bottom) 66 | } 67 | 68 | function isOnScreen(elem) { 69 | var bounding = elem.getBoundingClientRect(); 70 | return rectIntersect( 71 | bounding, 72 | { 73 | top: 0, 74 | left: 0, 75 | bottom: (window.innerHeight || document.documentElement.clientHeight), 76 | right: (window.innerWidth || document.documentElement.clientWidth) 77 | } 78 | ) 79 | } 80 | 81 | var opened_completer = null; 82 | var siteEl = document.getElementById("site"); 83 | $(siteEl).scroll((e) => { 84 | if (opened_completer && !isOnScreen(opened_completer.complete[0])) { 85 | opened_completer.close(); 86 | } 87 | }); 88 | 89 | var cssUrl = require.toUrl(Jupyter.kernelselector.kernelspecs.kotlin.resources["kernel.css"]); 90 | $('head').append(''); 91 | 92 | var keycodes = keyboard.keycodes; 93 | 94 | var prepend_n_prc = function(str, n) { 95 | for( var i =0 ; i< n ; i++){ 96 | str = '%'+str ; 97 | } 98 | return str; 99 | }; 100 | 101 | var _existing_completion = function(item, completion_array){ 102 | for( var i=0; i < completion_array.length; i++) { 103 | if (completion_array[i].trim().substr(-item.length) == item) { 104 | return true; 105 | } 106 | } 107 | return false; 108 | }; 109 | 110 | // what is the common start of all completions 111 | function shared_start(B, drop_prct) { 112 | if (B.length == 1) { 113 | return B[0]; 114 | } 115 | var A = []; 116 | var common; 117 | var min_lead_prct = 10; 118 | for (var i = 0; i < B.length; i++) { 119 | var str = B[i].replaceText; 120 | var localmin = 0; 121 | if(drop_prct === true){ 122 | while ( str.substr(0, 1) == '%') { 123 | localmin = localmin+1; 124 | str = str.substring(1); 125 | } 126 | } 127 | min_lead_prct = Math.min(min_lead_prct, localmin); 128 | A.push(str); 129 | } 130 | 131 | if (A.length > 1) { 132 | var tem1, tem2, s; 133 | A = A.slice(0).sort(); 134 | tem1 = A[0]; 135 | s = tem1.length; 136 | tem2 = A.pop(); 137 | while (s && tem2.indexOf(tem1) == -1) { 138 | tem1 = tem1.substring(0, --s); 139 | } 140 | if (tem1 === "" || tem2.indexOf(tem1) !== 0) { 141 | return { 142 | replaceText: prepend_n_prc('', min_lead_prct), 143 | type: "computed", 144 | from: B[0].from, 145 | to: B[0].to 146 | }; 147 | } 148 | return { 149 | replaceText: prepend_n_prc(tem1, min_lead_prct), 150 | type: "computed", 151 | from: B[0].from, 152 | to: B[0].to 153 | }; 154 | } 155 | return null; 156 | } 157 | 158 | Completer.prototype.startCompletion = function (doAutoPrint) { 159 | /** 160 | * call for a 'first' completion, that will set the editor and do some 161 | * special behavior like autopicking if only one completion available. 162 | */ 163 | this.do_auto_print = !!doAutoPrint; 164 | if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return; 165 | this.done = false; 166 | // use to get focus back on opera 167 | this.carry_on_completion(true); 168 | }; 169 | 170 | function indexOf(str, filter, from, to, step) { 171 | var cond = (from < to) ? (j => j <= to) : (j => j >= to); 172 | for (var i = from; cond(i); i += step) { 173 | if (filter(str[i])) 174 | return i; 175 | } 176 | return -1; 177 | } 178 | 179 | function getTokenBounds(buf, cursor, editor) { 180 | if (cursor > buf.length) { 181 | throw new Error("Position " + cursor + " does not exist in code snippet <" + buf + ">"); 182 | } 183 | 184 | var filter = c => !/^[A-Z0-9_]$/i.test(c); 185 | 186 | var start = indexOf(buf, filter, cursor - 1, 0, -1) + 1; 187 | var end = cursor 188 | 189 | return { 190 | before: buf.substring(0, start), 191 | token: buf.substring(start, end), 192 | tokenBeforeCursor: buf.substring(start, cursor), 193 | after: buf.substring(end, buf.length), 194 | start: start, 195 | end: end, 196 | posStart: editor.posFromIndex(start), 197 | posEnd: editor.posFromIndex(end) 198 | } 199 | } 200 | 201 | Completer.prototype.carry_on_completion = function (first_invocation) { 202 | /** 203 | * Pass true as parameter if you want the completer to autopick when 204 | * only one completion. This function is automatically reinvoked at 205 | * each keystroke with first_invocation = false 206 | */ 207 | var cur = this.editor.getCursor(); 208 | var line = this.editor.getLine(cur.line); 209 | var pre_cursor = this.editor.getRange({ 210 | line: cur.line, 211 | ch: cur.ch - 1 212 | }, cur); 213 | 214 | // we need to check that we are still on a word boundary 215 | // because while typing the completer is still reinvoking itself 216 | // so dismiss if we are on a "bad" character 217 | if (!this.reinvoke(pre_cursor) && !first_invocation) { 218 | this.close(); 219 | return; 220 | } 221 | 222 | this.autopick = !!first_invocation; 223 | 224 | // We want a single cursor position. 225 | if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) { 226 | return; 227 | } 228 | 229 | var cursor_pos = this.editor.indexFromPos(cur); 230 | var text = this.editor.getValue(); 231 | cursor_pos = utils.js_idx_to_char_idx(cursor_pos, text); 232 | 233 | var prevBounds = this.tokenBounds; 234 | var bounds = getTokenBounds(text, cursor_pos, this.editor); 235 | this.tokenBounds = bounds; 236 | if (prevBounds && this.raw_result) { 237 | if (bounds.before === prevBounds.before && 238 | bounds.after === prevBounds.after && 239 | bounds.end > prevBounds.end) { 240 | 241 | var newResult = this.raw_result.filter((v) => { 242 | var displayName = v.str; 243 | if (displayName[0] === '`') 244 | displayName = displayName.substring(1, displayName.length - 1); 245 | return displayName.startsWith(bounds.tokenBeforeCursor) 246 | }).map((completion) => { 247 | completion.from = bounds.posStart; 248 | completion.to = bounds.posEnd; 249 | return completion; 250 | }); 251 | 252 | if (newResult.length > 0) { 253 | this.raw_result = newResult; 254 | this.make_gui(this.prepare_cursor_pos(bounds.start, cursor_pos)); 255 | return; 256 | } 257 | } 258 | } 259 | 260 | // one kernel completion came back, finish_completing will be called with the results 261 | // we fork here and directly call finish completing if kernel is busy 262 | 263 | if (this.skip_kernel_completion) { 264 | this.finish_completing({ content: { 265 | matches: [], 266 | cursor_start: cursor_pos, 267 | cursor_end: cursor_pos, 268 | }}); 269 | } else { 270 | this.cell.kernel.complete(text, cursor_pos, 271 | $.proxy(this.finish_completing, this) 272 | ); 273 | } 274 | }; 275 | 276 | function convertPos(pos) { 277 | return { 278 | line: pos.line - 1, 279 | ch: pos.col - 1 280 | } 281 | } 282 | 283 | function adjustRange(start, end) { 284 | var s = convertPos(start); 285 | var e = convertPos(end); 286 | if (s.line === e.line && s.ch === e.ch) { 287 | s.ch --; 288 | } 289 | return { 290 | start: s, 291 | end: e 292 | } 293 | } 294 | 295 | function highlightErrors (errors, cell) { 296 | errors = errors || []; 297 | for (let error of errors) { 298 | var start = error.start; 299 | var end = error.end; 300 | if (!start || !end) 301 | continue; 302 | var r = adjustRange(start, end); 303 | error.range = r; 304 | var cl = diag_class[error.severity]; 305 | cell.code_mirror.markText(r.start, r.end, {className: cl}); 306 | } 307 | 308 | cell.errorsList = errors; 309 | } 310 | 311 | Cell.prototype.highlightErrors = function (errors) { 312 | highlightErrors(errors, this) 313 | }; 314 | 315 | Completer.prototype.make_gui = function(cur_pos, cur) { 316 | var start = cur_pos.start; 317 | cur = cur || this.editor.getCursor(); 318 | 319 | // if empty result return 320 | if (!this.raw_result || !this.raw_result.length) { 321 | this.close(); 322 | return; 323 | } 324 | 325 | // When there is only one completion, use it directly. 326 | if (this.do_auto_print && this.autopick && this.raw_result.length == 1) { 327 | this.insert(this.raw_result[0]); 328 | return; 329 | } 330 | 331 | if (this.raw_result.length == 1) { 332 | // test if first and only completion totally matches 333 | // what is typed, in this case dismiss 334 | var str = this.raw_result[0].str; 335 | var pre_cursor = this.editor.getRange({ 336 | line: cur.line, 337 | ch: cur.ch - str.length 338 | }, cur); 339 | if (pre_cursor == str) { 340 | this.close(); 341 | return; 342 | } 343 | } 344 | 345 | if (!this.visible) { 346 | this.complete = $('
').addClass('completions'); 347 | this.complete.attr('id', 'complete'); 348 | 349 | // Currently webkit doesn't use the size attr correctly. See: 350 | // https://code.google.com/p/chromium/issues/detail?id=4579 351 | this.sel = $('
    ') 352 | .attr('tabindex', -1) 353 | .attr('multiple', 'true') 354 | .addClass('jp-Completer'); 355 | this.complete.append(this.sel); 356 | this.visible = true; 357 | $(siteEl).append(this.complete); 358 | 359 | //build the container 360 | var that = this; 361 | this._handle_keydown = function (cm, event) { 362 | that.keydown(event); 363 | }; 364 | this.editor.on('keydown', this._handle_keydown); 365 | this._handle_keypress = function (cm, event) { 366 | that.keypress(event); 367 | }; 368 | this.editor.on('keypress', this._handle_keypress); 369 | } 370 | this.sel.attr('size', Math.min(10, this.raw_result.length)); 371 | 372 | // Clear and fill the list. 373 | this.sel.text(''); 374 | this.build_gui_list(this.raw_result); 375 | 376 | // After everything is on the page, compute the position. 377 | // We put it above the code if it is too close to the bottom of the page. 378 | var pos = this.editor.cursorCoords( 379 | this.editor.posFromIndex(start) 380 | ); 381 | var left = pos.left-3; 382 | var top; 383 | var cheight = this.complete.height(); 384 | var wheight = $(window).height(); 385 | if (pos.bottom+cheight+5 > wheight) { 386 | top = pos.top-cheight-4; 387 | } else { 388 | top = pos.bottom+1; 389 | } 390 | 391 | this.complete.css('left', left + siteEl.scrollLeft - siteEl.offsetLeft + 'px'); 392 | this.complete.css('top', top + siteEl.scrollTop - siteEl.offsetTop + 'px'); 393 | 394 | opened_completer = this; 395 | return true; 396 | }; 397 | 398 | Completer.prototype.prepare_cursor_pos = function(start, end) { 399 | if (end === null) { 400 | // adapted message spec replies don't have cursor position info, 401 | // interpret end=null as current position, 402 | // and negative start relative to that 403 | end = this.editor.indexFromPos(cur); 404 | if (start === null) { 405 | start = end; 406 | } else if (start < 0) { 407 | start = end + start; 408 | } 409 | } else { 410 | // handle surrogate pairs 411 | var text = this.editor.getValue(); 412 | end = utils.char_idx_to_js_idx(end, text); 413 | start = utils.char_idx_to_js_idx(start, text); 414 | } 415 | 416 | return { 417 | start: start, 418 | end: end 419 | } 420 | }; 421 | 422 | Completer.prototype.finish_completing = function (msg) { 423 | /** 424 | * let's build a function that wrap all that stuff into what is needed 425 | * for the new completer: 426 | */ 427 | 428 | // alert("WOW"); 429 | 430 | var content = msg.content; 431 | var start = content.cursor_start; 432 | var end = content.cursor_end; 433 | var matches = content.matches; 434 | var metadata = content.metadata || {}; 435 | var extMetadata = metadata._jupyter_extended_metadata || {}; 436 | 437 | console.log(content); 438 | 439 | // If completion error occurs 440 | if (matches === undefined) { 441 | return; 442 | } 443 | 444 | var cur = this.editor.getCursor(); 445 | 446 | var paragraph = content.paragraph; 447 | if (paragraph) { 448 | if (paragraph.cursor !== this.editor.indexFromPos(cur) 449 | || paragraph.text !== this.editor.getValue()) { 450 | // this.close(); 451 | return; 452 | } 453 | } 454 | 455 | var newPos = this.prepare_cursor_pos(start, end); 456 | start = newPos.start; 457 | end = newPos.end; 458 | 459 | var filtered_results = []; 460 | //remove results from context completion 461 | //that are already in kernel completion 462 | var i; 463 | 464 | // append the introspection result, in order, at at the beginning of 465 | // the table and compute the replacement range from current cursor 466 | // position and matched_text length. 467 | var from = this.editor.posFromIndex(start); 468 | var to = this.editor.posFromIndex(end); 469 | for (i = matches.length - 1; i >= 0; --i) { 470 | var info = extMetadata[i] || {}; 471 | var replaceText = info.text || matches[i]; 472 | var displayText = info.displayText || replaceText; 473 | 474 | filtered_results.unshift({ 475 | str: displayText, 476 | replaceText: replaceText, 477 | tail: info.tail, 478 | icon: info.icon, 479 | type: "introspection", 480 | deprecation: info.deprecation, 481 | from: from, 482 | to: to 483 | }); 484 | } 485 | 486 | // one the 2 sources results have been merge, deal with it 487 | this.raw_result = filtered_results; 488 | 489 | this.make_gui(newPos, cur); 490 | }; 491 | 492 | Completer.prototype.pickColor = function(iconText) { 493 | this.colorsDict = this.colorsDict || {}; 494 | var colorInd = this.colorsDict[iconText]; 495 | if (colorInd) { 496 | return colorInd; 497 | } 498 | colorInd = Math.floor(Math.random() * 10) + 1; 499 | this.colorsDict[iconText] = colorInd; 500 | return colorInd; 501 | }; 502 | 503 | Completer.prototype.insert = function (completion) { 504 | this.editor.replaceRange(completion.replaceText, completion.from, completion.to); 505 | }; 506 | 507 | Completer.prototype.build_gui_list = function (completions) { 508 | var MAXIMUM_GUI_LIST_LENGTH = 1000; 509 | var that = this; 510 | for (var i = 0; i < completions.length && i < MAXIMUM_GUI_LIST_LENGTH; ++i) { 511 | var comp = completions[i]; 512 | 513 | var icon = comp.icon || 'u'; 514 | var text = comp.replaceText || ''; 515 | var displayText = comp.str || ''; 516 | var tail = comp.tail || ''; 517 | 518 | var typeColorIndex = this.pickColor(icon); 519 | var iconTag = $('') 520 | .text(icon.charAt(0)) 521 | .addClass('jp-Completer-type') 522 | .attr('data-color-index', typeColorIndex); 523 | 524 | var matchTag = $('').text(displayText).addClass('jp-Completer-match'); 525 | if (comp.deprecation != null) { 526 | matchTag.addClass('jp-Completer-deprecated'); 527 | } 528 | 529 | var typeTag = $('').text(tail).addClass('jp-Completer-typeExtended'); 530 | 531 | var opt = $('
  • ').addClass('jp-Completer-item'); 532 | opt.click((function (k, event) { 533 | this.selIndex = k; 534 | this.pick(); 535 | this.editor.focus(); 536 | }).bind(that, i)); 537 | 538 | opt.append(iconTag, matchTag, typeTag); 539 | 540 | this.sel.append(opt); 541 | } 542 | this.sel.children().first().addClass('jp-mod-active'); 543 | this.selIndex = 0; 544 | // this.sel.scrollTop(0); 545 | }; 546 | 547 | Completer.prototype.close = function () { 548 | this.done = true; 549 | opened_completer = null; 550 | $('#complete').remove(); 551 | this.editor.off('keydown', this._handle_keydown); 552 | this.editor.off('keypress', this._handle_keypress); 553 | this.visible = false; 554 | }; 555 | 556 | Completer.prototype.pick = function () { 557 | var ind = this.selIndex; 558 | var completion = this.raw_result[ind]; 559 | this.insert(completion); 560 | this.close(); 561 | }; 562 | 563 | Completer.prototype.selectNew = function(newIndex) { 564 | $(this.sel.children()[this.selIndex]).removeClass('jp-mod-active'); 565 | this.selIndex = newIndex; 566 | 567 | var active = this.sel.children()[this.selIndex]; 568 | $(active).addClass('jp-mod-active'); 569 | active.scrollIntoViewIfNeeded(false); 570 | }; 571 | 572 | Completer.prototype.keydown = function (event) { 573 | var code = event.keyCode; 574 | 575 | // Enter 576 | var optionsLen; 577 | var index; 578 | var prevIndex; 579 | if (code == keycodes.enter && !(event.shiftKey || event.ctrlKey || event.metaKey || event.altKey)) { 580 | event.codemirrorIgnore = true; 581 | event._ipkmIgnore = true; 582 | event.preventDefault(); 583 | this.pick(); 584 | // Escape or backspace 585 | } else if (code == keycodes.esc) { 586 | event.codemirrorIgnore = true; 587 | event._ipkmIgnore = true; 588 | event.preventDefault(); 589 | this.close(); 590 | } else if (code == keycodes.tab) { 591 | //all the fastforwarding operation, 592 | //Check that shared start is not null which can append with prefixed completion 593 | // like %pylab , pylab have no shred start, and ff will result in py 594 | // to erase py 595 | 596 | var sh = shared_start(this.raw_result, true); 597 | if (sh.str !== '') { 598 | this.insert(sh); 599 | } 600 | this.close(); 601 | this.carry_on_completion(); 602 | 603 | // event.codemirrorIgnore = true; 604 | // event._ipkmIgnore = true; 605 | // event.preventDefault(); 606 | // this.pick(); 607 | // this.close(); 608 | 609 | } else if (code == keycodes.up || code == keycodes.down) { 610 | // need to do that to be able to move the arrow 611 | // when on the first or last line ofo a code cell 612 | event.codemirrorIgnore = true; 613 | event._ipkmIgnore = true; 614 | event.preventDefault(); 615 | 616 | optionsLen = this.raw_result.length; 617 | index = this.selIndex; 618 | if (code == keycodes.up) { 619 | index--; 620 | } 621 | if (code == keycodes.down) { 622 | index++; 623 | } 624 | index = Math.min(Math.max(index, 0), optionsLen-1); 625 | this.selectNew(index); 626 | } else if (code == keycodes.pageup || code == keycodes.pagedown) { 627 | event.codemirrorIgnore = true; 628 | event._ipkmIgnore = true; 629 | 630 | optionsLen = this.raw_result.length; 631 | index = this.selIndex; 632 | if (code == keycodes.pageup) { 633 | index -= 10; // As 10 is the hard coded size of the drop down menu 634 | } else { 635 | index += 10; 636 | } 637 | index = Math.min(Math.max(index, 0), optionsLen-1); 638 | this.selectNew(index); 639 | } else if (code == keycodes.left || code == keycodes.right) { 640 | this.close(); 641 | } 642 | }; 643 | 644 | function _isCompletionKey(key) { 645 | return /^[A-Z0-9.:"]$/i.test(key); 646 | } 647 | 648 | function _processCompletionOnChange(cm, changes, completer) { 649 | var close_completion = false 650 | for (var i = 0; i < changes.length; ++i) { 651 | var change = changes[i]; 652 | if (change.origin === "+input") { 653 | for (var j = 0; j < change.text.length; ++j) { 654 | var t = change.text[j]; 655 | for (var k = 0; k < t.length; ++k) { 656 | if (_isCompletionKey(t[k])) { 657 | completer.startCompletion(false); 658 | return; 659 | } 660 | } 661 | } 662 | } else { 663 | var line = change.from.line; 664 | var ch = change.from.ch; 665 | if (ch === 0) continue; 666 | var removed = change.removed; 667 | if (removed.length > 1 || removed[0].length > 0) { 668 | var prevChar = cm.getRange({line: line, ch: ch - 1}, change.from); 669 | if (_isCompletionKey(prevChar)) { 670 | completer.startCompletion(false); 671 | return; 672 | } 673 | else close_completion = true; 674 | } 675 | } 676 | } 677 | if (close_completion) completer.close(); 678 | } 679 | 680 | Completer.prototype.keypress = function (event) { 681 | /** 682 | * FIXME: This is a band-aid. 683 | * on keypress, trigger insertion of a single character. 684 | * This simulates the old behavior of completion as you type, 685 | * before events were disconnected and CodeMirror stopped 686 | * receiving events while the completer is focused. 687 | */ 688 | 689 | var that = this; 690 | var code = event.keyCode; 691 | 692 | // don't handle keypress if it's not a character (arrows on FF) 693 | // or ENTER/TAB/BACKSPACE 694 | if (event.charCode === 0 || 695 | code == keycodes.tab || 696 | code == keycodes.enter || 697 | code == keycodes.backspace 698 | ) return; 699 | 700 | if (_isCompletionKey(event.key)) 701 | return; 702 | 703 | // this.close(); 704 | this.editor.focus(); 705 | 706 | setTimeout(function () { 707 | that.carry_on_completion(); 708 | }, 10); 709 | }; 710 | 711 | var EMPTY_ERRORS_RESULT = [[], []]; 712 | 713 | CodeCell.prototype.findErrorsAtPos = function(pos) { 714 | if (pos.outside || Math.abs(pos.xRel) > 10) 715 | return EMPTY_ERRORS_RESULT; 716 | 717 | var ind = this.code_mirror.indexFromPos(pos); 718 | if (!this.errorsList) 719 | return EMPTY_ERRORS_RESULT; 720 | 721 | var filter = (er) => { 722 | if (!er.range) return false 723 | var er_start_ind = this.code_mirror.indexFromPos(er.range.start); 724 | var er_end_ind = this.code_mirror.indexFromPos(er.range.end); 725 | return er_start_ind <= ind && ind <= er_end_ind; 726 | }; 727 | var passed = []; 728 | var other = []; 729 | this.errorsList.forEach((er) => { 730 | if (filter(er)) { 731 | passed.push(er); 732 | } else { 733 | other.push(er); 734 | } 735 | }); 736 | return [passed, other]; 737 | }; 738 | 739 | function clearErrors(cell) { 740 | cell.code_mirror.getAllMarks() 741 | .filter(it => additionalClasses.some((cl) => it.className === cl)) 742 | .forEach(it => it.clear()); 743 | cell.errorsList = [] 744 | } 745 | 746 | function clearAllErrors(notebook) { 747 | notebook.get_cells().forEach((cell) =>{ 748 | if (cell.code_mirror) { 749 | clearErrors(cell); 750 | } 751 | }); 752 | } 753 | 754 | CodeCell.prototype._handle_change = function(cm, changes) { 755 | _processCompletionOnChange(cm, changes, this.completer) 756 | 757 | clearAllErrors(this.notebook); 758 | this.kernel.listErrors(cm.getValue(), (msg) => { 759 | var content = msg.content; 760 | console.log(content); 761 | 762 | if(content.code !== cm.getValue()) { 763 | return; 764 | } 765 | 766 | var errors = content.errors; 767 | this.highlightErrors(errors); 768 | this.errorsList = errors; 769 | 770 | cm.scrollIntoView(null) 771 | }); 772 | }; 773 | 774 | CodeCell.prototype._handle_move = function(e) { 775 | var rect = e.currentTarget.getBoundingClientRect(); 776 | var x = e.clientX - rect.left; 777 | var y = e.clientY - rect.top; 778 | var cursor_pos = this.code_mirror.coordsChar({left: x, top: y}, "local"); 779 | var res = this.findErrorsAtPos(cursor_pos); 780 | var errors = res[0]; 781 | var otherErrors = res[1]; 782 | errors.forEach((error) => { 783 | tempTooltip(this.code_mirror, error, cursor_pos); 784 | }); 785 | otherErrors.forEach((error) => { 786 | if (error.tip) { 787 | remove(error.tip); 788 | } 789 | }) 790 | }; 791 | 792 | 793 | CodeCell.prototype._isCompletionEvent = function(event, cur, editor) { 794 | if (event.type !== 'keydown' || event.ctrlKey || event.metaKey || event.altKey || !this.tooltip._hidden) 795 | return false; 796 | if (event.keyCode === keycodes.tab) 797 | return true; 798 | if (event.keyCode === keycodes.backspace){ 799 | var pre_cursor = editor.getRange({line:0,ch:0},cur); 800 | return pre_cursor.length > 1 && _isCompletionKey(pre_cursor[pre_cursor.length - 2]); 801 | } 802 | return _isCompletionKey(event.key); 803 | }; 804 | 805 | CodeCell.prototype.addEvent = function(obj, event, listener, bind_listener_name) { 806 | if (this[bind_listener_name]) { 807 | return; 808 | } 809 | var handler = this[bind_listener_name] = listener.bind(this); 810 | CodeMirror.off(obj, event, handler); 811 | CodeMirror.on(obj, event, handler); 812 | }; 813 | 814 | CodeCell.prototype.bindEvents = function() { 815 | this.addEvent(this.code_mirror, 'changes', this._handle_change, "binded_handle_change"); 816 | this.addEvent(this.code_mirror.display.lineSpace, 'mousemove', this._handle_move, 'binded_handle_move'); 817 | }; 818 | 819 | CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) { 820 | var that = this; 821 | this.bindEvents(); 822 | 823 | // whatever key is pressed, first, cancel the tooltip request before 824 | // they are sent, and remove tooltip if any, except for tab again 825 | var tooltip_closed = null; 826 | if (event.type === 'keydown' && event.which !== keycodes.tab ) { 827 | tooltip_closed = this.tooltip.remove_and_cancel_tooltip(); 828 | } 829 | 830 | var cur = editor.getCursor(); 831 | if (event.keyCode === keycodes.enter){ 832 | this.auto_highlight(); 833 | } 834 | 835 | if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) { 836 | // triger on keypress (!) otherwise inconsistent event.which depending on plateform 837 | // browser and keyboard layout ! 838 | // Pressing '(' , request tooltip, don't forget to reappend it 839 | // The second argument says to hide the tooltip if the docstring 840 | // is actually empty 841 | this.tooltip.pending(that, true); 842 | } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') { 843 | // If tooltip is active, cancel it. The call to 844 | // remove_and_cancel_tooltip above doesn't pass, force=true. 845 | // Because of this it won't actually close the tooltip 846 | // if it is in sticky mode. Thus, we have to check again if it is open 847 | // and close it with force=true. 848 | if (!this.tooltip._hidden) { 849 | this.tooltip.remove_and_cancel_tooltip(true); 850 | } 851 | // If we closed the tooltip, don't let CM or the global handlers 852 | // handle this event. 853 | event.codemirrorIgnore = true; 854 | event._ipkmIgnore = true; 855 | event.preventDefault(); 856 | return true; 857 | } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) { 858 | if (editor.somethingSelected() || editor.getSelections().length !== 1){ 859 | var anchor = editor.getCursor("anchor"); 860 | var head = editor.getCursor("head"); 861 | if( anchor.line !== head.line){ 862 | return false; 863 | } 864 | } 865 | var pre_cursor = editor.getRange({line:cur.line,ch:0},cur); 866 | if (pre_cursor.trim() === "") { 867 | // Don't show tooltip if the part of the line before the cursor 868 | // is empty. In this case, let CodeMirror handle indentation. 869 | return false; 870 | } 871 | this.tooltip.request(that); 872 | event.codemirrorIgnore = true; 873 | event.preventDefault(); 874 | return true; 875 | } else if (this._isCompletionEvent(event, cur, editor)) { 876 | // Tab completion. 877 | this.tooltip.remove_and_cancel_tooltip(); 878 | 879 | // completion does not work on multicursor, it might be possible though in some cases 880 | if (editor.somethingSelected() || editor.getSelections().length > 1) { 881 | return false; 882 | } 883 | var pre_cursor = editor.getRange({line:cur.line,ch:0},cur); 884 | if (pre_cursor.trim() === "" && event.keyCode === keycodes.tab) { 885 | // Don't autocomplete if the part of the line before the cursor 886 | // is empty. In this case, let CodeMirror handle indentation. 887 | return false; 888 | } else { 889 | if (event.keyCode === keycodes.tab) { 890 | event.preventDefault(); 891 | event.codemirrorIgnore = true; 892 | this.completer.startCompletion(true); 893 | } 894 | return true; 895 | } 896 | } 897 | 898 | // keyboard event wasn't one of those unique to code cells, let's see 899 | // if it's one of the generic ones (i.e. check edit mode shortcuts) 900 | return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]); 901 | }; 902 | 903 | CodeCell.prototype._handle_execute_reply = function (msg) { 904 | clearAllErrors(this.notebook) 905 | this.bindEvents(); 906 | 907 | this.set_input_prompt(msg.content.execution_count); 908 | this.element.removeClass("running"); 909 | this.events.trigger('set_dirty.Notebook', {value: true}); 910 | 911 | if (msg.content.status === 'error') { 912 | var addInfo = msg.content.additionalInfo || {}; 913 | var from = {line: addInfo.lineStart, col: addInfo.colStart}; 914 | var to; 915 | if (addInfo.lineEnd !== -1 && addInfo.colEnd !== -1) { 916 | to = {line: addInfo.lineEnd, col: addInfo.colEnd}; 917 | } else { 918 | to = {line: from.line, col: from.col + 3}; 919 | } 920 | 921 | if (from.line !== undefined && from.col !== undefined) { 922 | var message = addInfo.message; 923 | message = message.replace(/^\(\d+:\d+ - \d+\) /, ""); 924 | message = message.replace(/^\(\d+:\d+\) - \(\d+:\d+\) /, ""); 925 | 926 | this.errorsList = [ 927 | { 928 | start: from, 929 | end: to, 930 | message: message, 931 | severity: "ERROR" 932 | }]; 933 | this.highlightErrors(this.errorsList); 934 | } 935 | } 936 | }; 937 | 938 | function tempTooltip(cm, error, pos) { 939 | if (cm.state.errorTip) remove(cm.state.errorTip); 940 | var where = cm.charCoords(pos); 941 | var tip = makeTooltip(where.right + 1, where.bottom, error.message, error.tip); 942 | error.tip = cm.state.errorTip = tip; 943 | fadeIn(tip); 944 | 945 | function maybeClear() { 946 | old = true; 947 | if (!mouseOnTip) clear(); 948 | } 949 | function clear() { 950 | cm.state.ternTooltip = null; 951 | if (tip.parentNode) fadeOut(tip); 952 | clearActivity(); 953 | } 954 | var mouseOnTip = false, old = false; 955 | CodeMirror.on(tip, "mousemove", function() { mouseOnTip = true; }); 956 | CodeMirror.on(tip, "mouseout", function(e) { 957 | var related = e.relatedTarget || e.toElement; 958 | if (!related || !CodeMirror.contains(tip, related)) { 959 | if (old) clear(); 960 | else mouseOnTip = false; 961 | } 962 | }); 963 | setTimeout(maybeClear, 100000); 964 | var clearActivity = onEditorActivity(cm, clear) 965 | } 966 | 967 | function fadeIn(tooltip) { 968 | document.body.appendChild(tooltip); 969 | tooltip.style.opacity = "1"; 970 | } 971 | 972 | function fadeOut(tooltip) { 973 | tooltip.style.opacity = "0"; 974 | setTimeout(function() { remove(tooltip); }, 100); 975 | } 976 | 977 | function makeTooltip(x, y, content, element) { 978 | var node = element || elt("div", "kotlin-error-tooltip", content); 979 | node.style.left = x + "px"; 980 | node.style.top = y + "px"; 981 | return node; 982 | } 983 | 984 | function elt(tagname, cls /*, ... elts*/) { 985 | var e = document.createElement(tagname); 986 | if (cls) e.className = cls; 987 | for (var i = 2; i < arguments.length; ++i) { 988 | var elt = arguments[i]; 989 | if (typeof elt == "string") elt = document.createTextNode(elt); 990 | e.appendChild(elt); 991 | } 992 | return e; 993 | } 994 | 995 | function remove(node) { 996 | var p = node && node.parentNode; 997 | if (p) p.removeChild(node); 998 | } 999 | 1000 | function onEditorActivity(cm, f) { 1001 | cm.on("cursorActivity", f); 1002 | cm.on("blur", f); 1003 | cm.on("scroll", f); 1004 | cm.on("setDoc", f); 1005 | return function() { 1006 | cm.off("cursorActivity", f); 1007 | cm.off("blur", f); 1008 | cm.off("scroll", f); 1009 | cm.off("setDoc", f); 1010 | } 1011 | } 1012 | 1013 | Kernel.prototype.listErrors = function (code, callback) { 1014 | var callbacks; 1015 | if (callback) { 1016 | callbacks = { shell : { reply : callback } }; 1017 | } 1018 | var content = { 1019 | code : code 1020 | }; 1021 | return this.send_shell_message("list_errors_request", content, callbacks); 1022 | }; 1023 | 1024 | } 1025 | 1026 | return {onload: onload}; 1027 | 1028 | }); -------------------------------------------------------------------------------- /ghidra_jupyter/kernel/kernel.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_name": "Ghidra(Kotlin)", 3 | "language": "kotlin", 4 | "interrupt_mode": "message", 5 | "argv": [ 6 | "python", 7 | "-m", 8 | "ghidra_jupyter.dispatcher", 9 | "{connection_file}" 10 | ] 11 | } -------------------------------------------------------------------------------- /ghidra_jupyter/kernel/logo-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/logo-16x16.png -------------------------------------------------------------------------------- /ghidra_jupyter/kernel/logo-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/logo-64x64.png -------------------------------------------------------------------------------- /ghidra_jupyter/kernel/wavyline-orange.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/wavyline-orange.gif -------------------------------------------------------------------------------- /ghidra_jupyter/kernel/wavyline-red.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/wavyline-red.gif -------------------------------------------------------------------------------- /ghidra_jupyter/requirements.txt: -------------------------------------------------------------------------------- 1 | psutil 2 | attrs 3 | notebook 4 | tqdm 5 | requests -------------------------------------------------------------------------------- /ghidra_jupyter/setup.py: -------------------------------------------------------------------------------- 1 | import glob 2 | 3 | from setuptools import find_packages, setup 4 | 5 | GITHUB_URL = "https://github.com/GhidraJupyter/ghidra-jupyter-kotlin" 6 | 7 | LONG_DESCRIPTION = f""" 8 | # Ghidra-Jupyter 9 | 10 | A Jupyter kernel (notebook & QtConsole) plugin for Ghidra. 11 | 12 | Currently supporting [Kotlin-Jupyter](https://github.com/Kotlin/kotlin-jupyter). 13 | 14 | For info and installation see the [github repo]({GITHUB_URL}). 15 | """ 16 | 17 | 18 | def is_requirement(line): 19 | return not line.startswith("-e") and not line.startswith(".") 20 | 21 | 22 | def get_requirements(extra=None): 23 | if extra: 24 | filename = f"requirements.{extra}.txt" 25 | else: 26 | filename = "requirements.txt" 27 | 28 | with open(filename) as f: 29 | return [line for line in f.read().splitlines() if is_requirement(line)] 30 | 31 | 32 | DATA_FILES = [ 33 | ("share/jupyter/kernels/ghidra-kotlin", glob.glob("kernel/*")), 34 | ] 35 | 36 | 37 | PACKAGES = find_packages(where="src") 38 | INSTALL_REQUIRES = get_requirements() 39 | setup( 40 | name="ghidra_jupyter", 41 | version="1.1.0", 42 | packages=PACKAGES, 43 | package_dir={"": "src"}, 44 | include_package_data=True, 45 | url=GITHUB_URL, 46 | license="MIT License", 47 | author="GhidraJupyter", 48 | author_email="", 49 | description="A Jupyter kernel for Ghidra", 50 | long_description_content_type="text/markdown", 51 | long_description=LONG_DESCRIPTION, 52 | classifiers=[ 53 | "Programming Language :: Python :: 3", 54 | "License :: OSI Approved :: MIT License", 55 | "Operating System :: OS Independent", 56 | ], 57 | install_requires=INSTALL_REQUIRES, 58 | entry_points={ 59 | "console_scripts": [ 60 | "ghidra-jupyter=ghidra_jupyter.installer:main", 61 | ], 62 | }, 63 | data_files=DATA_FILES, 64 | python_requires=">=3.6", 65 | ) 66 | -------------------------------------------------------------------------------- /ghidra_jupyter/src/ghidra_jupyter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/src/ghidra_jupyter/__init__.py -------------------------------------------------------------------------------- /ghidra_jupyter/src/ghidra_jupyter/dispatcher.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import psutil 4 | import attr 5 | 6 | PROXY_ENV_VAR = "GHIDRA_JUPYTER_PROXY" 7 | 8 | 9 | @attr.s(auto_attribs=True, frozen=True, slots=True) 10 | class ProxyPaths: 11 | pid: str 12 | path: str 13 | 14 | 15 | def get_proxy_paths(): 16 | base = os.environ.get(PROXY_ENV_VAR) 17 | if not base: 18 | base = os.path.join(os.path.expanduser("~"), ".ghidra", "notebook_proxy") 19 | 20 | return ProxyPaths( 21 | pid=base + ".pid", 22 | path=base + ".path", 23 | ) 24 | 25 | 26 | def main(): 27 | print("starting!") 28 | proxy_paths = get_proxy_paths() 29 | 30 | with open(proxy_paths.path, "w") as f: 31 | f.write(sys.argv[1]) 32 | 33 | with open(proxy_paths.pid, "r") as f: 34 | pid = int(f.read().strip()) 35 | 36 | psutil.Process(pid=pid).wait() 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /ghidra_jupyter/src/ghidra_jupyter/installer.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import re 4 | import shutil 5 | import zipfile 6 | from tempfile import TemporaryDirectory 7 | from typing import Optional, Tuple 8 | from packaging import version 9 | from pathlib import Path 10 | 11 | import requests 12 | from tqdm import tqdm 13 | 14 | REPO = "GhidraJupyter/ghidra-jupyter-kotlin" 15 | # NAME_PATTERN = r"GhidraJupyterKotlin[v0-9.\-_]*\.zip" 16 | NAME_PATTERN = r"ghidra_(.+)_PUBLIC_(\d{8})_GhidraJupyterKotlin.zip" 17 | 18 | def download_file(url: str, path: str): 19 | with requests.get(url, stream=True) as response: 20 | response.raise_for_status() 21 | 22 | total_size_in_bytes = int(response.headers.get("content-length", 0)) or None 23 | block_size = 1024 # 1 Kibibyte 24 | 25 | with open(path, "wb") as f, tqdm( 26 | total=total_size_in_bytes, unit="iB", unit_scale=True 27 | ) as progress_bar: 28 | for chunk in response.iter_content(chunk_size=block_size): 29 | f.write(chunk) 30 | progress_bar.update(len(chunk)) 31 | 32 | 33 | def _install_from_path(ghidra_install_dir: str, extension_path: str): 34 | # First, remove the extension if it already exists 35 | install_path = os.path.join( 36 | ghidra_install_dir, "Ghidra", "Extensions", "GhidraJupyterKotlin" 37 | ) 38 | print(f"Installing extension to {install_path}") 39 | shutil.rmtree( 40 | install_path, 41 | ignore_errors=True, 42 | ) 43 | 44 | with zipfile.ZipFile(extension_path) as zip_ref: 45 | zip_ref.extractall(os.path.join(ghidra_install_dir, "Ghidra", "Extensions")) 46 | 47 | 48 | def _get_ghidra_dir(ghidra_install_dir: Optional[str]) -> str: 49 | return ghidra_install_dir or os.environ.get("GHIDRA_INSTALL_DIR") 50 | 51 | 52 | def _get_ghidra_version(ghidra_install_dir: Optional[str]) -> version.Version: 53 | app_properties = os.path.join(ghidra_install_dir, "Ghidra", "application.properties") 54 | with open(app_properties, "r") as f: 55 | for line in f.readlines(): 56 | prop_split = line.split("=") 57 | if len(prop_split) != 2: 58 | continue 59 | key, value = prop_split 60 | if key == "application.version": 61 | return version.parse(value) 62 | 63 | 64 | def get_download_url(repo, name_pattern) -> Tuple[str, version.Version]: 65 | release = requests.get(f"https://api.github.com/repos/{repo}/releases/latest") 66 | release.raise_for_status() 67 | for asset in release.json()["assets"]: 68 | m = re.match(name_pattern, asset["name"]) 69 | if m: 70 | extension_version = version.parse(m.group(1)) 71 | return asset["browser_download_url"], extension_version 72 | 73 | 74 | 75 | def install_extension( 76 | ghidra_install_dir: Optional[str], 77 | extension_path: Optional[str], 78 | extension_url: Optional[str], 79 | ): 80 | ghidra_install_dir = _get_ghidra_dir(ghidra_install_dir) 81 | ghidra_version = _get_ghidra_version(ghidra_install_dir) 82 | 83 | if not ghidra_install_dir: 84 | print("Missing $GHIDRA_INSTALL_DIR") 85 | return 86 | 87 | if extension_path: 88 | _install_from_path(ghidra_install_dir, extension_path) 89 | 90 | else: 91 | with TemporaryDirectory() as tempdir: 92 | extension_path = os.path.join(tempdir, "Extension.zip") 93 | if extension_url is None: 94 | extension_url, extension_version = get_download_url(REPO, NAME_PATTERN) 95 | print("Detected Ghidra Version: ", ghidra_version) 96 | print("Extension Version: ", extension_version) 97 | if extension_version.major != ghidra_version.major: 98 | print("ERROR: Major version of Ghidra (%s) and Extension (%s) don't match, refusing to install" % 99 | (ghidra_version, extension_version)) 100 | return 101 | elif ghidra_version > extension_version: 102 | print("WARNING: Your Ghidra version is newer than the extension version") 103 | print("There could be some unresolved compatibility issue or we forgot to bump the CI version") 104 | print("Please check https://github.com/%s" % REPO) 105 | elif extension_version > ghidra_version: 106 | print("!WARNING! " * 10) 107 | print("WARNING: Ghidra Version is %s, but extension_version is %s" 108 | % (ghidra_version, extension_version)) 109 | print("WARNING: Extension will still be installed, but might encounter unpredictable issues. " 110 | "Please update your Ghidra install or manually install an older release") 111 | print("!WARNING! " * 10) 112 | 113 | print(f"Downloading Ghidra extension from {extension_url}") 114 | download_file(extension_url, extension_path) 115 | print("Download complete.") 116 | _install_from_path(ghidra_install_dir, extension_path) 117 | 118 | print("Installation Complete.") 119 | 120 | 121 | def remove_extension(ghidra_install_dir: Optional[str]): 122 | ghidra_install_dir = _get_ghidra_dir(ghidra_install_dir) 123 | if not ghidra_install_dir: 124 | print("Missing $GHIDRA_INSTALL_DIR") 125 | return 126 | 127 | install_path = os.path.join( 128 | ghidra_install_dir, "Ghidra", "Extensions", "GhidraJupyterKotlin" 129 | ) 130 | print(f"Removing extension at {install_path}") 131 | shutil.rmtree( 132 | install_path, 133 | ignore_errors=True, 134 | ) 135 | 136 | print("Extension removed.") 137 | 138 | 139 | def create_parser(): 140 | parser = argparse.ArgumentParser() 141 | subparsers = parser.add_subparsers(dest="command") 142 | 143 | install_extension = subparsers.add_parser("install-extension") 144 | install_extension.add_argument( 145 | "--ghidra", 146 | nargs="?", 147 | help="Ghidra install directory. Defaults to $GHIDRA_INSTALL_DIR", 148 | ) 149 | install_extension.add_argument( 150 | "--extension-path", 151 | nargs="?", 152 | help="Path to a local .zip of the extension", 153 | ) 154 | install_extension.add_argument( 155 | "--extension-url", 156 | nargs="?", 157 | help="URL to download the extension from", 158 | ) 159 | 160 | remove_extension = subparsers.add_parser("remove-extension") 161 | remove_extension.add_argument( 162 | "--ghidra", 163 | nargs="?", 164 | help="Ghidra install directory. Defaults to $GHIDRA_INSTALL_DIR", 165 | ) 166 | 167 | return parser 168 | 169 | 170 | def main(): 171 | parser = create_parser() 172 | args = parser.parse_args() 173 | 174 | if args.command == "install-extension": 175 | install_extension(args.ghidra, args.extension_path, args.extension_url) 176 | 177 | elif args.command == "remove-extension": 178 | remove_extension(args.ghidra) 179 | 180 | 181 | if __name__ == "__main__": 182 | main() 183 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GRADLE_MIN=1.0 2 | GRADLE_MAX=100.0 -------------------------------------------------------------------------------- /resources/notebook-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 22 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 62 | 66 | 70 | 74 | 78 | 82 | 86 | 90 | 94 | 95 | 97 | 101 | 105 | 106 | 118 | 120 | 124 | 128 | 129 | 132 | 138 | 139 | 149 | 159 | 169 | 177 | 181 | 185 | 189 | 193 | 197 | 198 | 206 | 210 | 214 | 218 | 222 | 223 | 231 | 235 | 239 | 243 | 247 | 251 | 255 | 256 | 257 | 277 | 279 | 280 | 282 | image/svg+xml 283 | 285 | 286 | 287 | 288 | 289 | 293 | 296 | 301 | 305 | 315 | 316 | 317 | 322 | 326 | 336 | 337 | 338 | 339 | 342 | 343 | 351 | 355 | 356 | 360 | 361 | 365 | 366 | 370 | 371 | 375 | 376 | 377 | 378 | 382 | 383 | 384 | 392 | 396 | 397 | 401 | 402 | 406 | 407 | 411 | 412 | 413 | 414 | 418 | 419 | 420 | 428 | 432 | 433 | 437 | 438 | 442 | 443 | 447 | 448 | 452 | 453 | 457 | 458 | 459 | 460 | 464 | 465 | 466 | 467 | 468 | -------------------------------------------------------------------------------- /resources/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/options.png -------------------------------------------------------------------------------- /resources/qtconsole-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 26 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 50 | 53 | 56 | 59 | 61 | 65 | 69 | 73 | 77 | 81 | 85 | 89 | 93 | 94 | 97 | 101 | 105 | 106 | 108 | 112 | 116 | 117 | 129 | 132 | 136 | 137 | 139 | 143 | 147 | 148 | 158 | 168 | 178 | 188 | 191 | 197 | 198 | 208 | 218 | 228 | 236 | 240 | 244 | 248 | 252 | 256 | 257 | 265 | 269 | 273 | 277 | 281 | 282 | 290 | 294 | 298 | 302 | 306 | 310 | 314 | 315 | 316 | 336 | 338 | 339 | 341 | image/svg+xml 342 | 344 | 345 | 346 | 347 | 348 | 352 | 356 | 366 | 373 | 380 | 385 | 392 | 397 | 402 | 407 | 408 | 411 | 412 | 420 | 424 | 425 | 429 | 430 | 434 | 435 | 439 | 440 | 444 | 445 | 446 | 447 | 451 | 452 | 453 | 461 | 465 | 466 | 470 | 471 | 475 | 476 | 480 | 481 | 482 | 483 | 487 | 488 | 489 | 497 | 501 | 502 | 506 | 507 | 511 | 512 | 516 | 517 | 521 | 522 | 526 | 527 | 528 | 529 | 533 | 534 | 535 | 536 | 537 | -------------------------------------------------------------------------------- /resources/readme/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/buttons.png -------------------------------------------------------------------------------- /resources/readme/create_notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/create_notebook.png -------------------------------------------------------------------------------- /resources/readme/interrupt_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/interrupt_demo.png -------------------------------------------------------------------------------- /resources/readme/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/menu.png -------------------------------------------------------------------------------- /resources/readme/notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/notebook.png -------------------------------------------------------------------------------- /resources/readme/notebook_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/notebook_view.png -------------------------------------------------------------------------------- /resources/readme/qtconsole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/qtconsole.png -------------------------------------------------------------------------------- /resources/readme/qtconsole_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/qtconsole_window.png -------------------------------------------------------------------------------- /resources/readme/waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/waiting.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ":GhidraJupyterKotlin" 2 | --------------------------------------------------------------------------------