Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3175d14
Redact secrets when logging
seanbudd Apr 17, 2026
e3a0dea
Apply suggestions from code review
seanbudd Apr 17, 2026
a0702a8
fix inclusion
seanbudd Apr 17, 2026
94c1668
fix up detection and formatting
seanbudd Apr 17, 2026
520d9bc
Pre-commit auto-fix
pre-commit-ci[bot] Apr 17, 2026
098b3cb
document changes
seanbudd Apr 17, 2026
fb2fe7e
review suggestions
seanbudd Apr 17, 2026
9b266b5
Pre-commit auto-fix
pre-commit-ci[bot] Apr 17, 2026
a2551f4
add unit tests
seanbudd Apr 17, 2026
c15a546
Pre-commit auto-fix
pre-commit-ci[bot] Apr 17, 2026
86851a5
Merge remote-tracking branch 'origin/master' into detectSecrets
seanbudd Apr 20, 2026
f58c1f6
fix detect_secrets inclusion
seanbudd Apr 20, 2026
cb35311
fix up
seanbudd Apr 20, 2026
143bd04
Merge remote-tracking branch 'origin/master' into detectSecrets
seanbudd Apr 21, 2026
bec0131
Add log level and warning
seanbudd Apr 21, 2026
cdc846f
Fix behaviour
seanbudd Apr 21, 2026
2d8e7ed
Merge remote-tracking branch 'origin/master' into detectSecrets
seanbudd Apr 21, 2026
0b52053
Pre-commit auto-fix
pre-commit-ci[bot] Apr 21, 2026
3177144
fix elif
seanbudd Apr 21, 2026
43d007b
Add help id
seanbudd Apr 21, 2026
ccc6aa2
Apply suggestion from @seanbudd
seanbudd Apr 21, 2026
00eb8bf
Apply suggestion from @seanbudd
seanbudd Apr 21, 2026
8f54736
restore mathcat?
seanbudd Apr 21, 2026
f0c7c4b
Merge remote-tracking branch 'refs/remotes/origin/detectSecrets' into…
seanbudd Apr 21, 2026
49b2043
Update source/logHandler.py
seanbudd Apr 21, 2026
3924102
Update source/logHandler.py
seanbudd Apr 21, 2026
7c982ff
line end
seanbudd Apr 22, 2026
c71c883
Merge remote-tracking branch 'origin/master' into detectSecrets
seanbudd Apr 22, 2026
85dab52
fix ineq
seanbudd Apr 22, 2026
8a70eba
Merge remote-tracking branch 'origin/master' into detectSecrets
seanbudd Apr 22, 2026
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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ dependencies = [
# pinned due to incompatibility with py2exe
# https://github.com/py2exe/py2exe/issues/241
"charset-normalizer==3.4.4",
# secret scanning for logging
"detect-secrets==1.5.0",
]

[project.urls]
Expand Down
11 changes: 5 additions & 6 deletions source/argsParsing.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2024 NV Access Limited, Cyrille Bougot
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2024-2026 NV Access Limited, Cyrille Bougot
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

import argparse
import sys
Expand Down Expand Up @@ -108,8 +107,8 @@ def _createNVDAArgParser() -> NoConsoleOptionParser:
dest="logLevel",
type=int,
default=0, # 0 means unspecified in command line.
choices=[10, 12, 15, 20, 100],
help="The lowest level of message logged (debug 10, input/output 12, debugwarning 15, info 20, off 100).\n"
choices=[5, 10, 12, 15, 20, 100],
help="The lowest level of message logged (secrets 5, debug 10, input/output 12, debugwarning 15, info 20, off 100).\n"
"Default value is 20 (info) or the user configured setting.\n"
"Logging is always disabled if secure mode is enabled.\n",
)
Expand Down
3 changes: 2 additions & 1 deletion source/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def _loadConfig(self, fn: str | None, fileError: bool = False) -> ConfigObj:
profileUpgrader.upgrade(profile, self.validator, writeProfileFunc)
except Exception as e:
# Log at level info to ensure that the profile is logged.
log.info("Config before schema update:\n%s" % profileCopy, exc_info=False)
log.info("Config before schema update:\n%s" % profileCopy, exc_info=False, redactSecrets=True)
raise e
# since profile settings are not yet imported we have to "peek" to see
# if debug level logging is enabled.
Expand All @@ -617,6 +617,7 @@ def _loadConfig(self, fn: str | None, fileError: bool = False) -> ConfigObj:
"Config loaded (after upgrade, and in the state it will be used by NVDA):\n{0}".format(
profile,
),
redactSecrets=True,
)
return profile

