feat(gui): improvements of Quark integration (#1119) (PR #1199)

* Add quark installation
* add error/warning dialog
* change Quark task to background task
* fix missing the last line of input stream
This commit is contained in:
Shaun Dang
2021-06-30 23:04:50 +08:00
committed by GitHub
parent fef3e21c70
commit cc99409a7e
@@ -7,18 +7,21 @@ import java.io.File;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;
import javax.swing.WindowConstants;
import org.slf4j.Logger;
@@ -27,6 +30,8 @@ import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import jadx.gui.jobs.IBackgroundTask;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JRoot;
import jadx.gui.utils.NLS;
@@ -37,67 +42,51 @@ class QuarkDialog extends JDialog {
private static final long serialVersionUID = 4855753773520368215L;
private static final Logger LOG = LoggerFactory.getLogger(QuarkDialog.class);
private static final String QUARK_CMD_LOG_MESSAGE = "Running Quark cmd: {}";
private static final String QUARK_INTERRUPT_MESSAGE = "Quark process interrupted: {}";
private static final String QUARK_FAILED_MESSAGE = "Failed to execute Quark.";
private static final String QUARK_CMD = "quark";
private static final int LARGE_APK_SIZE = 30;
private static final Path QUARK_DIR_PATH = Paths.get(System.getProperty("user.home"), ".quark-engine");
private Path venvPath = Paths.get(QUARK_DIR_PATH.toString(), "quark_venv");
private File quarkReportFile;
private final transient JadxSettings settings;
private final transient MainWindow mainWindow;
private JProgressBar progressBar;
private JPanel progressPane;
private JComboBox<String> selectFile;
private JComboBox<String> fileSelectCombo;
private final List<Path> files;
private ArrayList<Path> analyzeFile = new ArrayList<Path>();
private Map<String, Path> choosableFiles = new HashMap<>();
public QuarkDialog(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.settings = mainWindow.getSettings();
this.files = mainWindow.getWrapper().getOpenPaths();
if (!prepareAnalysis()) {
// The files are unable to analysis by Quark
fileNameExtensionFilter();
if (choosableFiles.isEmpty()) {
UiUtils.errorMessage(mainWindow, "Quark is unable to analyze the selected file.");
LOG.error("Quark: The files cannot be analyze. {}", files);
return;
}
initUI();
}
private boolean prepareAnalysis() {
String[] exts = new String[] { "apk", "dex" };
private void fileNameExtensionFilter() {
String[] extensions = new String[] { "apk", "dex" };
if (this.files.size() != 1) {
for (Path filePath : this.files) {
String fileName = filePath.toString();
int dotIndex = fileName.lastIndexOf('.');
String extension = (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
for (Path filePath : this.files) {
String fileName = filePath.toString();
int dotIndex = fileName.lastIndexOf('.');
String extension = (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
if (Arrays.stream(exts).noneMatch(extension::equals)) {
LOG.warn("Quark: Current file can't be analysis: {}", fileName);
continue;
}
analyzeFile.add(filePath);
if (Arrays.stream(extensions).noneMatch(extension::equals)) {
LOG.debug("Quark: {} is not apk nor dex", fileName);
continue;
}
return true;
choosableFiles.put(fileName, filePath);
}
String fileName = this.files.get(0).toString();
int dotIndex = fileName.lastIndexOf('.');
String extension = (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
if (Arrays.stream(exts).noneMatch(extension::equals)) {
LOG.warn("Quark: Current file can't be analysis: {}", fileName);
return false;
}
analyzeFile.add(this.files.get(0));
return true;
}
private String[] filesToStringArr() {
String[] arr = new String[files.size()];
int index = 0;
for (Path file : analyzeFile) {
arr[index] = file.getFileName().toString();
index++;
}
return arr;
}
public final void initUI() {
@@ -105,7 +94,8 @@ class QuarkDialog extends JDialog {
JLabel selectApkText = new JLabel("Select Apk/Dex");
description.setAlignmentX(0.5f);
selectFile = new JComboBox<String>(filesToStringArr());
String[] comboFiles = choosableFiles.keySet().toArray(new String[choosableFiles.size()]);
fileSelectCombo = new JComboBox<>(comboFiles);
JPanel textPane = new JPanel();
@@ -113,30 +103,19 @@ class QuarkDialog extends JDialog {
JPanel selectApkPanel = new JPanel();
selectApkPanel.add(selectApkText);
selectApkPanel.add(selectFile);
progressPane = new JPanel();
progressPane.setVisible(false);
progressPane.setSize(150, 10);
progressBar = new JProgressBar(0, 100);
progressBar.setSize(150, 10);
progressBar.setIndeterminate(true);
progressBar.setStringPainted(false);
progressPane.add(progressBar);
selectApkPanel.add(fileSelectCombo);
JPanel buttonPane = new JPanel();
JButton start = new JButton("Start");
JButton close = new JButton(NLS.str("tabs.close"));
close.addActionListener(event -> close());
start.addActionListener(event -> analyzeAPK());
start.addActionListener(event -> mainWindow.getBackgroundExecutor().execute(new QuarkTask()));
buttonPane.add(start);
buttonPane.add(close);
getRootPane().setDefaultButton(close);
JPanel centerPane = new JPanel();
centerPane.add(selectApkPanel);
centerPane.add(progressPane);
Container contentPane = getContentPane();
contentPane.add(textPane, BorderLayout.PAGE_START);
@@ -154,25 +133,6 @@ class QuarkDialog extends JDialog {
UiUtils.addEscapeShortCutToDispose(this);
}
private void analyzeAPK() {
LoadTask task = new LoadTask();
task.execute();
}
private void loadReportFile() {
try (Reader reader = new FileReader(quarkReportFile)) {
JsonObject quarkReport = (JsonObject) JsonParser.parseReader(reader);
QuarkReport quarkNode = QuarkReport.analysisAPK(quarkReport);
JRoot root = mainWindow.getCacheObject().getJRoot();
root.update();
root.add(quarkNode);
mainWindow.reloadTree();
} catch (Exception e) {
LOG.error("Quark: Load report failed: ", e);
}
}
private void close() {
dispose();
}
@@ -183,52 +143,259 @@ class QuarkDialog extends JDialog {
super.dispose();
}
private class LoadTask extends SwingWorker<Void, Void> {
public LoadTask() {
progressPane.setVisible(true);
private class QuarkTask implements IBackgroundTask {
private Process quarkProcess;
private boolean isVenv = false;
public QuarkTask() {
dispose();
}
@Override
public Void doInBackground() {
try {
quarkReportFile = File.createTempFile("QuarkReport-", ".json");
private boolean isPipInstalled() {
List<String> cmdList = new ArrayList<>();
cmdList.add("pip3");
return executeCommand(cmdList);
}
String apkName = (String) selectFile.getSelectedItem();
String apkPath = null;
for (Path path : files) {
if (path.getFileName().toString().equals(apkName)) {
apkPath = path.toString();
}
private boolean isQuarkInstalled() {
List<String> cmdList = new ArrayList<>();
cmdList.add(QUARK_CMD);
if (executeCommand(cmdList)) {
return true;
}
isVenv = true;
cmdList = new ArrayList<>();
cmdList.add(getVenvPath(QUARK_CMD).toString());
return executeCommand(cmdList);
}
private void createVirtualenv() {
// Check if venv exist
if (Files.exists(getVenvPath("activate"))) {
return;
}
List<String> cmdList = new ArrayList<>();
if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) {
cmdList.add("python");
cmdList.add("-m");
cmdList.add("venv");
} else {
cmdList.add("virtualenv");
}
cmdList.add(venvPath.toString());
try {
LOG.debug(QUARK_CMD_LOG_MESSAGE, cmdList);
Process process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
process.waitFor();
} catch (InterruptedException e) {
LOG.error(QUARK_INTERRUPT_MESSAGE, e.getMessage(), e);
Thread.currentThread().interrupt();
} catch (Exception e) {
UiUtils.errorMessage(mainWindow, "Failed to create virtual environment.");
LOG.error("Failed to create virtual environment: {}", e.getMessage(), e);
}
}
private boolean installQuark() {
List<String> cmdList = new ArrayList<>();
String command = (isVenv) ? getVenvPath("pip3").toString() : "pip3";
cmdList.add(command);
cmdList.add("install");
cmdList.add("quark-engine");
cmdList.add("--upgrade");
try {
LOG.debug(QUARK_CMD_LOG_MESSAGE, cmdList);
Process process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
process.waitFor();
if (!isQuarkInstalled()) {
return false;
}
} catch (InterruptedException e) {
LOG.error(QUARK_INTERRUPT_MESSAGE, e.getMessage(), e);
Thread.currentThread().interrupt();
} catch (Exception e) {
UiUtils.errorMessage(mainWindow, "Failed to install quark-engine.");
LOG.error("Failed to execute pip install command: {}", String.join(" ", cmdList), e);
return false;
}
return true;
}
private void updateQuarkRules() {
List<String> cmdList = new ArrayList<>();
String command = (isVenv) ? getVenvPath("freshquark").toString() : "freshquark";
cmdList.add(command);
executeCommand(cmdList);
}
private boolean analyzeAPK() {
try {
updateQuarkRules();
quarkReportFile = File.createTempFile("QuarkReport-", ".json");
String apkName = (String) fileSelectCombo.getSelectedItem();
String apkPath = choosableFiles.get(apkName).toString();
List<String> cmdList = new ArrayList<>();
cmdList.add("quark");
String command = (isVenv) ? getVenvPath(QUARK_CMD).toString() : QUARK_CMD;
cmdList.add(command);
cmdList.add("-a");
cmdList.add(apkPath);
cmdList.add("-s");
cmdList.add("-o");
cmdList.add(quarkReportFile.getAbsolutePath());
LOG.debug("Running Quark cmd: {}", String.join(" ", cmdList));
Process process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
try (BufferedReader buf = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
LOG.debug("Quark analyzing...");
while (process.isAlive()) {
String output = buf.readLine();
if (output != null) {
LOG.debug(output);
}
LOG.debug(QUARK_CMD_LOG_MESSAGE, cmdList);
quarkProcess = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
try (BufferedReader buf = new BufferedReader(new InputStreamReader(quarkProcess.getInputStream()))) {
String output = null;
while ((output = buf.readLine()) != null) {
LOG.debug(output);
}
}
} catch (Exception e) {
LOG.error("Quark failed: ", e);
dispose();
LOG.error("Failed to execute Quark: {}", e.getMessage(), e);
return false;
}
return true;
}
private boolean executeCommand(List<String> cmdList) {
try {
LOG.debug(QUARK_CMD_LOG_MESSAGE, cmdList);
Process process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
process.waitFor();
} catch (InterruptedException e) {
LOG.error(QUARK_INTERRUPT_MESSAGE, e.getMessage(), e);
Thread.currentThread().interrupt();
} catch (Exception e) {
LOG.error("Failed to execute command: {}", String.join(" ", cmdList), e);
return false;
}
return true;
}
public boolean checkFileSize(int sizeThreshold) {
String apkName = (String) fileSelectCombo.getSelectedItem();
try {
int fileSize = (int) Files.size(choosableFiles.get(apkName)) / 1024 / 1024;
if (fileSize > sizeThreshold) {
return false;
}
} catch (Exception e) {
LOG.error("Failed to calculate file: {}", e.getMessage(), e);
return false;
}
return true;
}
private void loadReportFile() {
try (Reader reader = new FileReader(quarkReportFile)) {
JsonObject quarkReport = (JsonObject) JsonParser.parseReader(reader);
QuarkReport quarkNode = QuarkReport.analysisAPK(quarkReport);
JRoot root = mainWindow.getCacheObject().getJRoot();
root.update();
root.add(quarkNode);
mainWindow.reloadTree();
} catch (Exception e) {
UiUtils.errorMessage(mainWindow, "Failed to load Quark report.");
LOG.error("Failed to load Quark report.", e);
}
}
private Path getVenvPath(String cmd) {
String os = System.getProperty("os.name").toLowerCase();
if (os.indexOf("win") >= 0) {
return Paths.get(venvPath.toString(), "Scripts", String.format("%s.exe", cmd));
} else {
return Paths.get(venvPath.toString(), "bin", cmd);
}
return null;
}
@Override
public void done() {
public String getTitle() {
return "Quark:";
}
@Override
public boolean canBeCanceled() {
return true;
}
@Override
public List<Runnable> scheduleJobs() {
List<Runnable> jobs = new ArrayList<>();
// mkdir `$HOME/.quark-engine/`
File directory = new File(QUARK_DIR_PATH.toString());
if (!directory.isDirectory()) {
directory.mkdirs();
}
if (!checkFileSize(LARGE_APK_SIZE)) {
int result = JOptionPane.showConfirmDialog(mainWindow,
"The selected file size is too large (over 30M) that may take a long time to analyze, do you want to continue",
"Quark: Warning", JOptionPane.YES_NO_OPTION);
if (result == JOptionPane.NO_OPTION) {
return jobs;
}
}
jobs.add(() -> {
if (!isPipInstalled()) {
UiUtils.errorMessage(mainWindow, "Pip is not installed.");
LOG.error("Pip is not installed");
mainWindow.cancelBackgroundJobs();
}
});
jobs.add(() -> {
mainWindow.getProgressPane().setLabel("Check Quark installed");
if (!isQuarkInstalled()) {
LOG.warn("Quark is not installed, do you want to install it from PyPI?");
int result = JOptionPane.showConfirmDialog(mainWindow,
"Quark is not installed, do you want to install it from PyPI?", "Warning",
JOptionPane.YES_NO_OPTION);
if (result == JOptionPane.YES_OPTION) {
mainWindow.getProgressPane().setLabel("Installing Quark");
createVirtualenv();
if (!installQuark()) {
UiUtils.errorMessage(mainWindow, "Failed to install quark-engine.");
mainWindow.cancelBackgroundJobs();
}
}
if (result == JOptionPane.NO_OPTION) {
mainWindow.cancelBackgroundJobs();
}
}
});
jobs.add(() -> {
mainWindow.getProgressPane().setLabel("Analyzing");
if (!analyzeAPK()) {
UiUtils.errorMessage(mainWindow, "Quark: Failed to analyze apk.");
mainWindow.cancelBackgroundJobs();
}
});
return jobs;
}
@Override
public void onFinish(TaskStatus status, long skipped) {
if (quarkProcess.exitValue() != 0) {
LOG.error(QUARK_FAILED_MESSAGE);
return;
}
loadReportFile();
dispose();
}
}
}