Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,19 @@ jobs:
run: $GHIDRA_INSTALL_DIR/support/sleigh $GHIDRA_INSTALL_DIR/Ghidra/Processors/x86/data/languages/x86-64.slaspec

- name: Run tests
run: xvfb-run ./gradlew test --info
run: xvfb-run ./gradlew test

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-ghidra-${{ matrix.ghidra }}
path: build/reports/tests/test/

- name: Publish test report
if: always()
uses: dorny/test-reporter@v1
with:
name: Tests (Ghidra ${{ matrix.ghidra }})
path: build/test-results/test/*.xml
reporter: java-junit
3 changes: 2 additions & 1 deletion ghidra_scripts/RevEngExamplePostScript.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder;
import ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService;
import ai.reveng.toolkit.ghidra.core.services.api.types.ApiInfo;
import ai.reveng.toolkit.ghidra.plugins.ReaiPluginPackage;
import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
import ghidra.app.script.GhidraScript;

public class RevEngExamplePostScript extends GhidraScript {
@Override
protected void run() throws Exception {
// Services are not available in headless mode, so we have to instantiate it ourselves
var ghidraRevengService = new GhidraRevengService(ApiInfo.fromConfig());
var ghidraRevengService = new GhidraRevengService(ApiInfo.fromConfig(ReaiPluginPackage.DEFAULT_CONFIG_PATH));

ghidraRevengService.upload(currentProgram);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ public BinaryTableModel(PluginTool tool) {

@Override
protected void doLoad(Accumulator<BinaryRowObject> accumulator, TaskMonitor monitor) throws CancelledException {
serviceProvider.getService(GhidraRevengService.class)
.getActiveAnalysisIDsFilter()
var service = serviceProvider.getService(GhidraRevengService.class);
if (service == null) return;
service.getActiveAnalysisIDsFilter()
.forEach(
analysisResult -> accumulator.add(new BinaryRowObject(analysisResult, true))
);

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ public CollectionTableModel(PluginTool plugin) {

@Override
protected void doLoad(Accumulator<CollectionRowObject> accumulator, TaskMonitor monitor){
var service = serviceProvider.getService(GhidraRevengService.class);
if (service == null) return;
monitor.setProgress(0);
monitor.setMessage("Loading collections");
serviceProvider.getService(GhidraRevengService.class).getActiveCollections().forEach(collection -> {
service.getActiveCollections().forEach(collection -> {
accumulator.add(new CollectionRowObject(collection, true));
});

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ public TypedApiImplementation(String baseUrl, String apiKey) {
try {
// This file comes from the release.yml running in the CI
var inputStream = ResourceManager.getResourceAsStream("reai_ghidra_plugin_version.txt");
pluginVersion = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).trim();
inputStream.close();
if (inputStream != null) {
pluginVersion = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).trim();
inputStream.close();
}
} catch (IOException e) {

// ignore — fall back to "unknown"
}
// Looks like:
// Ghidra/11.3.2-PUBLIC (LINUX(Linux) X86_64(amd64)) RevEng.AI_Plugin/v0.15
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package ai.reveng.toolkit.ghidra.core.services.api.types;

import ai.reveng.toolkit.ghidra.core.models.ReaiConfig;
import ai.reveng.toolkit.ghidra.core.services.api.TypedApiImplementation;
import ai.reveng.toolkit.ghidra.core.services.api.types.exceptions.InvalidAPIInfoException;
import com.google.gson.Gson;
import org.json.JSONException;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;

public record ApiInfo(
URI hostURI,
Expand All @@ -21,21 +18,15 @@ public ApiInfo(String hostURI, String portalURI, String apiKey) {
this(URI.create(hostURI), URI.create(portalURI), apiKey);
}

public void checkCredentials() throws InvalidAPIInfoException {
if (hostURI == null || apiKey == null){
throw new InvalidAPIInfoException("hostURI and apiKey must not be null");
}
var api = new TypedApiImplementation(this);

// Throws InvalidAPIInfoException if authentication fails
api.authenticate();
}

public static ApiInfo fromConfig(Path configFilePath) throws FileNotFoundException {
// Read and parse the config file as JSON
FileReader reader = new FileReader(configFilePath.toString());
Gson gson = new Gson();
ReaiConfig config = gson.fromJson(reader, ReaiConfig.class);
ReaiConfig config;
try (FileReader reader = new FileReader(configFilePath.toString())) {
config = new Gson().fromJson(reader, ReaiConfig.class);
} catch (FileNotFoundException e) {
throw e;
} catch (java.io.IOException e) {
throw new RuntimeException("Failed to read config file: " + configFilePath, e);
}
var apikey = config.getPluginSettings().getApiKey();
var hostname = config.getPluginSettings().getHostname();
var portalHostname = config.getPluginSettings().getPortalHostname();
Expand All @@ -46,15 +37,4 @@ public static ApiInfo fromConfig(Path configFilePath) throws FileNotFoundExcepti
}
return new ApiInfo(hostname, portalHostname, apikey);
}

public static ApiInfo fromConfig() throws FileNotFoundException {
String uHome = System.getProperty("user.home");
String cDir = ".reai";
String cFileName = "reai.json";
Path configDirPath = Paths.get(uHome, cDir);
Path configFilePath = configDirPath.resolve(cFileName);

return fromConfig(configFilePath);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ai.reveng.toolkit.ghidra.core.ui.wizard;

import ai.reveng.toolkit.ghidra.core.services.api.TypedApiImplementation;
import ai.reveng.toolkit.ghidra.core.services.api.types.ApiInfo;
import ai.reveng.toolkit.ghidra.core.services.api.types.exceptions.InvalidAPIInfoException;

/**
* Strategy for validating API credentials.
* Production code uses {@link #defaultValidator()} which makes a real API call;
* tests can supply a mock that avoids network calls.
*/
@FunctionalInterface
public interface CredentialValidator {
void validate(ApiInfo apiInfo) throws InvalidAPIInfoException;

/** Default validator that makes a real API call. */
static CredentialValidator defaultValidator() {
return apiInfo -> {
if (apiInfo.hostURI() == null || apiInfo.apiKey() == null) {
throw new InvalidAPIInfoException("hostURI and apiKey must not be null");
}
var api = new TypedApiImplementation(apiInfo);
api.authenticate();
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -55,11 +54,11 @@ protected void doFinish() throws IllegalPanelStateException {
tool.getOptions(REAI_OPTIONS_CATEGORY).setString(ReaiPluginPackage.OPTION_KEY_MODEL, model);
tool.getOptions(REAI_OPTIONS_CATEGORY).setString(REAI_WIZARD_RUN_PREF, "true");

String uHome = System.getProperty("user.home");
String cDir = ".reai";
String cFileName = "reai.json";
Path configDirPath = Paths.get(uHome, cDir);
Path configFilePath = configDirPath.resolve(cFileName);
String configFileOverride = (String) getState().get(SetupWizardStateKey.CONFIGFILE);
Path configFilePath = configFileOverride != null
? Path.of(configFileOverride)
: ReaiPluginPackage.DEFAULT_CONFIG_PATH;
Path configDirPath = configFilePath.getParent();

// check that our .reai directory exists
if (!Files.exists(configDirPath)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public enum SetupWizardStateKey {
CREDENTIALS_VALIDATED,
MODEL,
CONFIGFILE,
CREDENTIAL_VALIDATOR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ai.reveng.toolkit.ghidra.core.services.api.types.ApiInfo;
import ai.reveng.toolkit.ghidra.core.services.api.types.exceptions.InvalidAPIInfoException;
import ai.reveng.toolkit.ghidra.core.services.logging.ReaiLoggingService;
import ai.reveng.toolkit.ghidra.core.ui.wizard.CredentialValidator;
import ai.reveng.toolkit.ghidra.core.ui.wizard.SetupWizardStateKey;
import docking.wizard.AbstractMageJPanel;
import docking.wizard.IllegalPanelStateException;
Expand All @@ -26,6 +27,7 @@ public class UserCredentialsPanel extends AbstractMageJPanel<SetupWizardStateKey
private JTextField tfApiHostname;
private JTextField tfPortalHostname;
private Boolean credentialsValidated = false;
private CredentialValidator credentialValidator = CredentialValidator.defaultValidator();

private ReaiLoggingService loggingService;

Expand Down Expand Up @@ -73,6 +75,7 @@ public void changedUpdate(DocumentEvent e) {
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0;
tfApiKey = new JTextField(30);
tfApiKey.setName("apiKey");
tfApiKey.getDocument().addDocumentListener(documentListener);
tfApiKey.setToolTipText("API key from your account settings");
userDetailsPanel.add(tfApiKey, gbc);
Expand All @@ -89,6 +92,7 @@ public void changedUpdate(DocumentEvent e) {
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0;
tfApiHostname = new JTextField(30);
tfApiHostname.setName("apiHostname");
tfApiHostname.getDocument().addDocumentListener(documentListener);
tfApiHostname.setToolTipText("URL hosting the RevEng.AI Server");
tfApiHostname.setText("https://api.reveng.ai");
Expand All @@ -106,6 +110,7 @@ public void changedUpdate(DocumentEvent e) {
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0;
tfPortalHostname = new JTextField(30);
tfPortalHostname.setName("portalHostname");
tfPortalHostname.getDocument().addDocumentListener(documentListener);
tfPortalHostname.setToolTipText("URL hosting the RevEng.AI Portal");
tfPortalHostname.setText("https://portal.reveng.ai");
Expand Down Expand Up @@ -158,16 +163,13 @@ public void mouseClicked(java.awt.event.MouseEvent e) {
runTestsButton.addActionListener(e -> {
var apiInfo = new ApiInfo(tfApiHostname.getText(), tfPortalHostname.getText(), tfApiKey.getText());
try {
apiInfo.checkCredentials();
credentialValidator.validate(apiInfo);
credentialsValidated = true;
// TODO: Get the user for this key once the API exists
notifyListenersOfValidityChanged();

} catch (InvalidAPIInfoException ex) {
credentialsValidated = false;
notifyListenersOfStatusMessage("Problem with user info:\n" + ex.getMessage());
}

});
userDetailsPanel.add(runTestsButton, gbc);
}
Expand All @@ -186,6 +188,12 @@ public WizardPanelDisplayability getPanelDisplayabilityAndUpdateState(WizardStat

@Override
public void enterPanel(WizardState<SetupWizardStateKey> state) throws IllegalPanelStateException {
// Pick up credential validator from state if provided (e.g. by tests)
CredentialValidator validator = (CredentialValidator) state.get(SetupWizardStateKey.CREDENTIAL_VALIDATOR);
if (validator != null) {
this.credentialValidator = validator;
}

// Populate fields with existing state information if present
String existingApiKey = (String) state.get(SetupWizardStateKey.API_KEY);
String existingHostname = (String) state.get(SetupWizardStateKey.HOSTNAME);
Expand Down Expand Up @@ -213,11 +221,6 @@ public void enterPanel(WizardState<SetupWizardStateKey> state) throws IllegalPan
}
}

private void validateCredentialsFromState() {
// This method is no longer used - validation state is preserved from wizard state
// Keeping for backwards compatibility but it should not be called
loggingService.warn("validateCredentialsFromState() called - this should not happen with new validation logic");
}

@Override
public void leavePanel(WizardState<SetupWizardStateKey> state) {
Expand Down
Loading
Loading