Expand Down
7 changes: 5 additions & 2 deletions source/config/configFlags.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022-2026 NV Access Limited, Cyrille Bougot, Cary-rowen
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

"""Flags used to define the possible values for an option in the configuration.
Use Flag.MEMBER.value to set a new value or compare with an option in the config;
Expand Down Expand Up @@ -421,6 +421,7 @@ class LoggingLevel(DisplayStringIntEnum):
DEBUGWARNING = Logger.DEBUGWARNING
IO = Logger.IO
DEBUG = Logger.DEBUG
SECRETS = Logger.SECRETS

@property
def _displayStringLabels(self) -> dict[int, str]:
Expand All @@ -435,6 +436,8 @@ def _displayStringLabels(self) -> dict[int, str]:
self.IO: _("input/output"),
# Translators: One of the log levels of NVDA (the debug mode shows debug messages as NVDA runs).
self.DEBUG: _("debug"),
# Translators: One of the log levels of NVDA (the secrets mode logs debug messages without redacting secrets).
self.SECRETS: _("secrets"),
}


Expand Down
6 changes: 3 additions & 3 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Copyright (C) 2006-2026 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt,
# Joseph Lee, Dawid Pieper, mltony, Bram Duvigneau, Cyrille Bougot, Rob Meredith,
# Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka, Cary-rowen, Kefas Lungu
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

from io import StringIO
from configobj import ConfigObj
Expand All @@ -24,7 +24,7 @@
saveConfigurationOnExit = boolean(default=True)
askToExit = boolean(default=true)
playStartAndExitSounds = boolean(default=true)
#possible log levels are DEBUG, IO, DEBUGWARNING, INFO
# possible log levels are SECRETS, DEBUG, IO, DEBUGWARNING, INFO and OFF
loggingLevel = string(default="INFO")
showWelcomeDialogAtStartup = boolean(default=true)
preventDisplayTurningOff = boolean(default=true)
Expand Down
57 changes: 52 additions & 5 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# Takuya Nishimoto, jakubl7545, Tony Malykh, Rob Meredith,
# Burman's Computer and Education Ltd, hwf1324, Cary-rowen, Christopher Proß, Tianze
# Neil Soiffer, Ryan McCleary, Kefas Lungu.
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

import bisect
import copy
Expand Down Expand Up @@ -85,6 +85,7 @@

import gui
import gui.contextHelp
import gui.message
import screenCurtain
import api
import ui
Expand Down Expand Up @@ -6187,6 +6188,47 @@ class PrivacyAndSecuritySettingsPanel(SettingsPanel):
title = _("Privacy and Security")
helpId = "PrivacyAndSecuritySettings"

def _getSelectedLogLevel(self) -> LoggingLevel:
selection = self._logLevelCombo.GetSelection()
if selection == wx.NOT_FOUND:
return LoggingLevel[config.conf["general"]["loggingLevel"]]
return list(LoggingLevel)[selection]

def _confirmLogLevelChange(self, selectedLogLevel: LoggingLevel) -> bool:
if selectedLogLevel == LoggingLevel.SECRETS:
message = _(
# Translators: Warning shown when enabling the secrets log level from NVDA settings.
"Setting the logging level to secrets will write sensitive information to the log without redaction, "
"including passwords, API keys, or other private data. "
"Only enable this temporarily if you explicitly need unredacted diagnostic logs. "
"Do you want to continue?",
)
caption = _(
# Translators: Title of the warning dialog shown when enabling the secrets log level.
"High risk logging level",
)
else:
message = _(
# Translators: Warning shown when enabling a logging level above info from NVDA settings.
"Setting the logging level above info may record sensitive information such as typed input, "
"speech output, or other private data in the log. "
"Only enable higher logging levels temporarily while troubleshooting. "
"Do you want to continue?",
)
caption = _(
# Translators: Title of the warning dialog shown when enabling a risky logging level.
"Warning",
)
dialog = gui.message.MessageDialog(
parent=self,
message=message,
title=caption,
dialogType=gui.message.DialogType.WARNING,
buttons=gui.message.DefaultButtonSet.YES_NO,
helpId="GeneralSettingsLogLevel",
)
return dialog.ShowModal() == gui.message.ReturnCode.YES

def makeSettings(self, sizer: wx.BoxSizer):
sHelper = guiHelper.BoxSizerHelper(self, sizer=sizer)

Expand Down Expand Up @@ -6265,6 +6307,7 @@ def makeSettings(self, sizer: wx.BoxSizer):
)
except StopIteration:
log.debugWarning("Could not set log level list to current log level")
self._savedLogLevel = self._getSelectedLogLevel()

self._allowUsageStatsCheckBox: wx.CheckBox = generalGroup.addItem(
# Translators: The label of a checkbox in privacy and security settings to toggle allowing of usage stats gathering
Expand Down Expand Up @@ -6301,10 +6344,14 @@ def onSave(self):
)

if not logHandler.isLogLevelForced():
config.conf["general"]["loggingLevel"] = logging.getLevelName(
list(LoggingLevel)[self._logLevelCombo.GetSelection()],
selectedLogLevel = self._getSelectedLogLevel()
updateLogLevel = selectedLogLevel != self._savedLogLevel and (
selectedLogLevel >= LoggingLevel.INFO or self._confirmLogLevelChange(selectedLogLevel)
)
logHandler.setLogLevelFromConfig()
if updateLogLevel:
config.conf["general"]["loggingLevel"] = logging.getLevelName(selectedLogLevel)
logHandler.setLogLevelFromConfig()
self._savedLogLevel = selectedLogLevel

if updateCheck:
config.conf["update"]["allowUsageStats"] = self._allowUsageStatsCheckBox.IsChecked()
Expand Down
64 changes: 51 additions & 13 deletions source/logHandler.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2007-2026 NV Access Limited, Rui Batista, Joseph Lee, Leonard de Ruijter, Babbage B.V.,
# Accessolutions, Julien Cochuyt, Cyrille Bougot, Łukasz Golonka
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

"""Utilities and classes to manage logging in NVDA"""

Expand All @@ -21,6 +21,7 @@
import winKernel
import buildVersion
from typing import (
Any,
Literal,
NamedTuple,
Protocol,
Expand All @@ -31,6 +32,7 @@
import NVDAState
from NVDAState import WritePaths


if TYPE_CHECKING:
import extensionPoints

Expand Down Expand Up @@ -222,6 +224,7 @@ class Logger(logging.Logger):
from logging import DEBUG, INFO, WARNING, WARN, ERROR, CRITICAL

# Our custom levels.
SECRETS = 5
IO = 12
DEBUGWARNING = 15
OFF = 100
Expand All @@ -233,15 +236,30 @@ class Logger(logging.Logger):

def _log(
self,
level,
msg,
args,
exc_info=None,
extra=None,
codepath=None,
activateLogViewer=False,
stack_info=None,
):
level: int,
msg: str,
args: tuple[Any, ...],
exc_info: _excInfo_t | bool | BaseException = None,
extra: dict | None = None,
codepath: str | None = None,
activateLogViewer: bool = False,
stack_info: list[traceback.FrameSummary] | bool | None = None,
redactSecrets: bool = False,
) -> Any:
"""Logs a message with the given severity level.

