update ida-settings api

This commit is contained in:
mr-tz
2025-11-05 16:25:04 +00:00
parent 503c34b8f9
commit e9b02b372a
2 changed files with 33 additions and 27 deletions

View File

@@ -41,6 +41,7 @@
- add `ida-plugin.json` for inclusion in the IDA Pro plugin repository @williballenthin - add `ida-plugin.json` for inclusion in the IDA Pro plugin repository @williballenthin
- ida plugin: add Qt compatibility layer for PyQt5 and PySide6 support @williballenthin #2707 - ida plugin: add Qt compatibility layer for PyQt5 and PySide6 support @williballenthin #2707
- ida plugin: update ida-settings API @mr-tz #2736
### Development ### Development

View File

@@ -54,7 +54,6 @@ from capa.ida.plugin.qt_compat import QtGui, QtCore, QtWidgets
from capa.features.extractors.base_extractor import FunctionHandle from capa.features.extractors.base_extractor import FunctionHandle
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
settings = ida_settings.IDASettings("capa")
CAPA_SETTINGS_RULE_PATH = "rule_path" CAPA_SETTINGS_RULE_PATH = "rule_path"
CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author" CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author"
@@ -79,6 +78,13 @@ AnalyzeOptionsText = {
} }
def get_setting(key: str, default=None):
try:
return ida_settings.get_current_plugin_setting(key)
except KeyError:
return default
def write_file(path: Path, data): def write_file(path: Path, data):
""" """ """ """
path.write_bytes(data) path.write_bytes(data)
@@ -107,7 +113,7 @@ class QLineEditClicked(QtWidgets.QLineEdit):
old = self.text() old = self.text()
new = str( new = str(
QtWidgets.QFileDialog.getExistingDirectory( QtWidgets.QFileDialog.getExistingDirectory(
self.parent(), "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "") self.parent(), "Please select a capa rules directory", get_setting(CAPA_SETTINGS_RULE_PATH, "")
) )
) )
if new: if new:
@@ -125,8 +131,8 @@ class CapaSettingsInputDialog(QtWidgets.QDialog):
self.setMinimumWidth(500) self.setMinimumWidth(500)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.edit_rule_path = QLineEditClicked(settings.user.get(CAPA_SETTINGS_RULE_PATH, "")) self.edit_rule_path = QLineEditClicked(get_setting(CAPA_SETTINGS_RULE_PATH, ""))
self.edit_rule_author = QtWidgets.QLineEdit(settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "")) self.edit_rule_author = QtWidgets.QLineEdit(get_setting(CAPA_SETTINGS_RULEGEN_AUTHOR, ""))
self.edit_rule_scope = QtWidgets.QComboBox() self.edit_rule_scope = QtWidgets.QComboBox()
self.edit_rules_link = QtWidgets.QLabel() self.edit_rules_link = QtWidgets.QLabel()
self.edit_analyze = QtWidgets.QComboBox() self.edit_analyze = QtWidgets.QComboBox()
@@ -141,11 +147,11 @@ class CapaSettingsInputDialog(QtWidgets.QDialog):
scopes = ("file", "function", "basic block", "instruction") scopes = ("file", "function", "basic block", "instruction")
self.edit_rule_scope.addItems(scopes) self.edit_rule_scope.addItems(scopes)
self.edit_rule_scope.setCurrentIndex(scopes.index(settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"))) self.edit_rule_scope.setCurrentIndex(scopes.index(get_setting(CAPA_SETTINGS_RULEGEN_SCOPE, "function")))
self.edit_analyze.addItems(AnalyzeOptionsText.values()) self.edit_analyze.addItems(AnalyzeOptionsText.values())
# set the default analysis option here # set the default analysis option here
self.edit_analyze.setCurrentIndex(settings.user.get(CAPA_SETTINGS_ANALYZE, Options.NO_ANALYSIS)) self.edit_analyze.setCurrentIndex(get_setting(CAPA_SETTINGS_ANALYZE, Options.NO_ANALYSIS))
buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self) buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self)
@@ -235,7 +241,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.Show() self.Show()
analyze = settings.user.get(CAPA_SETTINGS_ANALYZE) analyze = get_setting(CAPA_SETTINGS_ANALYZE)
if analyze != Options.NO_ANALYSIS or (option & Options.ANALYZE_AUTO) == Options.ANALYZE_AUTO: if analyze != Options.NO_ANALYSIS or (option & Options.ANALYZE_AUTO) == Options.ANALYZE_AUTO:
self.analyze_program(analyze=analyze) self.analyze_program(analyze=analyze)
@@ -581,7 +587,7 @@ class CapaExplorerForm(idaapi.PluginForm):
def ensure_capa_settings_rule_path(self): def ensure_capa_settings_rule_path(self):
try: try:
path: str = settings.user.get(CAPA_SETTINGS_RULE_PATH, "") path: str = get_setting(CAPA_SETTINGS_RULE_PATH, "")
# resolve rules directory - check self and settings first, then ask user # resolve rules directory - check self and settings first, then ask user
# pathlib.Path considers "" equivalent to "." so we first check if rule path is an empty string # pathlib.Path considers "" equivalent to "." so we first check if rule path is an empty string
@@ -611,7 +617,7 @@ class CapaExplorerForm(idaapi.PluginForm):
logger.error("rule path %s does not exist or cannot be accessed", path) logger.error("rule path %s does not exist or cannot be accessed", path)
return False return False
settings.user[CAPA_SETTINGS_RULE_PATH] = path ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULE_PATH, path)
except UserCancelledError: except UserCancelledError:
capa.ida.helpers.inform_user_ida_ui("Analysis requires capa rules") capa.ida.helpers.inform_user_ida_ui("Analysis requires capa rules")
logger.warning( logger.warning(
@@ -635,7 +641,7 @@ class CapaExplorerForm(idaapi.PluginForm):
if not self.ensure_capa_settings_rule_path(): if not self.ensure_capa_settings_rule_path():
return False return False
rule_path: Path = Path(settings.user.get(CAPA_SETTINGS_RULE_PATH, "")) rule_path: Path = Path(get_setting(CAPA_SETTINGS_RULE_PATH, ""))
try: try:
def on_load_rule(_, i, total): def on_load_rule(_, i, total):
@@ -649,10 +655,10 @@ class CapaExplorerForm(idaapi.PluginForm):
return None return None
except Exception as e: except Exception as e:
capa.ida.helpers.inform_user_ida_ui( capa.ida.helpers.inform_user_ida_ui(
f"Failed to load capa rules from {settings.user[CAPA_SETTINGS_RULE_PATH]}" f"Failed to load capa rules from {get_setting(CAPA_SETTINGS_RULE_PATH)}"
) )
logger.error("Failed to load capa rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e) logger.error("Failed to load capa rules from %s (error: %s).", get_setting(CAPA_SETTINGS_RULE_PATH), e)
logger.error( logger.error(
"Make sure your file directory contains properly " # noqa: G003 [logging statement uses +] "Make sure your file directory contains properly " # noqa: G003 [logging statement uses +]
+ "formatted capa rules. You can download and extract the official rules from %s. " + "formatted capa rules. You can download and extract the official rules from %s. "
@@ -661,7 +667,7 @@ class CapaExplorerForm(idaapi.PluginForm):
CAPA_RULESET_DOC_URL, CAPA_RULESET_DOC_URL,
) )
settings.user[CAPA_SETTINGS_RULE_PATH] = "" ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULE_PATH, "")
return None return None
def load_capa_results(self, new_analysis, from_cache): def load_capa_results(self, new_analysis, from_cache):
@@ -701,7 +707,7 @@ class CapaExplorerForm(idaapi.PluginForm):
update_wait_box("verifying cached results") update_wait_box("verifying cached results")
count_source_rules = self.program_analysis_ruleset_cache.source_rule_count count_source_rules = self.program_analysis_ruleset_cache.source_rule_count
user_settings = settings.user[CAPA_SETTINGS_RULE_PATH] user_settings = get_setting(CAPA_SETTINGS_RULE_PATH)
view_status_rules: str = f"{user_settings} ({count_source_rules} rules)" view_status_rules: str = f"{user_settings} ({count_source_rules} rules)"
# warn user about potentially outdated rules, depending on the use-case this may be expected # warn user about potentially outdated rules, depending on the use-case this may be expected
@@ -775,7 +781,7 @@ class CapaExplorerForm(idaapi.PluginForm):
update_wait_box("extracting features") update_wait_box("extracting features")
try: try:
meta = capa.ida.helpers.collect_metadata([Path(settings.user[CAPA_SETTINGS_RULE_PATH])]) meta = capa.ida.helpers.collect_metadata([Path(get_setting(CAPA_SETTINGS_RULE_PATH))])
capabilities = capa.capabilities.common.find_capabilities( capabilities = capa.capabilities.common.find_capabilities(
ruleset, self.feature_extractor, disable_progress=True ruleset, self.feature_extractor, disable_progress=True
) )
@@ -855,7 +861,7 @@ class CapaExplorerForm(idaapi.PluginForm):
except Exception as e: except Exception as e:
logger.exception("Failed to save results to database (error: %s)", e) logger.exception("Failed to save results to database (error: %s)", e)
return False return False
user_settings = settings.user[CAPA_SETTINGS_RULE_PATH] user_settings = get_setting(CAPA_SETTINGS_RULE_PATH)
count_source_rules = self.program_analysis_ruleset_cache.source_rule_count count_source_rules = self.program_analysis_ruleset_cache.source_rule_count
new_view_status = f"capa rules: {user_settings} ({count_source_rules} rules)" new_view_status = f"capa rules: {user_settings} ({count_source_rules} rules)"
# regardless of new analysis, render results - e.g. we may only want to render results after checking # regardless of new analysis, render results - e.g. we may only want to render results after checking
@@ -1076,13 +1082,13 @@ class CapaExplorerForm(idaapi.PluginForm):
# load preview and feature tree # load preview and feature tree
self.view_rulegen_preview.load_preview_meta( self.view_rulegen_preview.load_preview_meta(
self.rulegen_current_function.address if self.rulegen_current_function else None, self.rulegen_current_function.address if self.rulegen_current_function else None,
settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"), get_setting(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"),
settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"), get_setting(CAPA_SETTINGS_RULEGEN_SCOPE, "function"),
) )
self.view_rulegen_features.load_features(all_file_features, all_function_features) self.view_rulegen_features.load_features(all_file_features, all_function_features)
self.set_view_status_label(f"capa rules: {settings.user[CAPA_SETTINGS_RULE_PATH]}") self.set_view_status_label(f"capa rules: {get_setting(CAPA_SETTINGS_RULE_PATH)}")
except Exception as e: except Exception as e:
logger.exception("Failed to render views (error: %s)", e) logger.exception("Failed to render views (error: %s)", e)
return False return False
@@ -1303,12 +1309,11 @@ class CapaExplorerForm(idaapi.PluginForm):
""" """ """ """
dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent) dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent)
if dialog.exec_(): if dialog.exec_():
( vals = dialog.get_values()
settings.user[CAPA_SETTINGS_RULE_PATH], ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULE_PATH, vals[0])
settings.user[CAPA_SETTINGS_RULEGEN_AUTHOR], ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULEGEN_AUTHOR, vals[1])
settings.user[CAPA_SETTINGS_RULEGEN_SCOPE], ida_settings.set_current_plugin_setting(CAPA_SETTINGS_RULEGEN_SCOPE, vals[2])
settings.user[CAPA_SETTINGS_ANALYZE], ida_settings.set_current_plugin_setting(CAPA_SETTINGS_ANALYZE, vals[3])
) = dialog.get_values()
def save_program_analysis(self): def save_program_analysis(self):
""" """ """ """
@@ -1408,7 +1413,7 @@ class CapaExplorerForm(idaapi.PluginForm):
"""create Qt dialog to ask user for a directory""" """create Qt dialog to ask user for a directory"""
return str( return str(
QtWidgets.QFileDialog.getExistingDirectory( QtWidgets.QFileDialog.getExistingDirectory(
self.parent, "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "") self.parent, "Please select a capa rules directory", get_setting(CAPA_SETTINGS_RULE_PATH, "")
) )
) )
@@ -1417,7 +1422,7 @@ class CapaExplorerForm(idaapi.PluginForm):
return QtWidgets.QFileDialog.getSaveFileName( return QtWidgets.QFileDialog.getSaveFileName(
None, None,
"Please select a location to save capa rule file", "Please select a location to save capa rule file",
settings.user.get(CAPA_SETTINGS_RULE_PATH, ""), get_setting(CAPA_SETTINGS_RULE_PATH, ""),
"*.yml", "*.yml",
)[0] )[0]