:param level: The severity level of the log message.
:param msg: The log message, which may contain format specifiers that will be replaced by the values in `args`.
:param args: The arguments to be merged into `msg` using the `%` operator for string formatting.
:param exc_info: Exception information to be logged
:param extra: Additional information to be logged
:param codepath: The code path where the log was generated
:param activateLogViewer: Whether to activate the log viewer
:param stack_info: Stack information to be logged
:param redactSecrets: Whether to check for and redact secrets in the log message
:return: The result of the logging operation (None for builtin handlers).
"""

if not extra:
extra = {}

Expand Down Expand Up @@ -273,7 +291,26 @@ def _log(
"".join(traceback.format_list(stack_info)).rstrip(),
)

res = super()._log(level, msg, args, exc_info, extra)
if redactSecrets and self.getEffectiveLevel() < self.SECRETS:
from detect_secrets.core.scan import scan_line
from detect_secrets.settings import default_settings

try:
formattedMsg = msg % args if args else msg
except Exception:
formattedMsg = msg
self.exception(
"Failed to format log message for secret redaction, logging unredacted exception.",
)

with default_settings():
for secret in list(scan_line(formattedMsg)):
formattedMsg = formattedMsg.replace(secret.secret_value, "****")

res = super()._log(level, formattedMsg, (), exc_info, extra)

else:
res = super()._log(level, msg, args, exc_info, extra)

if activateLogViewer:
# Make the log text we just wrote appear in the log viewer.
Expand Down Expand Up @@ -583,6 +620,7 @@ def initialize(shouldDoRemoteLogging=False):
@type shouldDoRemoteLogging: bool
"""
global log, logHandler
logging.addLevelName(Logger.SECRETS, "SECRETS")
logging.addLevelName(Logger.DEBUGWARNING, "DEBUGWARNING")
logging.addLevelName(Logger.IO, "IO")
logging.addLevelName(Logger.OFF, "OFF")
Expand Down Expand Up @@ -661,7 +699,7 @@ def setLogLevelFromConfig():
level = logging.getLevelNamesMapping().get(levelName)
# The lone exception to level higher than INFO is "OFF" (100).
# Setting a log level to something other than options found in the GUI is unsupported.
if level is None or level not in (log.DEBUG, log.IO, log.DEBUGWARNING, log.INFO, log.OFF):
if level is None or level not in (log.SECRETS, log.DEBUG, log.IO, log.DEBUGWARNING, log.INFO, log.OFF):
log.warning("invalid setting for logging level: %s" % levelName)
level = log.INFO
config.conf["general"]["loggingLevel"] = logging.getLevelName(log.INFO)
Expand Down
12 changes: 10 additions & 2 deletions source/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,6 @@ def _genManifestTemplate(shouldHaveUIAccess: bool) -> tuple[int, int, bytes]:
"winxptheme",
# numpy is an optional dependency of comtypes but we don't require it.
"numpy",
# multiprocessing isn't going to work in a frozen environment
"multiprocessing",
"concurrent.futures.process",
# Tomli is part of Python 3.11+ as Tomlib, but is imported as tomli by cryptography, which causes an infinite loop in py2exe
"tomli",
Expand All @@ -302,6 +300,16 @@ def _genManifestTemplate(shouldHaveUIAccess: bool) -> tuple[int, int, bytes]:
"NVDAObjects.JAB",
"NVDAObjects.UIA",
"NVDAObjects.window",
# detect-secrets loads plugins and filters dynamically using pkgutil/importlib,
# so the relevant packages must be bundled explicitly for frozen builds.
"detect_secrets",
"detect_secrets.core",
"detect_secrets.core.plugins",
"detect_secrets.filters",
"detect_secrets.filters.gibberish",
"detect_secrets.plugins",
"detect_secrets.transformers",
"detect_secrets.util",
"virtualBuffers",
"appModules",
"comInterfaces",
Expand Down
Loading
Loading