diff --git a/.githooks/pre-commit b/.githooks/pre-commit index c34cdcfd94..108c6319e7 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -10,12 +10,9 @@ if [[ -n "$PY_FILES" ]]; then if [[ ! -v VIRTUAL_ENV ]]; then source .venv/bin/activate fi - echo "Checking:" - echo "$PY_FILES" - # Run black against changed python files for this commit - black --check --diff ${PY_FILES//$'\n'/ } # Run ruff (against all files, it's fast enough) - ruff check . --diff && echo "ruff passed!" + ruff format . && ruff check . --diff \ + && echo "ruff passed!" else exit 0 fi diff --git a/Makefile b/Makefile index c22c3adf3d..9d091fa15c 100644 --- a/Makefile +++ b/Makefile @@ -61,16 +61,6 @@ update-pip-requirements: update-admin-pip-requirements update-python3-requiremen # ################# -.PHONY: check-black -check-black: ## Check Python source code formatting with black - @echo "███ Running black check..." - @black --check --diff . - @echo - -.PHONY: black -black: ## Update Python source code formatting with black - @black securedrop . - .PHONY: ansible-config-lint ansible-config-lint: ## Run custom Ansible linting tasks. @echo "███ Linting Ansible configuration..." @@ -94,15 +84,19 @@ app-lint-full: ## Test pylint compliance, with no checks disabled. @echo .PHONY: check-ruff -check-ruff: ## Lint Python source files. +check-ruff: ## Check linting and formatting of Python source files. @echo "███ Running ruff..." - @ruff check . --show-source + @ruff format . --diff + @ruff check . @echo .PHONY: ruff ruff: ## Update Python source file formatting. + @ruff format . @ruff check . --fix +fix: ruff ## Apply automatic fixes. + # The --disable=names is required to use the BEM syntax # # https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/ .PHONY: html-lint @@ -139,7 +133,7 @@ yamllint: ## Lint YAML files (does not validate syntax!). # While the order mostly doesn't matter here, keep "check-ruff" first, since it # gives the broadest coverage and runs (and therefore fails) fastest. .PHONY: lint -lint: check-ruff ansible-config-lint app-lint check-black html-lint shellcheck typelint yamllint check-strings check-supported-locales check-desktop-files ## Runs all lint checks +lint: check-ruff ansible-config-lint app-lint html-lint shellcheck typelint yamllint check-strings check-supported-locales check-desktop-files ## Runs all lint checks .PHONY: safety safety: ## Run `safety check` to check python dependencies for vulnerabilities. diff --git a/admin/bootstrap.py b/admin/bootstrap.py index a85f2be7a5..838a57b408 100755 --- a/admin/bootstrap.py +++ b/admin/bootstrap.py @@ -117,10 +117,7 @@ def is_missing_dependency() -> bool: return True # If any packages couldn't be found, it may point to an apt cache issue - if "Unable to locate package" in apt_query_result.stderr: - return True - - return False + return "Unable to locate package" in apt_query_result.stderr except subprocess.CalledProcessError as e: sdlog.error("Error checking apt dependencies") @@ -185,7 +182,6 @@ def envsetup(args: argparse.Namespace, virtualenv_dir: str = VENV_DIR) -> None: # virtualenv doesnt exist? Install dependencies and create if not os.path.exists(virtualenv_dir): - # Technically you can create a virtualenv from within python # but pip can only be run over Tor on Tails, and debugging that # along with instaling a third-party dependency is not worth @@ -260,12 +256,12 @@ def install_pip_dependencies( ] ansible_ver = subprocess.run( - maybe_torify() + ansible_vercheck_cmd, text=True, capture_output=True + maybe_torify() + ansible_vercheck_cmd, text=True, capture_output=True, check=False ) if ansible_ver.stdout.startswith("2.9"): sdlog.info("Ansible is out-of-date, removing it.") delete_result = subprocess.run( - maybe_torify() + ansible_uninstall_cmd, capture_output=True, text=True + maybe_torify() + ansible_uninstall_cmd, capture_output=True, text=True, check=False ) if delete_result.returncode != 0: sdlog.error( diff --git a/admin/securedrop_admin/__init__.py b/admin/securedrop_admin/__init__.py index be35b16a66..ae69c6ba99 100755 --- a/admin/securedrop_admin/__init__.py +++ b/admin/securedrop_admin/__init__.py @@ -66,7 +66,7 @@ # Check OpenSSH version - ansible requires an extra argument for scp on OpenSSH 9 def openssh_version() -> int: try: - result = subprocess.run(["ssh", "-V"], capture_output=True, text=True) + result = subprocess.run(["ssh", "-V"], capture_output=True, text=True, check=False) if result.stderr.startswith("OpenSSH_9"): return 9 elif result.stderr.startswith("OpenSSH_8"): @@ -75,7 +75,6 @@ def openssh_version() -> int: return 0 except subprocess.CalledProcessError: return 0 - pass return 0 @@ -130,14 +129,14 @@ def validate(self, document: Document) -> bool: class ValidateTime(Validator): def validate(self, document: Document) -> bool: - if document.text.isdigit() and int(document.text) in range(0, 24): + if document.text.isdigit() and int(document.text) in range(24): return True raise ValidationError(message="Must be an integer between 0 and 23") class ValidateUser(Validator): def validate(self, document: Document) -> bool: text = document.text - if text != "" and text != "root" and text != "amnesia": + if text not in ("", "root", "amnesia"): return True raise ValidationError(message="Must not be root, amnesia or an empty string") @@ -160,8 +159,8 @@ def validate(self, document: Document) -> bool: raise ValidationError( message=( "DNS server(s) should be a space/comma-separated list " - "of up to {} IP addresses" - ).format(MAX_NAMESERVERS) + f"of up to {MAX_NAMESERVERS} IP addresses" + ) ) return True @@ -194,7 +193,7 @@ def validate(self, document: Document) -> bool: class ValidateYesNo(Validator): def validate(self, document: Document) -> bool: text = document.text.lower() - if text == "yes" or text == "no": + if text in ("yes", "no"): return True raise ValidationError(message="Must be either yes or no") @@ -769,7 +768,6 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: update_status, latest_tag = check_for_updates(cli_args) if update_status is True: - # Useful for troubleshooting branch_status = get_git_branch(cli_args) @@ -793,7 +791,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: ) sdlog.error( "If you are certain you want to proceed, run:\n\n\t" - "./securedrop-admin --force {}\n".format(cmd_name) + f"./securedrop-admin --force {cmd_name}\n" ) sdlog.error("To apply the latest updates, run:\n\n\t" "./securedrop-admin update\n") sdlog.error( diff --git a/admin/tests/test_integration.py b/admin/tests/test_integration.py index 618b547659..9ccff54b44 100644 --- a/admin/tests/test_integration.py +++ b/admin/tests/test_integration.py @@ -531,7 +531,7 @@ def test_sdconfig_enable_https_disable_pow_on_source_interface(): """ -@pytest.fixture() +@pytest.fixture def securedrop_git_repo(tmpdir): cwd = os.getcwd() os.chdir(str(tmpdir)) diff --git a/admin/tests/test_securedrop-admin.py b/admin/tests/test_securedrop-admin.py index 5b55807c40..acd65999f2 100644 --- a/admin/tests/test_securedrop-admin.py +++ b/admin/tests/test_securedrop-admin.py @@ -76,9 +76,7 @@ def test_update_check_decorator_when_no_update_needed(self, caplog): "securedrop_admin.check_for_updates", side_effect=[[False, "1.5.0"]] ) as mocked_check, mock.patch( "securedrop_admin.get_git_branch", side_effect=["develop"] - ), mock.patch( - "sys.exit" - ) as mocked_exit: + ), mock.patch("sys.exit") as mocked_exit: # The decorator itself interprets --force args = argparse.Namespace(force=False) rv = securedrop_admin.update_check_required("update_check_test")(lambda _: 100)(args) @@ -101,9 +99,7 @@ def test_update_check_decorator_when_update_needed(self, caplog): "securedrop_admin.check_for_updates", side_effect=[[True, "1.5.0"]] ) as mocked_check, mock.patch( "securedrop_admin.get_git_branch", side_effect=["bad_branch"] - ), mock.patch( - "sys.exit" - ) as mocked_exit: + ), mock.patch("sys.exit") as mocked_exit: # The decorator itself interprets --force args = argparse.Namespace(force=False) securedrop_admin.update_check_required("update_check_test")(lambda _: _)(args) @@ -125,9 +121,7 @@ def test_update_check_decorator_when_skipped(self, caplog): "securedrop_admin.check_for_updates", side_effect=[[True, "1.5.0"]] ) as mocked_check, mock.patch( "securedrop_admin.get_git_branch", side_effect=["develop"] - ), mock.patch( - "sys.exit" - ) as mocked_exit: + ), mock.patch("sys.exit") as mocked_exit: # The decorator itself interprets --force args = argparse.Namespace(force=True) rv = securedrop_admin.update_check_required("update_check_test")(lambda _: 100)(args) @@ -847,7 +841,7 @@ def verify_desc_consistency_optional(self, site_config, desc): if callable(default): default = default() assert site_config.user_prompt_config_one(desc, None) == default - assert type(default) == etype + assert type(default) is etype def verify_desc_consistency(self, site_config, desc): self.verify_desc_consistency_optional(site_config, desc) @@ -935,7 +929,7 @@ def verify_desc_consistency_allow_empty(self, site_config, desc): (var, default, etype, prompt, validator, transform, condition) = desc # verify the default passes validation assert site_config.user_prompt_config_one(desc, None) == default - assert type(default) == etype + assert type(default) is etype def verify_prompt_fingerprint(self, site_config, desc): self.verify_prompt_not_empty(site_config, desc) @@ -962,7 +956,7 @@ def verify_prompt_securedrop_supported_locales(self, site_config, desc): (var, default, etype, prompt, validator, transform, condition) = desc # verify the default passes validation assert site_config.user_prompt_config_one(desc, None) == default - assert type(default) == etype + assert type(default) is etype assert site_config.user_prompt_config_one(desc, "fr_FR en_US") == ["fr_FR", "en_US"] assert site_config.user_prompt_config_one(desc, ["fr_FR", "en_US"]) == ["fr_FR", "en_US"] assert site_config.user_prompt_config_one(desc, "") == [] diff --git a/devops/scripts/verify-mo.py b/devops/scripts/verify-mo.py index 8309e8f5c8..f7cf2ff77e 100755 --- a/devops/scripts/verify-mo.py +++ b/devops/scripts/verify-mo.py @@ -21,7 +21,7 @@ import shlex import subprocess from pathlib import Path -from typing import Any, Iterator, Optional, Set +from typing import Iterator, Set import polib from translate.tools.pocompile import convertmo @@ -59,9 +59,9 @@ def __enter__(self) -> "CatalogVerifier": def __exit__( self, - exc_type: Optional[Any], - exc_value: Optional[Any], - traceback: Optional[Any], + exc_type: object, + exc_value: object, + traceback: object, ) -> None: """Clean up.""" @@ -113,11 +113,12 @@ def diffoscope_call( # because we want to inherit the Python virtual environment # in which we're invoked. # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true - return subprocess.run( + return subprocess.run( # noqa: S602 cmd, capture_output=True, env=os.environ, - shell=True, # noqa: S602 + shell=True, + check=False, ) def reproduce(self) -> None: diff --git a/install_files/ansible-base/roles/restore/files/compare_torrc.py b/install_files/ansible-base/roles/restore/files/compare_torrc.py index 53c34aae6c..c46091e60b 100644 --- a/install_files/ansible-base/roles/restore/files/compare_torrc.py +++ b/install_files/ansible-base/roles/restore/files/compare_torrc.py @@ -53,15 +53,12 @@ def strset(s): sys.exit(0) print( - "The Tor configuration on the app server offers version {} services.".format( - strset(server_versions) - ) + f"The Tor configuration on the app server offers version {strset(server_versions)} " + "services." ) print( - "The Tor configuration in this backup offers version {} services.".format( - strset(backup_versions) - ) + f"The Tor configuration in this backup offers version {strset(backup_versions)} services." ) print("\nIncompatible configuration: Restoring a backup including a different ") diff --git a/install_files/ansible-base/roles/tails-config/files/securedrop_init.py b/install_files/ansible-base/roles/tails-config/files/securedrop_init.py index a7d5a68338..813f76e3f9 100644 --- a/install_files/ansible-base/roles/tails-config/files/securedrop_init.py +++ b/install_files/ansible-base/roles/tails-config/files/securedrop_init.py @@ -207,7 +207,6 @@ ) if "OK" in verify_proc: - # Updating the cert chain requires sudo privileges os.setresgid(0, 0, -1) os.setresuid(0, 0, -1) diff --git a/journalist_gui/journalist_gui/SecureDropUpdater.py b/journalist_gui/journalist_gui/SecureDropUpdater.py index a243cf0e71..75e8018035 100644 --- a/journalist_gui/journalist_gui/SecureDropUpdater.py +++ b/journalist_gui/journalist_gui/SecureDropUpdater.py @@ -17,16 +17,12 @@ def password_is_set(): - pwd_flag = subprocess.check_output(["passwd", "--status"]).decode("utf-8").split()[1] - if pwd_flag == "NP": - return False - return True + return pwd_flag != "NP" def prevent_second_instance(app: QtWidgets.QApplication, name: str) -> None: - # Null byte triggers abstract namespace IDENTIFIER = "\0" + name ALREADY_BOUND_ERRNO = 98 diff --git a/journalist_gui/test_gui.py b/journalist_gui/test_gui.py index 4d57b8b931..fbec32c4f8 100644 --- a/journalist_gui/test_gui.py +++ b/journalist_gui/test_gui.py @@ -101,7 +101,6 @@ def test_default_tab(self): assert self.window.tabWidget.currentIndex() == 0 def test_output_tab(self): - tab = self.window.tabWidget.tabBar() QTest.mouseClick(tab, Qt.LeftButton) assert self.window.tabWidget.currentIndex() == self.window.tabWidget.indexOf( diff --git a/molecule/testinfra/app-code/test_securedrop_app_code.py b/molecule/testinfra/app-code/test_securedrop_app_code.py index 245ef7683b..d795cce67b 100644 --- a/molecule/testinfra/app-code/test_securedrop_app_code.py +++ b/molecule/testinfra/app-code/test_securedrop_app_code.py @@ -52,7 +52,7 @@ def test_unwanted_packages_absent(host, package): assert not host.package(package).is_installed -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_securedrop_application_test_locale(host): """ Ensure both SecureDrop DEFAULT_LOCALE and SUPPORTED_LOCALES are present. @@ -66,7 +66,7 @@ def test_securedrop_application_test_locale(host): assert "\nSUPPORTED_LOCALES = ['el', 'ar', 'en_US']\n" in securedrop_config.content_string -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_securedrop_application_test_journalist_key(host): """ Ensure the SecureDrop Application GPG public key file is present. diff --git a/molecule/testinfra/app-code/test_securedrop_rqrequeue.py b/molecule/testinfra/app-code/test_securedrop_rqrequeue.py index 16b414cdfa..d5f49ecc6d 100644 --- a/molecule/testinfra/app-code/test_securedrop_rqrequeue.py +++ b/molecule/testinfra/app-code/test_securedrop_rqrequeue.py @@ -17,13 +17,9 @@ def test_securedrop_rqrequeue_service(host): "Wants=redis-server.service", "", "[Service]", - 'Environment=PYTHONPATH="{}:{}"'.format( - securedrop_test_vars.securedrop_code, - securedrop_test_vars.securedrop_venv_site_packages, - ), - "ExecStart={}/python /var/www/securedrop/scripts/rqrequeue --interval 60".format( - securedrop_test_vars.securedrop_venv_bin - ), + f'Environment=PYTHONPATH="{securedrop_test_vars.securedrop_code}:{securedrop_test_vars.securedrop_venv_site_packages}"', + f"ExecStart={securedrop_test_vars.securedrop_venv_bin}/python /var/www/securedrop/" + "scripts/rqrequeue --interval 60", "PrivateDevices=yes", "PrivateTmp=yes", "ProtectSystem=full", diff --git a/molecule/testinfra/app-code/test_securedrop_rqworker.py b/molecule/testinfra/app-code/test_securedrop_rqworker.py index 1afdb20c47..88347d10cb 100644 --- a/molecule/testinfra/app-code/test_securedrop_rqworker.py +++ b/molecule/testinfra/app-code/test_securedrop_rqworker.py @@ -19,10 +19,7 @@ def test_securedrop_rqworker_service(host): "Wants=redis-server.service", "", "[Service]", - 'Environment=PYTHONPATH="{}:{}"'.format( - securedrop_test_vars.securedrop_code, - securedrop_test_vars.securedrop_venv_site_packages, - ), + f'Environment=PYTHONPATH="{securedrop_test_vars.securedrop_code}:{securedrop_test_vars.securedrop_venv_site_packages}"', f"ExecStart={securedrop_test_vars.securedrop_venv_bin}/rqworker -c rq_config", "PrivateDevices=yes", "PrivateTmp=yes", diff --git a/molecule/testinfra/app-code/test_securedrop_shredder_configuration.py b/molecule/testinfra/app-code/test_securedrop_shredder_configuration.py index a39164197f..688d2b30c3 100644 --- a/molecule/testinfra/app-code/test_securedrop_shredder_configuration.py +++ b/molecule/testinfra/app-code/test_securedrop_shredder_configuration.py @@ -16,13 +16,9 @@ def test_securedrop_shredder_service(host): "Description=SecureDrop shredder", "", "[Service]", - 'Environment=PYTHONPATH="{}:{}"'.format( - securedrop_test_vars.securedrop_code, - securedrop_test_vars.securedrop_venv_site_packages, - ), - "ExecStart={}/python /var/www/securedrop/scripts/shredder --interval 60".format( - securedrop_test_vars.securedrop_venv_bin - ), + f'Environment=PYTHONPATH="{securedrop_test_vars.securedrop_code}:{securedrop_test_vars.securedrop_venv_site_packages}"', + f"ExecStart={securedrop_test_vars.securedrop_venv_bin}/python /var/www/securedrop/" + "scripts/shredder --interval 60", "PrivateDevices=yes", "PrivateTmp=yes", "ProtectSystem=full", diff --git a/molecule/testinfra/app-code/test_securedrop_source_deleter_configuration.py b/molecule/testinfra/app-code/test_securedrop_source_deleter_configuration.py index 9de809a288..a30077a386 100644 --- a/molecule/testinfra/app-code/test_securedrop_source_deleter_configuration.py +++ b/molecule/testinfra/app-code/test_securedrop_source_deleter_configuration.py @@ -15,13 +15,9 @@ def test_securedrop_source_deleter_service(host): "Description=SecureDrop Source deleter", "", "[Service]", - 'Environment=PYTHONPATH="{}:{}"'.format( - securedrop_test_vars.securedrop_code, - securedrop_test_vars.securedrop_venv_site_packages, - ), - "ExecStart={}/python /var/www/securedrop/scripts/source_deleter --interval 10".format( - securedrop_test_vars.securedrop_venv_bin - ), + f'Environment=PYTHONPATH="{securedrop_test_vars.securedrop_code}:{securedrop_test_vars.securedrop_venv_site_packages}"', + f"ExecStart={securedrop_test_vars.securedrop_venv_bin}/python /var/www/securedrop/" + "scripts/source_deleter --interval 10", "PrivateDevices=yes", "PrivateTmp=yes", "ProtectSystem=full", diff --git a/molecule/testinfra/app/apache/test_apache_journalist_interface.py b/molecule/testinfra/app/apache/test_apache_journalist_interface.py index f3977ad46f..7d71a3a216 100644 --- a/molecule/testinfra/app/apache/test_apache_journalist_interface.py +++ b/molecule/testinfra/app/apache/test_apache_journalist_interface.py @@ -111,20 +111,14 @@ def test_apache_logging_journalist_interface(host): AllowOverride None Require all denied -""".strip( - "\n" - ), +""".strip("\n"), """ Require all granted # Cache static resources for 1 hour Header set Cache-Control "max-age=3600" -""".strip( - "\n" - ).format( - securedrop_test_vars.securedrop_code - ), +""".strip("\n").format(securedrop_test_vars.securedrop_code), """ Options None @@ -136,11 +130,7 @@ def test_apache_logging_journalist_interface(host): Require all denied -""".strip( - "\n" - ).format( - securedrop_test_vars.securedrop_code - ), +""".strip("\n").format(securedrop_test_vars.securedrop_code), ], ) def test_apache_config_journalist_interface_access_control(host, apache_opt): diff --git a/molecule/testinfra/app/apache/test_apache_source_interface.py b/molecule/testinfra/app/apache/test_apache_source_interface.py index 68830dc991..f348422bd4 100644 --- a/molecule/testinfra/app/apache/test_apache_source_interface.py +++ b/molecule/testinfra/app/apache/test_apache_source_interface.py @@ -79,20 +79,14 @@ def test_apache_config_source_interface_headers_per_distro(host): AllowOverride None Require all denied -""".strip( - "\n" - ), +""".strip("\n"), """ Require all granted # Cache static resources for 1 hour Header set Cache-Control "max-age=3600" -""".strip( - "\n" - ).format( - securedrop_test_vars.securedrop_code - ), +""".strip("\n").format(securedrop_test_vars.securedrop_code), """ Options None @@ -104,11 +98,7 @@ def test_apache_config_source_interface_headers_per_distro(host): Require all denied -""".strip( - "\n" - ).format( - securedrop_test_vars.securedrop_code - ), +""".strip("\n").format(securedrop_test_vars.securedrop_code), ], ) def test_apache_config_source_interface_access_control(host, apache_opt): diff --git a/molecule/testinfra/app/apache/test_apache_system_config.py b/molecule/testinfra/app/apache/test_apache_system_config.py index b40a0e9cc4..1d5456ab0f 100644 --- a/molecule/testinfra/app/apache/test_apache_system_config.py +++ b/molecule/testinfra/app/apache/test_apache_system_config.py @@ -90,9 +90,7 @@ def test_apache_ports_config(host, port): assert f.group == "root" assert f.mode == 0o644 - listening_regex = "^Listen {}:{}$".format( - re.escape(securedrop_test_vars.apache_listening_address), port - ) + listening_regex = f"^Listen {re.escape(securedrop_test_vars.apache_listening_address)}:{port}$" assert f.contains(listening_regex) diff --git a/molecule/testinfra/app/test_app_network.py b/molecule/testinfra/app/test_app_network.py index cf20efeac1..fb5474ce40 100644 --- a/molecule/testinfra/app/test_app_network.py +++ b/molecule/testinfra/app/test_app_network.py @@ -9,9 +9,8 @@ testinfra_hosts = [securedrop_test_vars.app_hostname] -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_app_iptables_rules(host): - local = host.get_host("local://") # Build a dict of variables to pass to jinja for iptables comparison @@ -32,9 +31,7 @@ def test_app_iptables_rules(host): # Build iptables scrape cmd, purge comments + counters iptables = r"iptables-save | sed 's/ \[[0-9]*\:[0-9]*\]//g' | egrep -v '^#'" environment = os.environ.get("SECUREDROP_TESTINFRA_TARGET_HOST", "staging") - iptables_file = "{}/iptables-app-{}.j2".format( - os.path.dirname(os.path.abspath(__file__)), environment - ) + iptables_file = f"{os.path.dirname(os.path.abspath(__file__))}/iptables-app-{environment}.j2" # template out a local iptables jinja file jinja_iptables = Template(open(iptables_file).read()) diff --git a/molecule/testinfra/app/test_apparmor.py b/molecule/testinfra/app/test_apparmor.py index 2977bf6469..5ce70ffaa3 100644 --- a/molecule/testinfra/app/test_apparmor.py +++ b/molecule/testinfra/app/test_apparmor.py @@ -103,7 +103,7 @@ def test_aastatus_unconfined(host): expected_unconfined = 0 unconfined_chk = str( - "{} processes are unconfined but have" " a profile defined".format(expected_unconfined) + f"{expected_unconfined} processes are unconfined but have" " a profile defined" ) with host.sudo(): aa_status_output = host.check_output("aa-status") diff --git a/molecule/testinfra/app/test_appenv.py b/molecule/testinfra/app/test_appenv.py index e5d4f7b904..ead90c1a0a 100644 --- a/molecule/testinfra/app/test_appenv.py +++ b/molecule/testinfra/app/test_appenv.py @@ -8,14 +8,16 @@ @pytest.mark.parametrize("exp_pip_pkg", sdvars.pip_deps) def test_app_pip_deps(host, exp_pip_pkg): """Ensure expected package versions are installed""" - cmd = "{}/bin/python3 -c \"from importlib.metadata import version; print(version('{}'))\"".format( # noqa - sdvars.securedrop_venv, exp_pip_pkg["name"] + cmd = ( + "{}/bin/python3 -c \"from importlib.metadata import version; print(version('{}'))\"".format( + sdvars.securedrop_venv, exp_pip_pkg["name"] + ) ) result = host.run(cmd) assert result.stdout.strip() == exp_pip_pkg["version"] -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_app_wsgi(host): """ensure logging is enabled for source interface in staging""" f = host.file("/var/www/source.wsgi") @@ -74,9 +76,8 @@ def test_app_code_venv(host): """ Ensure the securedrop-app-code virtualenv is correct. """ - cmd = """test -z $VIRTUAL_ENV && . {}/bin/activate && test "$VIRTUAL_ENV" = "{}" """.format( - sdvars.securedrop_venv, sdvars.securedrop_venv - ) + cmd = f"""test -z $VIRTUAL_ENV && . {sdvars.securedrop_venv}/bin/activate && " + "test "$VIRTUAL_ENV" = "{sdvars.securedrop_venv}" """ result = host.run(cmd) assert result.rc == 0 @@ -87,7 +88,7 @@ def test_supervisor_not_installed(host): assert host.package("supervisor").is_installed is False -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_gpg_key_in_keyring(host): """ensure test gpg key is present in app keyring""" with host.sudo(sdvars.securedrop_user): diff --git a/molecule/testinfra/app/test_ossec_agent.py b/molecule/testinfra/app/test_ossec_agent.py index e972e8ab83..50d7071a72 100644 --- a/molecule/testinfra/app/test_ossec_agent.py +++ b/molecule/testinfra/app/test_ossec_agent.py @@ -39,7 +39,7 @@ def test_ossec_agent_installed(host): # Permissions don't match between Ansible and OSSEC deb packages postinst. -@pytest.mark.xfail() +@pytest.mark.xfail def test_ossec_keyfile_present(host): """ensure client keyfile for ossec-agent is present""" pattern = "^1024 {} {} [0-9a-f]{{64}}$".format( diff --git a/molecule/testinfra/app/test_smoke.py b/molecule/testinfra/app/test_smoke.py index 1f4c949cea..b28361f1e2 100644 --- a/molecule/testinfra/app/test_smoke.py +++ b/molecule/testinfra/app/test_smoke.py @@ -60,7 +60,7 @@ def test_redwood(host): assert len(parsed[2]) == 40 -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_weak_submission_key(host): """ If the Submission Key is weak (e.g. has a SHA-1 signature), diff --git a/molecule/testinfra/app/test_tor_config.py b/molecule/testinfra/app/test_tor_config.py index c9422c87cf..ba6ce93fec 100644 --- a/molecule/testinfra/app/test_tor_config.py +++ b/molecule/testinfra/app/test_tor_config.py @@ -68,7 +68,7 @@ def test_tor_torrc_sandbox(host): assert not f.contains("^.*Sandbox.*$") -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_tor_v2_onion_url_file_absent(host): v2_url_filepath = "/var/lib/securedrop/source_v2_url" with host.sudo(): @@ -76,7 +76,7 @@ def test_tor_v2_onion_url_file_absent(host): assert not f.exists -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_tor_v3_onion_url_readable_by_app(host): v3_url_filepath = "/var/lib/securedrop/source_v3_url" with host.sudo(): diff --git a/molecule/testinfra/app/test_tor_hidden_services.py b/molecule/testinfra/app/test_tor_hidden_services.py index ffe1431d6a..38d22a1643 100644 --- a/molecule/testinfra/app/test_tor_hidden_services.py +++ b/molecule/testinfra/app/test_tor_hidden_services.py @@ -9,7 +9,7 @@ # Prod Tor services may have unexpected configs # TODO: read from admin workstation site-specific file if available -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod @pytest.mark.parametrize("tor_service", sdvars.tor_services) def test_tor_service_directories(host, tor_service): """ @@ -23,7 +23,7 @@ def test_tor_service_directories(host, tor_service): assert f.group == "debian-tor" -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod @pytest.mark.parametrize("tor_service", sdvars.tor_services) def test_tor_service_hostnames(host, tor_service): """ @@ -58,7 +58,7 @@ def test_tor_service_hostnames(host, tor_service): assert re.search(f"^{ths_hostname_regex_v3}$", f.content_string) -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod @pytest.mark.parametrize("tor_service", sdvars.tor_services) def test_tor_services_config(host, tor_service): """ diff --git a/molecule/testinfra/common/test_fpf_apt_repo.py b/molecule/testinfra/common/test_fpf_apt_repo.py index cfcde90a2c..91b945ee43 100644 --- a/molecule/testinfra/common/test_fpf_apt_repo.py +++ b/molecule/testinfra/common/test_fpf_apt_repo.py @@ -28,9 +28,8 @@ def test_fpf_apt_repo_present(host): f = host.file("/etc/apt/sources.list.d/apt_test_freedom_press.list") else: f = host.file("/etc/apt/sources.list.d/apt_freedom_press.list") - repo_regex = r"^deb \[arch=amd64\] {} {} main$".format( - re.escape(test_vars.fpf_apt_repo_url), re.escape(host.system_info.codename) - ) + repo_regex = rf"^deb \[arch=amd64\] {re.escape(test_vars.fpf_apt_repo_url)} " + rf"{re.escape(host.system_info.codename)} main$" assert f.contains(repo_regex) @@ -61,7 +60,6 @@ def test_fpf_apt_repo_fingerprint(host): [ "pub 4096R/FC9F6818 2014-10-26 [expired: 2016-10-27]", "pub 4096R/00F4AD77 2016-10-20 [expired: 2017-10-20]", - "pub 4096R/00F4AD77 2016-10-20 [expired: 2017-10-20]", "pub 4096R/7B22E6A3 2021-05-10 [expired: 2022-07-04]", "pub 4096R/7B22E6A3 2021-05-10 [expired: 2023-07-04]", "pub 4096R/7B22E6A3 2021-05-10 [expired: 2024-07-08]", diff --git a/molecule/testinfra/common/test_grsecurity.py b/molecule/testinfra/common/test_grsecurity.py index 49364e7b45..83d17f58eb 100644 --- a/molecule/testinfra/common/test_grsecurity.py +++ b/molecule/testinfra/common/test_grsecurity.py @@ -113,9 +113,7 @@ def test_grsecurity_paxtest(host): paxtest_cmd += " | grep -P '^(Executable|Return)'" paxtest_results = host.check_output(paxtest_cmd) - paxtest_template_path = "{}/paxtest_results.j2".format( - os.path.dirname(os.path.abspath(__file__)) - ) + paxtest_template_path = f"{os.path.dirname(os.path.abspath(__file__))}/paxtest_results.j2" memcpy_result = "Killed" # Versions of paxtest newer than 0.9.12 or so will report diff --git a/molecule/testinfra/common/test_user_config.py b/molecule/testinfra/common/test_user_config.py index 3d703cd442..339aaaa2d8 100644 --- a/molecule/testinfra/common/test_user_config.py +++ b/molecule/testinfra/common/test_user_config.py @@ -88,7 +88,7 @@ def test_tmux_installed(host): assert host.package("tmux").is_installed -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_sudoers_tmux_env_deprecated(host): """ Previous version of the Ansible config set the tmux config diff --git a/molecule/testinfra/mon/test_mon_network.py b/molecule/testinfra/mon/test_mon_network.py index 69a80601ce..92efb7e296 100644 --- a/molecule/testinfra/mon/test_mon_network.py +++ b/molecule/testinfra/mon/test_mon_network.py @@ -9,9 +9,8 @@ testinfra_hosts = [securedrop_test_vars.monitor_hostname] -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod def test_mon_iptables_rules(host): - local = host.get_host("local://") # Build a dict of variables to pass to jinja for iptables comparison @@ -32,9 +31,7 @@ def test_mon_iptables_rules(host): # Build iptables scrape cmd, purge comments + counters iptables = r"iptables-save | sed 's/ \[[0-9]*\:[0-9]*\]//g' | egrep -v '^#'" environment = os.environ.get("SECUREDROP_TESTINFRA_TARGET_HOST", "staging") - iptables_file = "{}/iptables-mon-{}.j2".format( - os.path.dirname(os.path.abspath(__file__)), environment - ) + iptables_file = f"{os.path.dirname(os.path.abspath(__file__))}/iptables-mon-{environment}.j2" # template out a local iptables jinja file jinja_iptables = Template(open(iptables_file).read()) @@ -54,7 +51,7 @@ def test_mon_iptables_rules(host): assert iptables_expected == iptables -@pytest.mark.skip_in_prod() +@pytest.mark.skip_in_prod @pytest.mark.parametrize( "ossec_service", [ diff --git a/molecule/testinfra/mon/test_ossec_server.py b/molecule/testinfra/mon/test_ossec_server.py index 32534885a6..a515b836ef 100644 --- a/molecule/testinfra/mon/test_ossec_server.py +++ b/molecule/testinfra/mon/test_ossec_server.py @@ -32,7 +32,7 @@ def test_ossec_service_start_style(host): # Permissions don't match between Ansible and OSSEC deb packages postinst. -@pytest.mark.xfail() +@pytest.mark.xfail @pytest.mark.parametrize( "keyfile", [ @@ -59,7 +59,7 @@ def test_ossec_keyfiles(host, keyfile): # Permissions don't match between Ansible and OSSEC deb packages postinst. -@pytest.mark.xfail() +@pytest.mark.xfail def test_procmail_log(host): """ Ensure procmail log file exist with proper ownership. diff --git a/molecule/testinfra/mon/test_postfix.py b/molecule/testinfra/mon/test_postfix.py index 3801bb9eaf..19dadd0946 100644 --- a/molecule/testinfra/mon/test_postfix.py +++ b/molecule/testinfra/mon/test_postfix.py @@ -37,11 +37,8 @@ def test_postfix_generic_maps(host): """ assert host.file("/etc/postfix/generic").exists assert host.file("/etc/postfix/generic").contains( - "^ossec@{} {}@{}".format( - securedrop_test_vars.monitor_hostname, - securedrop_test_vars.sasl_username, - securedrop_test_vars.sasl_domain, - ) + f"^ossec@{securedrop_test_vars.monitor_hostname} {securedrop_test_vars.sasl_username}@" + f"{securedrop_test_vars.sasl_domain}" ) assert host.file("/etc/postfix/main.cf").contains("^smtp_generic_maps") assert host.file("/etc/postfix/main.cf").contains( diff --git a/molecule/testinfra/ossec/test_journalist_mail.py b/molecule/testinfra/ossec/test_journalist_mail.py index 1e0fc1938f..f483e35000 100644 --- a/molecule/testinfra/ossec/test_journalist_mail.py +++ b/molecule/testinfra/ossec/test_journalist_mail.py @@ -70,7 +70,7 @@ def test_procmail(self, host): ): # Look up CWD, in case tests move in the future current_dir = os.path.dirname(os.path.abspath(__file__)) - self.ansible(host, "copy", "dest=/tmp/{f} src={d}/{f}".format(f=f, d=current_dir)) + self.ansible(host, "copy", f"dest=/tmp/{f} src={current_dir}/{f}") assert self.run(host, "/var/ossec/process_submissions_today.sh forget") assert self.run(host, "postsuper -d ALL") assert self.run(host, f"cat /tmp/{f} | mail -s 'abc' root@localhost") @@ -96,12 +96,10 @@ def trigger(who, payload): assert self.run(host, f"! mailq | grep -q {who}@ossec.test") assert self.run( host, - """ + f""" ( echo 'Subject: TEST' ; echo ; echo -e '{payload}' ) | \ /var/ossec/send_encrypted_alarm.sh {who} - """.format( - who=who, payload=payload - ), + """, ) assert self.wait_for_command(host, f"mailq | grep -q {who}@ossec.test") @@ -116,14 +114,12 @@ def trigger(who, payload): trigger(who, payload) assert self.run( host, - """ + f""" job=$(mailq | sed -n -e '2p' | cut -f1 -d ' ') postcat -q $job | tee /dev/stderr | \ gpg --homedir /var/ossec/.gnupg --decrypt 2>&1 | \ grep -q {expected} - """.format( - expected=expected - ), + """, ) # # failure to encrypt must trigger an emergency mail to ossec contact diff --git a/pyproject.toml b/pyproject.toml index 799c8aa95a..8db51d4599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,13 +2,10 @@ name = "securedrop" requires-python = ">=3.8" -[tool.black] -line-length = 100 -target-version = ["py38"] -# Don't use `extend`, we want the default behavior that honors .gitignore rules - [tool.ruff] line-length = 100 +extend-include = ["securedrop/scripts/*"] +[tool.ruff.lint] select = [ # pycodestyle errors "E", @@ -79,14 +76,13 @@ ignore = [ # nested if and with statements, too many violations for now "SIM102", "SIM117", ] -extend-include = ["securedrop/scripts/*"] -[tool.ruff.isort] +[tool.ruff.lint.isort] # ruff's isort is smart enough to know these are first party, but let's treat # them as third-party to match O.G. isort until we fix our package layout known-third-party = ["journalist_app", "management", "source_app", "tests"] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "**/test**.py" = [ # use of `assert` "S101", @@ -105,6 +101,8 @@ known-third-party = ["journalist_app", "management", "source_app", "tests"] "securedrop/pretty_bad_protocol/*.py" = [ # legacy code that still uses `assert` "S101", + # too much % formatting, not worth fixing for now + "UP031", ] [tool.mypy] diff --git a/redwood/build-wheel.py b/redwood/build-wheel.py index 666747dfad..1134bef63c 100644 --- a/redwood/build-wheel.py +++ b/redwood/build-wheel.py @@ -12,6 +12,7 @@ * Copy and rename that into the Python package structure * Build a Python wheel using setuptools """ + import argparse import os import shutil diff --git a/securedrop/alembic/versions/3da3fcab826a_delete_orphaned_submissions.py b/securedrop/alembic/versions/3da3fcab826a_delete_orphaned_submissions.py index 19887fa014..f81cc38be7 100644 --- a/securedrop/alembic/versions/3da3fcab826a_delete_orphaned_submissions.py +++ b/securedrop/alembic/versions/3da3fcab826a_delete_orphaned_submissions.py @@ -24,12 +24,12 @@ def raw_sql_grab_orphaned_objects(table_name: str) -> str: """Objects that have a source ID that doesn't exist in the sources table OR a NULL source ID should be deleted.""" - return ( # noqa: S608 - "SELECT id, filename, source_id FROM {table} " + return ( + f"SELECT id, filename, source_id FROM {table_name} " # noqa: S608 "WHERE source_id NOT IN (SELECT id FROM sources) " - "UNION SELECT id, filename, source_id FROM {table} " + f"UNION SELECT id, filename, source_id FROM {table_name} " "WHERE source_id IS NULL" - ).format(table=table_name) + ) def upgrade() -> None: diff --git a/securedrop/debian/ossec-common/var/ossec/checksdconfig.py b/securedrop/debian/ossec-common/var/ossec/checksdconfig.py index ecfb085907..fa0b9c06ed 100755 --- a/securedrop/debian/ossec-common/var/ossec/checksdconfig.py +++ b/securedrop/debian/ossec-common/var/ossec/checksdconfig.py @@ -32,7 +32,7 @@ def list_iptables_rules() -> dict: - result = subprocess.run(["iptables", "-S"], capture_output=True) + result = subprocess.run(["iptables", "-S"], capture_output=True, check=False) rules = result.stdout.decode("utf-8").splitlines() policies = [r for r in rules if r.startswith("-P")] input_rules = [r for r in rules if r.startswith("-A INPUT")] diff --git a/securedrop/i18n.py b/securedrop/i18n.py index 596ca74076..0b281b6e92 100644 --- a/securedrop/i18n.py +++ b/securedrop/i18n.py @@ -114,9 +114,8 @@ def configure_babel(config: SecureDropConfig, app: Flask) -> Babel: # verify that Babel is only using the translations we told it about if list(babel.translation_directories) != [translations_directory]: raise ValueError( - "Babel translation directories ({}) do not match SecureDrop configuration ({})".format( - babel.translation_directories, [translations_directory] - ) + f"Babel translation directories ({babel.translation_directories}) do not match " + f"SecureDrop configuration ({[translations_directory]})" ) # register the function used to determine the locale of a request diff --git a/securedrop/journalist_app/admin.py b/securedrop/journalist_app/admin.py index b823fb0cda..606ed43d58 100644 --- a/securedrop/journalist_app/admin.py +++ b/securedrop/journalist_app/admin.py @@ -216,7 +216,7 @@ def add_user() -> Union[str, werkzeug.Response]: ), "error", ) - current_app.logger.error("Adding user " "'{}' failed: {}".format(username, e)) + current_app.logger.error("Adding user " f"'{username}' failed: {e}") if form_valid: if new_user.is_totp: @@ -423,9 +423,8 @@ def delete_user(user_id: int) -> werkzeug.Response: abort(403) elif not user: current_app.logger.error( - "Admin {} tried to delete nonexistent user with pk={}".format( - session.get_user().username, user_id - ) + f"Admin {session.get_user().username} tried to delete nonexistent user with " + f"pk={user_id}" ) abort(404) elif user.is_deleted_user(): @@ -456,9 +455,8 @@ def new_password(user_id: int) -> werkzeug.Response: if user.id == session.get_uid(): current_app.logger.error( - "Admin {} tried to change their password without validation.".format( - session.get_user().username - ) + f"Admin {session.get_user().username} tried to change their password without " + "validation." ) abort(403) diff --git a/securedrop/journalist_app/forms.py b/securedrop/journalist_app/forms.py index 7543166f00..e52974e012 100644 --- a/securedrop/journalist_app/forms.py +++ b/securedrop/journalist_app/forms.py @@ -27,7 +27,6 @@ def __init__( *args: Any, **kwargs: Any, ) -> None: - self.other_field_name = other_field_name if custom_message is not None: self.custom_message = custom_message diff --git a/securedrop/journalist_app/main.py b/securedrop/journalist_app/main.py index 5a95feb241..b6a319b0d1 100644 --- a/securedrop/journalist_app/main.py +++ b/securedrop/journalist_app/main.py @@ -138,9 +138,7 @@ def reply() -> werkzeug.Response: return redirect(url_for("col.col", filesystem_id=g.filesystem_id)) g.source.interaction_count += 1 - filename = "{}-{}-reply.gpg".format( - g.source.interaction_count, g.source.journalist_filename - ) + filename = f"{g.source.interaction_count}-{g.source.journalist_filename}-reply.gpg" EncryptionManager.get_default().encrypt_journalist_reply( for_source=g.source, reply_in=form.message.data, @@ -163,12 +161,10 @@ def reply() -> werkzeug.Response: # with responses to sources. It's possible the exception message # could contain information we don't want to write to disk. current_app.logger.error( - "Reply from '{}' (ID {}) failed: {}!".format( - session.get_user().username, session.get_uid(), exc.__class__ - ) + f"Reply from '{session.get_user().username}' (ID {session.get_uid()}) " + f"failed: {exc.__class__}!" ) else: - flash( Markup( "{} {}".format( diff --git a/securedrop/loaddata.py b/securedrop/loaddata.py index 241e00f435..ffd18091db 100755 --- a/securedrop/loaddata.py +++ b/securedrop/loaddata.py @@ -381,16 +381,10 @@ def add_sources(args: argparse.Namespace, journalists: Tuple[Journalist, ...]) - add_reply(source, journalist_who_replied, journalist_who_saw) print( - "Created source {}/{} (codename: '{}', journalist designation '{}', " - "files: {}, messages: {}, replies: {})".format( - i, - args.source_count, - codename, - source.journalist_designation, - args.files_per_source, - args.messages_per_source, - args.replies_per_source if i <= replied_sources_count else 0, - ) + f"Created source {i}/{args.source_count} (codename: '{codename}', " + f"journalist designation '{source.journalist_designation}', " + f"files: {args.files_per_source}, messages: {args.messages_per_source}, " + f"replies: {args.replies_per_source if i <= replied_sources_count else 0})" ) diff --git a/securedrop/manage.py b/securedrop/manage.py index fc69c9e37d..169e0921ac 100755 --- a/securedrop/manage.py +++ b/securedrop/manage.py @@ -19,18 +19,18 @@ sys.path.insert(0, "/var/www/securedrop") -import qrcode # noqa: E402 -from sqlalchemy.orm.exc import NoResultFound # noqa: E402 +import qrcode +from sqlalchemy.orm.exc import NoResultFound if not os.environ.get("SECUREDROP_ENV"): os.environ["SECUREDROP_ENV"] = "dev" -from db import db # noqa: E402 -from management import SecureDropConfig, app_context # noqa: E402 -from management.run import run # noqa: E402 -from management.sources import remove_pending_sources # noqa: E402 -from management.submissions import ( # noqa: E402 +from db import db +from management import SecureDropConfig, app_context +from management.run import run +from management.sources import remove_pending_sources +from management.submissions import ( add_check_db_disconnect_parser, add_check_fs_disconnect_parser, add_delete_db_disconnect_parser, @@ -39,7 +39,7 @@ add_list_fs_disconnect_parser, add_were_there_submissions_today, ) -from models import FirstOrLastNameError, InvalidUsernameException, Journalist # noqa: E402 +from models import FirstOrLastNameError, InvalidUsernameException, Journalist logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger(__name__) @@ -197,8 +197,8 @@ def _add_user(is_admin: bool = False, context: Optional[AppContext] = None) -> i if len(tmp_str) != 40: print( "The length of the secret is not correct. " - "Expected 40 characters, but received {}. " - "Try again.".format(len(tmp_str)) + f"Expected 40 characters, but received {len(tmp_str)}. " + "Try again." ) continue if otp_secret: @@ -239,7 +239,7 @@ def _add_user(is_admin: bool = False, context: Optional[AppContext] = None) -> i 'you will need to change the "Non-ASCII Font", which ' "is your profile's Text settings.\n\nCan't scan the " "barcode? Enter following shared secret manually:" - "\n{}\n".format(user.formatted_otp_secret) + f"\n{user.formatted_otp_secret}\n" ) return 0 @@ -249,11 +249,9 @@ def _get_username_to_delete() -> str: def _get_delete_confirmation(username: str) -> bool: - confirmation = obtain_input( - "Are you sure you want to delete user " '"{}" (y/n)?'.format(username) - ) + confirmation = obtain_input("Are you sure you want to delete user " f'"{username}" (y/n)?') if confirmation.lower() != "y": - print('Confirmation not received: user "{}" was NOT ' "deleted".format(username)) + print(f'Confirmation not received: user "{username}" was NOT ' "deleted") return False return True @@ -414,14 +412,13 @@ def set_clean_tmp_parser(subps: _SubParsersAction, name: str) -> None: default=default_days, type=int, help=( - "remove files not modified in a given number of DAYS " - "(default {} days)".format(default_days) + "remove files not modified in a given number of DAYS " f"(default {default_days} days)" ), ) parser.add_argument( "--directory", default=config.TEMP_DIR, - help=("remove old files from DIRECTORY " "(default {})".format(config.TEMP_DIR)), + help=("remove old files from DIRECTORY " f"(default {config.TEMP_DIR})"), ) parser.set_defaults(func=clean_tmp) diff --git a/securedrop/management/submissions.py b/securedrop/management/submissions.py index 5eee374d29..5217b6c435 100644 --- a/securedrop/management/submissions.py +++ b/securedrop/management/submissions.py @@ -160,9 +160,8 @@ def delete_disconnected_fs_submissions(args: argparse.Namespace) -> None: time_elapsed += file_elapsed rate = bytes_deleted / time_elapsed print( - "elapsed: {:.2f}s rate: {:.1f} MB/s overall rate: {:.1f} MB/s".format( - file_elapsed, filesize / 1048576 / file_elapsed, rate / 1048576 - ) + f"elapsed: {file_elapsed:.2f}s rate: {filesize / 1048576 / file_elapsed:.1f} " + f"MB/s overall rate: {rate / 1048576:.1f} MB/s" ) else: print(f"Not removing {f}.") diff --git a/securedrop/models.py b/securedrop/models.py index a68f9bed1a..badd5b7e59 100644 --- a/securedrop/models.py +++ b/securedrop/models.py @@ -35,12 +35,7 @@ def get_one_or_else( try: return query.one() except MultipleResultsFound as e: - logger.error( - "Found multiple while executing {} when one was expected: {}".format( - query, - e, - ) - ) + logger.error(f"Found multiple while executing {query} when one was expected: {e}") failure_method(500) except NoResultFound as e: logger.error(f"Found none when one was expected: {e}") @@ -87,7 +82,7 @@ def __init__( self.uuid = str(uuid.uuid4()) def __repr__(self) -> str: - return "" % (self.journalist_designation) + return f"" @property def journalist_filename(self) -> str: @@ -194,7 +189,7 @@ def __init__(self, source: Source, filename: str, storage: Storage) -> None: self.size = os.stat(storage.path(source.filesystem_id, filename)).st_size def __repr__(self) -> str: - return "" % (self.filename) + return f"" @property def is_file(self) -> bool: @@ -254,10 +249,7 @@ def seen(self) -> bool: If the submission has been downloaded or seen by any journalist, then the submission is considered seen. """ - if self.downloaded or self.seen_files.count() or self.seen_messages.count(): - return True - - return False + return bool(self.downloaded or self.seen_files.count() or self.seen_messages.count()) class Reply(db.Model): @@ -292,7 +284,7 @@ def __init__( self.size = os.stat(storage.path(source.filesystem_id, filename)).st_size def __repr__(self) -> str: - return "" % (self.filename) + return f"" def to_json(self) -> "Dict[str, Any]": seen_by = [r.journalist.uuid for r in SeenReply.query.filter(SeenReply.reply_id == self.id)] @@ -323,7 +315,7 @@ class SourceStar(db.Model): source_id = Column("source_id", Integer, ForeignKey("sources.id")) starred = Column("starred", Boolean, default=True) - def __eq__(self, other: "Any") -> bool: + def __eq__(self, other: object) -> bool: if isinstance(other, SourceStar): return ( self.source_id == other.source_id @@ -426,7 +418,6 @@ def __init__( is_admin: bool = False, otp_secret: "Optional[str]" = None, ) -> None: - self.check_username_acceptable(username) self.username = username if first_name: @@ -657,9 +648,8 @@ def throttle_login(cls, user: "Journalist") -> None: ) if len(attempts_within_period) > cls._MAX_LOGIN_ATTEMPTS_PER_PERIOD: raise LoginThrottledException( - "throttled ({} attempts in last {} seconds)".format( - len(attempts_within_period), cls._LOGIN_ATTEMPT_PERIOD - ) + f"throttled ({len(attempts_within_period)} attempts in last " + f"{cls._LOGIN_ATTEMPT_PERIOD} seconds)" ) @classmethod @@ -863,16 +853,11 @@ class InstanceConfig(db.Model): def __repr__(self) -> str: return ( - "".format( - self.version, - self.valid_until, - self.allow_document_uploads, - self.organization_name, - self.initial_message_min_len, - self.reject_message_with_codename, - ) + f"" ) def copy(self) -> "InstanceConfig": diff --git a/securedrop/passphrases.py b/securedrop/passphrases.py index 344d58716f..93282e88c3 100644 --- a/securedrop/passphrases.py +++ b/securedrop/passphrases.py @@ -18,7 +18,6 @@ class InvalidWordListError(Exception): class PassphraseGenerator: - PASSPHRASE_WORDS_COUNT = 7 # Enforce a reasonable maximum length for passphrases to avoid DoS @@ -47,12 +46,9 @@ def __init__( word_list_size = len(word_list) if word_list_size < self._WORD_LIST_MINIMUM_SIZE: raise InvalidWordListError( - "The word list for language '{}' only contains {} long-enough words;" - " minimum required is {} words.".format( - language, - word_list_size, - self._WORD_LIST_MINIMUM_SIZE, - ) + f"The word list for language '{language}' only contains {word_list_size}" + " long-enough words;" + f" minimum required is {self._WORD_LIST_MINIMUM_SIZE} words." ) # Ensure all words are ascii @@ -69,14 +65,11 @@ def __init__( longest_passphrase_length += self.PASSPHRASE_WORDS_COUNT # One space between each word if longest_passphrase_length >= self.MAX_PASSPHRASE_LENGTH: raise InvalidWordListError( - "Passphrases over the maximum length ({}) may be generated:" - " longest word in word list for language '{}' is '{}' and number of words per" - " passphrase is {}".format( - self.MAX_PASSPHRASE_LENGTH, - language, - longest_word, - self.PASSPHRASE_WORDS_COUNT, - ) + f"Passphrases over the maximum length ({self.MAX_PASSPHRASE_LENGTH}) " + "may be generated:" + f" longest word in word list for language '{language}' is '{longest_word}' " + "and number of words per" + f" passphrase is {self.PASSPHRASE_WORDS_COUNT}" ) # Ensure that passphrases shorter than what's supported can't be generated @@ -85,14 +78,11 @@ def __init__( shortest_passphrase_length += self.PASSPHRASE_WORDS_COUNT if shortest_passphrase_length <= self.MIN_PASSPHRASE_LENGTH: raise InvalidWordListError( - "Passphrases under the minimum length ({}) may be generated:" - " shortest word in word list for language '{}' is '{}' and number of words per" - " passphrase is {}".format( - self.MIN_PASSPHRASE_LENGTH, - language, - shortest_word, - self.PASSPHRASE_WORDS_COUNT, - ) + f"Passphrases under the minimum length ({self.MIN_PASSPHRASE_LENGTH}) " + "may be generated:" + f" shortest word in word list for language '{language}' is '{shortest_word}' " + "and number of words per" + f" passphrase is {self.PASSPHRASE_WORDS_COUNT}" ) @classmethod diff --git a/securedrop/pretty_bad_protocol/_meta.py b/securedrop/pretty_bad_protocol/_meta.py index 368f37e854..55e68a18fa 100644 --- a/securedrop/pretty_bad_protocol/_meta.py +++ b/securedrop/pretty_bad_protocol/_meta.py @@ -20,7 +20,6 @@ attribute creation and handling. """ - import atexit import codecs import encodings @@ -718,7 +717,7 @@ def _set_verbose(self, verbose): # type: ignore[no-untyped-def] self.verbose = verbose - def _collect_output(self, process, result, writer=None, stdin=None): # type: ignore[no-untyped-def] # noqa + def _collect_output(self, process, result, writer=None, stdin=None): # type: ignore[no-untyped-def] """Drain the subprocesses output streams, writing the collected output to the result. If a writer thread (writing to the subprocess) is given, make sure it's joined before returning. If a stdin stream is given, @@ -749,7 +748,7 @@ def _collect_output(self, process, result, writer=None, stdin=None): # type: ig stderr.close() stdout.close() - def _handle_io(self, args, file, result, passphrase=False, binary=False): # type: ignore[no-untyped-def] # noqa + def _handle_io(self, args, file, result, passphrase=False, binary=False): # type: ignore[no-untyped-def] """Handle a call to GPG - pass input data, collect output data.""" p = self._open_subprocess(args, passphrase) if not binary: @@ -1042,7 +1041,7 @@ def _encrypt( # type: ignore[no-untyped-def] return result - def _add_recipient_string(self, args, hidden_recipients, recipient): # type: ignore[no-untyped-def] # noqa + def _add_recipient_string(self, args, hidden_recipients, recipient): # type: ignore[no-untyped-def] if isinstance(hidden_recipients, (list, tuple)): if [s for s in hidden_recipients if recipient in str(s)]: args.append("--hidden-recipient %s" % recipient) diff --git a/securedrop/pretty_bad_protocol/_parsers.py b/securedrop/pretty_bad_protocol/_parsers.py index e5ff49b921..6e26e4d1fc 100644 --- a/securedrop/pretty_bad_protocol/_parsers.py +++ b/securedrop/pretty_bad_protocol/_parsers.py @@ -20,7 +20,6 @@ options. """ - import re from collections import OrderedDict @@ -231,9 +230,7 @@ def _is_hex(string): # type: ignore[no-untyped-def] :param str string: The string to check. """ - if HEXADECIMAL.match(string): - return True - return False + return bool(HEXADECIMAL.match(string)) def _sanitise(*args): # type: ignore[no-untyped-def] @@ -1005,9 +1002,7 @@ def __init__(self, gpg): # type: ignore[no-untyped-def] self.secring = None def __nonzero__(self): # type: ignore[no-untyped-def] - if self.fingerprint: - return True - return False + return bool(self.fingerprint) __bool__ = __nonzero__ @@ -1300,7 +1295,7 @@ def __init__(self, gpg): # type: ignore[no-untyped-def] #: Counts of all the status message results, :data:`_fields` which #: have appeared. - self.counts = OrderedDict(zip(self._fields, [int(0) for x in range(len(self._fields))])) + self.counts = OrderedDict(zip(self._fields, [0 for x in range(len(self._fields))])) #: A list of strings containing the fingerprints of the GnuPG keyIDs #: imported. @@ -1318,9 +1313,7 @@ def __nonzero__(self): # type: ignore[no-untyped-def] """ if self.counts["not_imported"] > 0: return False - if len(self.fingerprints) == 0: - return False - return True + return len(self.fingerprints) != 0 __bool__ = __nonzero__ @@ -1408,7 +1401,7 @@ def __init__(self, gpg): # type: ignore[no-untyped-def] #: Counts of all the status message results, :data:`_fields` which #: have appeared. - self.counts = OrderedDict(zip(self._fields, [int(0) for x in range(len(self._fields))])) + self.counts = OrderedDict(zip(self._fields, [0 for x in range(len(self._fields))])) #: A list of strings containing the fingerprints of the GnuPG keyIDs #: exported. @@ -1422,9 +1415,7 @@ def __nonzero__(self): # type: ignore[no-untyped-def] """ if self.counts["not_imported"] > 0: return False - if len(self.fingerprints) == 0: - return False - return True + return len(self.fingerprints) != 0 __bool__ = __nonzero__ @@ -1785,9 +1776,7 @@ def __init__(self, gpg): # type: ignore[no-untyped-def] self.data_filename = None def __nonzero__(self): # type: ignore[no-untyped-def] - if self.ok: - return True - return False + return self.ok __bool__ = __nonzero__ diff --git a/securedrop/pretty_bad_protocol/_trust.py b/securedrop/pretty_bad_protocol/_trust.py index 03eef6a232..0ad4bd672a 100644 --- a/securedrop/pretty_bad_protocol/_trust.py +++ b/securedrop/pretty_bad_protocol/_trust.py @@ -22,7 +22,6 @@ a suitable subclass as their first argument. """ - import os from . import _util diff --git a/securedrop/pretty_bad_protocol/_util.py b/securedrop/pretty_bad_protocol/_util.py index d90b83082a..72d81cb656 100644 --- a/securedrop/pretty_bad_protocol/_util.py +++ b/securedrop/pretty_bad_protocol/_util.py @@ -399,7 +399,7 @@ def _threaded_copy_data(instream, outstream): # type: ignore[no-untyped-def] return copy_thread -def _which(executable, flags=os.X_OK, abspath_only=False, disallow_symlinks=False): # type: ignore[no-untyped-def] # noqa +def _which(executable, flags=os.X_OK, abspath_only=False, disallow_symlinks=False): # type: ignore[no-untyped-def] """Borrowed from Twisted's :mod:twisted.python.proutils . Search PATH for executable files with the given name. @@ -468,7 +468,7 @@ def _write_passphrase(stream, passphrase, encoding): # type: ignore[no-untyped- class InheritableProperty: """Based on the emulation of PyProperty_Type() in Objects/descrobject.c""" - def __init__(self, fget=None, fset=None, fdel=None, doc=None): # type: ignore[no-untyped-def] # noqa + def __init__(self, fget=None, fset=None, fdel=None, doc=None): # type: ignore[no-untyped-def] self.fget = fget self.fset = fset self.fdel = fdel diff --git a/securedrop/pretty_bad_protocol/gnupg.py b/securedrop/pretty_bad_protocol/gnupg.py index c16d2fd413..50ed25cea8 100644 --- a/securedrop/pretty_bad_protocol/gnupg.py +++ b/securedrop/pretty_bad_protocol/gnupg.py @@ -130,32 +130,20 @@ def __init__( # type: ignore[no-untyped-def] log.info( textwrap.dedent( - """ + f""" Initialised settings: - binary: {} - binary version: {} - homedir: {} - ignore_homedir_permissions: {} - keyring: {} - secring: {} - default_preference_list: {} - keyserver: {} - options: {} - verbose: {} - use_agent: {} - """.format( - self.binary, - self.binary_version, - self.homedir, - self.ignore_homedir_permissions, - self.keyring, - self.secring, - self.default_preference_list, - self.keyserver, - self.options, - str(self.verbose), - str(self.use_agent), - ) + binary: {self.binary} + binary version: {self.binary_version} + homedir: {self.homedir} + ignore_homedir_permissions: {self.ignore_homedir_permissions} + keyring: {self.keyring} + secring: {self.secring} + default_preference_list: {self.default_preference_list} + keyserver: {self.keyserver} + options: {self.options} + verbose: {str(self.verbose)} + use_agent: {str(self.use_agent)} + """ ) ) @@ -374,7 +362,7 @@ def recv_keys(self, *keyids, **kwargs): # type: ignore[no-untyped-def] else: log.error("No keyids requested for --recv-keys!") - def delete_keys(self, fingerprints, secret=False, subkeys=False): # type: ignore[no-untyped-def] # noqa + def delete_keys(self, fingerprints, secret=False, subkeys=False): # type: ignore[no-untyped-def] """Delete a key, or list of keys, from the current keyring. The keys must be referred to by their full fingerprints for GnuPG to @@ -410,7 +398,7 @@ def delete_keys(self, fingerprints, secret=False, subkeys=False): # type: ignor self._collect_output(p, result, stdin=p.stdin) return result - def export_keys(self, keyids, secret=False, subkeys=False, passphrase=None): # type: ignore[no-untyped-def] # noqa + def export_keys(self, keyids, secret=False, subkeys=False, passphrase=None): # type: ignore[no-untyped-def] """Export the indicated ``keyids``. :param str keyids: A keyid or fingerprint in any format that GnuPG will @@ -492,7 +480,7 @@ def list_packets(self, raw_data): # type: ignore[no-untyped-def] self._handle_io(args, _make_binary_stream(raw_data, self._encoding), result) return result - def sign_key(self, keyid, default_key=None, passphrase=None): # type: ignore[no-untyped-def] # noqa + def sign_key(self, keyid, default_key=None, passphrase=None): # type: ignore[no-untyped-def] """sign (an imported) public key - keyid, with default secret key >>> import gnupg @@ -553,7 +541,6 @@ def check_sigs(self, *keyids): # type: ignore[no-untyped-def] return self._process_keys(keyids, check_sig=True) def _process_keys(self, keyids, check_sig=False): # type: ignore[no-untyped-def] - if len(keyids) > self._batch_limit: raise ValueError( "List signatures is limited to %d keyids simultaneously" % self._batch_limit @@ -589,7 +576,7 @@ def _parse_keys(self, result): # type: ignore[no-untyped-def] if keyword in valid_keywords: getattr(result, keyword)(L) - def expire(self, keyid, expiration_time="1y", passphrase=None, expire_subkeys=True): # type: ignore[no-untyped-def] # noqa + def expire(self, keyid, expiration_time="1y", passphrase=None, expire_subkeys=True): # type: ignore[no-untyped-def] """Changes GnuPG key expiration by passing in new time period (from now) through subprocess's stdin @@ -680,7 +667,7 @@ def gen_key(self, input): # type: ignore[no-untyped-def] return key - def gen_key_input(self, separate_keyring=False, save_batchfile=False, testing=False, **kwargs): # type: ignore[no-untyped-def] # noqa + def gen_key_input(self, separate_keyring=False, save_batchfile=False, testing=False, **kwargs): # type: ignore[no-untyped-def] """Generate a batch file for input to :meth:`~gnupg.GPG.gen_key`. The GnuPG batch file key generation feature allows unattended key @@ -959,13 +946,11 @@ def gen_key_input(self, separate_keyring=False, save_batchfile=False, testing=Fa else: docs = "" # docstring=None if run with "python -OO" links = "\n".join(x.strip() for x in docs.splitlines()[-2:]) - explain = """ -This directory was created by python-gnupg, on {}, and + explain = f""" +This directory was created by python-gnupg, on {_util.now()}, and it contains saved batch files, which can be given to GnuPG to automatically generate keys. Please see -{}""".format( - _util.now(), links - ) # sometimes python is awesome. +{links}""" # sometimes python is awesome. with open(readme, "a+") as fh: [fh.write(line) for line in explain] @@ -1082,7 +1067,7 @@ def decrypt(self, message, **kwargs): # type: ignore[no-untyped-def] stream.close() return result - def decrypt_file(self, filename, always_trust=False, passphrase=None, output=None): # type: ignore[no-untyped-def] # noqa + def decrypt_file(self, filename, always_trust=False, passphrase=None, output=None): # type: ignore[no-untyped-def] """Decrypt the contents of a file-like object ``filename`` . :param str filename: A file-like object to decrypt. diff --git a/securedrop/requirements/python3/develop-requirements.in b/securedrop/requirements/python3/develop-requirements.in index 9bba74d2dc..17ac53f2b8 100644 --- a/securedrop/requirements/python3/develop-requirements.in +++ b/securedrop/requirements/python3/develop-requirements.in @@ -3,7 +3,6 @@ ansible-lint>=4.2.0 ansible==6.7.0 ansible-compat<3.0.0 argon2_cffi>=20.1.0 -black>=24.3.0 cffi>=1.16.0 cryptography>=41.0.5 # >= OpenSSL 3.1.4 for CVE-2023-5363 diffoscope diff --git a/securedrop/requirements/python3/develop-requirements.txt b/securedrop/requirements/python3/develop-requirements.txt index a33ef9ef02..0876ad798e 100644 --- a/securedrop/requirements/python3/develop-requirements.txt +++ b/securedrop/requirements/python3/develop-requirements.txt @@ -108,30 +108,6 @@ binaryornot==0.4.4 \ --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 # via cookiecutter -black==24.3.0 \ - --hash=sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f \ - --hash=sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93 \ - --hash=sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11 \ - --hash=sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0 \ - --hash=sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9 \ - --hash=sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5 \ - --hash=sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213 \ - --hash=sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d \ - --hash=sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7 \ - --hash=sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837 \ - --hash=sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f \ - --hash=sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395 \ - --hash=sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995 \ - --hash=sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f \ - --hash=sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597 \ - --hash=sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959 \ - --hash=sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5 \ - --hash=sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb \ - --hash=sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4 \ - --hash=sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7 \ - --hash=sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd \ - --hash=sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7 - # via -r requirements/python3/develop-requirements.in boltons==21.0.0 \ --hash=sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13 \ --hash=sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b @@ -236,7 +212,6 @@ click==8.1.2 \ --hash=sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e \ --hash=sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72 # via - # black # click-completion # click-help-colors # click-option-group @@ -586,10 +561,6 @@ molecule==3.0.2.1 \ # via # -r requirements/python3/develop-requirements.in # molecule-vagrant -mypy-extensions==0.4.3 \ - --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ - --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 - # via black netaddr==0.7.19 \ --hash=sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd \ --hash=sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca @@ -604,7 +575,6 @@ packaging==24.0 \ # via # ansible-compat # ansible-core - # black # dparse # pytest # safety @@ -616,9 +586,7 @@ paramiko==2.12.0 \ pathspec==0.9.0 \ --hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \ --hash=sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1 - # via - # black - # yamllint + # via yamllint peewee==3.17.1 \ --hash=sha256:e009ac4227c4fdc0058a56e822ad5987684f0a1fbb20fed577200785102581c3 # via semgrep @@ -641,9 +609,7 @@ pkgutil-resolve-name==1.3.10 \ platformdirs==4.2.0 \ --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 - # via - # black - # virtualenv + # via virtualenv pluggy==0.13.1 \ --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d @@ -972,24 +938,25 @@ ruamel.yaml==0.17.32 \ # ansible-lint # safety # semgrep -ruff==0.0.277 \ - --hash=sha256:14a7b2f00f149c5a295f188a643ac25226ff8a4d08f7a62b1d4b0a1dc9f9b85c \ - --hash=sha256:2d4444c60f2e705c14cd802b55cd2b561d25bf4311702c463a002392d3116b22 \ - --hash=sha256:2dab13cdedbf3af6d4427c07f47143746b6b95d9e4a254ac369a0edb9280a0d2 \ - --hash=sha256:323b674c98078be9aaded5b8b51c0d9c424486566fb6ec18439b496ce79e5998 \ - --hash=sha256:3250b24333ef419b7a232080d9724ccc4d2da1dbbe4ce85c4caa2290d83200f8 \ - --hash=sha256:3a43fbe026ca1a2a8c45aa0d600a0116bec4dfa6f8bf0c3b871ecda51ef2b5dd \ - --hash=sha256:3e60605e07482183ba1c1b7237eca827bd6cbd3535fe8a4ede28cbe2a323cb97 \ - --hash=sha256:468bfb0a7567443cec3d03cf408d6f562b52f30c3c29df19927f1e0e13a40cd7 \ - --hash=sha256:479864a3ccd8a6a20a37a6e7577bdc2406868ee80b1e65605478ad3b8eb2ba0b \ - --hash=sha256:6fe81732f788894a00f6ade1fe69e996cc9e485b7c35b0f53fb00284397284b2 \ - --hash=sha256:734165ea8feb81b0d53e3bf523adc2413fdb76f1264cde99555161dd5a725522 \ - --hash=sha256:74e4b206cb24f2e98a615f87dbe0bde18105217cbcc8eb785bb05a644855ba50 \ - --hash=sha256:7baa97c3d7186e5ed4d5d4f6834d759a27e56cf7d5874b98c507335f0ad5aadb \ - --hash=sha256:88d0f2afb2e0c26ac1120e7061ddda2a566196ec4007bd66d558f13b374b9efc \ - --hash=sha256:a9879f59f763cc5628aa01c31ad256a0f4dc61a29355c7315b83c2a5aac932b5 \ - --hash=sha256:f32ec416c24542ca2f9cc8c8b65b84560530d338aaf247a4a78e74b99cd476b4 \ - --hash=sha256:f612e0a14b3d145d90eb6ead990064e22f6f27281d847237560b4e10bf2251f3 +ruff==0.6.5 \ + --hash=sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae \ + --hash=sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810 \ + --hash=sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200 \ + --hash=sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680 \ + --hash=sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9 \ + --hash=sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc \ + --hash=sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb \ + --hash=sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0 \ + --hash=sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276 \ + --hash=sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19 \ + --hash=sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972 \ + --hash=sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748 \ + --hash=sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178 \ + --hash=sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253 \ + --hash=sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69 \ + --hash=sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c \ + --hash=sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f \ + --hash=sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5 # via -r requirements/python3/develop-requirements.in safety==2.3.1 \ --hash=sha256:6e6fcb7d4e8321098cf289f59b65051cafd3467f089c6e57c9f894ae32c23b71 \ @@ -1069,7 +1036,6 @@ tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via - # black # pytest # semgrep translate-toolkit==3.10.1 \ @@ -1084,7 +1050,6 @@ typing-extensions==4.2.0 \ --hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \ --hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376 # via - # black # rich # semgrep urllib3==2.1.0 \ diff --git a/securedrop/scripts/rqrequeue b/securedrop/scripts/rqrequeue index c6ebc33d56..92a8838f04 100755 --- a/securedrop/scripts/rqrequeue +++ b/securedrop/scripts/rqrequeue @@ -8,8 +8,8 @@ import time sys.path.insert(0, "/var/www/securedrop") -from sdconfig import SecureDropConfig # noqa: E402 -from worker import requeue_interrupted_jobs # noqa: E402 +from sdconfig import SecureDropConfig +from worker import requeue_interrupted_jobs def parse_args(): diff --git a/securedrop/scripts/shredder b/securedrop/scripts/shredder index ebb265cafc..7ebf322a04 100755 --- a/securedrop/scripts/shredder +++ b/securedrop/scripts/shredder @@ -11,9 +11,9 @@ import time sys.path.insert(0, "/var/www/securedrop") -import journalist_app # noqa: E402 -from sdconfig import SecureDropConfig # noqa: E402 -from store import Storage # noqa: E402 +import journalist_app +from sdconfig import SecureDropConfig +from store import Storage def parse_args(): @@ -22,7 +22,10 @@ def parse_args(): description="Utility for clearing deleted content in the SecureDrop store.", ) parser.add_argument( - "-i", "--interval", type=int, help="Keep running every 'interval' seconds.", + "-i", + "--interval", + type=int, + help="Keep running every 'interval' seconds.", ) return parser.parse_args() @@ -37,9 +40,7 @@ def clear_shredder(): def main(): args = parse_args() - logging.basicConfig( - format="%(asctime)s %(levelname)s %(message)s", level=logging.INFO - ) + logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s", level=logging.INFO) if args.interval: logging.info(f"Clearing shredder every {args.interval} seconds.") while 1: diff --git a/securedrop/scripts/source_deleter b/securedrop/scripts/source_deleter index ecba6acaa0..ec9c5b1595 100755 --- a/securedrop/scripts/source_deleter +++ b/securedrop/scripts/source_deleter @@ -11,8 +11,8 @@ import time sys.path.insert(0, "/var/www/securedrop") -import journalist_app # noqa: E402 -from sdconfig import SecureDropConfig # noqa: E402 +import journalist_app +from sdconfig import SecureDropConfig def parse_args(): @@ -21,7 +21,10 @@ def parse_args(): description="Utility for asynchronous deletion of SecureDrop sources and their data.", ) parser.add_argument( - "-i", "--interval", type=int, help="Keep running every 'interval' seconds.", + "-i", + "--interval", + type=int, + help="Keep running every 'interval' seconds.", ) return parser.parse_args() @@ -36,9 +39,7 @@ def purge_deleted_sources(): def main(): args = parse_args() - logging.basicConfig( - format="%(asctime)s %(levelname)s %(message)s", level=logging.INFO - ) + logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s", level=logging.INFO) if args.interval: logging.info(f"Purging deleted sources every {args.interval} seconds.") while 1: diff --git a/securedrop/source_app/forms.py b/securedrop/source_app/forms.py index 1de4f9bf0e..2d6728e221 100644 --- a/securedrop/source_app/forms.py +++ b/securedrop/source_app/forms.py @@ -10,7 +10,6 @@ class LoginForm(FlaskForm): - codename = PasswordField( "codename", validators=[ diff --git a/securedrop/source_app/main.py b/securedrop/source_app/main.py index 55be14ef38..605b4477c4 100644 --- a/securedrop/source_app/main.py +++ b/securedrop/source_app/main.py @@ -169,9 +169,9 @@ def lookup(logged_in_source: SourceUser) -> str: ) reply.decrypted = decrypted_reply except UnicodeDecodeError: - current_app.logger.error("Could not decode reply %s" % reply.filename) + current_app.logger.error(f"Could not decode reply {reply.filename}") except FileNotFoundError: - current_app.logger.error("Reply file missing: %s" % reply.filename) + current_app.logger.error(f"Reply file missing: {reply.filename}") except (GpgDecryptError, RedwoodError) as e: current_app.logger.error(f"Could not decrypt reply {reply.filename}: {str(e)}") else: @@ -253,9 +253,8 @@ def submit(logged_in_source: SourceUser) -> werkzeug.Response: if not os.path.exists(Storage.get_default().path(logged_in_source.filesystem_id)): current_app.logger.debug( - "Store directory not found for source '{}', creating one.".format( - logged_in_source_in_db.journalist_designation - ) + f"Store directory not found for source " + f"'{logged_in_source_in_db.journalist_designation}', creating one." ) os.mkdir(Storage.get_default().path(logged_in_source.filesystem_id)) diff --git a/securedrop/store.py b/securedrop/store.py index a9b4987c3c..7cc4da8a03 100644 --- a/securedrop/store.py +++ b/securedrop/store.py @@ -157,9 +157,8 @@ def path(self, filesystem_id: str, filename: str = "") -> str: absolute = os.path.realpath(joined) if not self.verify(absolute): raise PathException( - """Could not resolve ("{}", "{}") to a path within the store.""".format( - filesystem_id, filename - ) + f'Could not resolve ("{filesystem_id}", "{filename}") to a path within ' + "the store." ) return absolute @@ -303,7 +302,6 @@ def save_file_submission( filename: Optional[str], stream: BinaryIO, ) -> str: - if filename is not None: sanitized_filename = secure_filename(filename) else: diff --git a/securedrop/tests/conftest.py b/securedrop/tests/conftest.py index 4822ad8e9a..2c5cf42a95 100644 --- a/securedrop/tests/conftest.py +++ b/securedrop/tests/conftest.py @@ -99,7 +99,7 @@ def setup_journalist_key_and_gpg_folder() -> Generator[Tuple[str, Path], None, N shutil.rmtree(tmp_gpg_dir, ignore_errors=True) -@pytest.fixture() +@pytest.fixture def config( setup_journalist_key_and_gpg_folder: Tuple[str, Path], setup_rqworker: Tuple[str, str], @@ -119,7 +119,7 @@ def config( yield config -@pytest.fixture() +@pytest.fixture def alembic_config(config: SecureDropConfig) -> Generator[Path, None, None]: base_dir = path.join(path.dirname(__file__), "..") migrations_dir = path.join(base_dir, "alembic") @@ -148,12 +148,12 @@ def alembic_config(config: SecureDropConfig) -> Generator[Path, None, None]: os.environ["SECUREDROP_ENV"] = previous_env_value -@pytest.fixture() +@pytest.fixture def app_storage(config: SecureDropConfig) -> "Storage": return Storage(str(config.STORE_DIR), str(config.TEMP_DIR)) -@pytest.fixture() +@pytest.fixture def source_app(config: SecureDropConfig, app_storage: Storage) -> Generator[Flask, None, None]: with mock.patch("store.Storage.get_default") as mock_storage_global: mock_storage_global.return_value = app_storage @@ -168,7 +168,7 @@ def source_app(config: SecureDropConfig, app_storage: Storage) -> Generator[Flas db.drop_all() -@pytest.fixture() +@pytest.fixture def journalist_app(config: SecureDropConfig, app_storage: Storage) -> Generator[Flask, None, None]: with mock.patch("store.Storage.get_default") as mock_storage_global: mock_storage_global.return_value = app_storage @@ -183,7 +183,7 @@ def journalist_app(config: SecureDropConfig, app_storage: Storage) -> Generator[ db.drop_all() -@pytest.fixture() +@pytest.fixture def test_journo(journalist_app: Flask) -> Dict[str, Any]: with journalist_app.app_context(): user, password = utils.db_helper.init_journalist(is_admin=False) @@ -201,7 +201,7 @@ def test_journo(journalist_app: Flask) -> Dict[str, Any]: } -@pytest.fixture() +@pytest.fixture def test_admin(journalist_app: Flask) -> Dict[str, Any]: with journalist_app.app_context(): user, password = utils.db_helper.init_journalist(is_admin=True) @@ -216,7 +216,7 @@ def test_admin(journalist_app: Flask) -> Dict[str, Any]: } -@pytest.fixture() +@pytest.fixture def test_source(journalist_app: Flask, app_storage: Storage) -> Dict[str, Any]: with journalist_app.app_context(): passphrase = PassphraseGenerator.get_default().generate_passphrase() @@ -237,7 +237,7 @@ def test_source(journalist_app: Flask, app_storage: Storage) -> Dict[str, Any]: } -@pytest.fixture() +@pytest.fixture def test_submissions(journalist_app: Flask, app_storage: Storage) -> Dict[str, Any]: with journalist_app.app_context(): source, codename = utils.db_helper.init_source(app_storage) @@ -251,7 +251,7 @@ def test_submissions(journalist_app: Flask, app_storage: Storage) -> Dict[str, A } -@pytest.fixture() +@pytest.fixture def test_files(journalist_app, test_journo, app_storage): with journalist_app.app_context(): source, codename = utils.db_helper.init_source(app_storage) @@ -267,7 +267,7 @@ def test_files(journalist_app, test_journo, app_storage): } -@pytest.fixture() +@pytest.fixture def test_files_deleted_journalist(journalist_app, test_journo, app_storage): with journalist_app.app_context(): source, codename = utils.db_helper.init_source(app_storage) @@ -286,7 +286,7 @@ def test_files_deleted_journalist(journalist_app, test_journo, app_storage): } -@pytest.fixture() +@pytest.fixture def journalist_api_token(journalist_app, test_journo): with journalist_app.test_client() as app: valid_token = TOTP(test_journo["otp_secret"]).now() diff --git a/securedrop/tests/functional/app_navigators/journalist_app_nav.py b/securedrop/tests/functional/app_navigators/journalist_app_nav.py index 1280a0000a..dcf908ad1d 100644 --- a/securedrop/tests/functional/app_navigators/journalist_app_nav.py +++ b/securedrop/tests/functional/app_navigators/journalist_app_nav.py @@ -111,7 +111,7 @@ def journalist_downloads_first_message(self) -> str: # using requests, but we need to pass the cookies for logged in user # for Flask to allow this. def cookie_string_from_selenium_cookies( - cookies: Iterable[Dict[str, str]] + cookies: Iterable[Dict[str, str]], ) -> Dict[str, str]: result = {} for cookie in cookies: diff --git a/securedrop/tests/functional/conftest.py b/securedrop/tests/functional/conftest.py index 2feead5a08..d40a000e61 100644 --- a/securedrop/tests/functional/conftest.py +++ b/securedrop/tests/functional/conftest.py @@ -21,14 +21,14 @@ # Function-scoped so that tests can be run in parallel if needed -@pytest.fixture() +@pytest.fixture def firefox_web_driver() -> WebDriver: # type: ignore with get_web_driver(web_driver_type=WebDriverTypeEnum.FIREFOX) as web_driver: yield web_driver # Function-scoped so that tests can be run in parallel if needed -@pytest.fixture() +@pytest.fixture def tor_browser_web_driver() -> WebDriver: # type: ignore with get_web_driver(web_driver_type=WebDriverTypeEnum.TOR_BROWSER) as web_driver: yield web_driver @@ -216,7 +216,7 @@ def sd_servers( yield sd_servers_result -@pytest.fixture() +@pytest.fixture def sd_servers_with_clean_state( setup_journalist_key_and_gpg_folder: Tuple[str, Path], setup_rqworker: Tuple[str, Path], @@ -239,7 +239,7 @@ def sd_servers_with_clean_state( yield sd_servers_result -@pytest.fixture() +@pytest.fixture def sd_servers_with_submitted_file( setup_journalist_key_and_gpg_folder: Tuple[str, Path], setup_rqworker: Tuple[str, Path], diff --git a/securedrop/tests/functional/pageslayout/test_journalist.py b/securedrop/tests/functional/pageslayout/test_journalist.py index 34d4a17029..72a847f198 100644 --- a/securedrop/tests/functional/pageslayout/test_journalist.py +++ b/securedrop/tests/functional/pageslayout/test_journalist.py @@ -21,7 +21,7 @@ @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestJournalistLayout: def test_login_index_and_edit(self, locale, sd_servers, firefox_web_driver): # Given an SD server diff --git a/securedrop/tests/functional/pageslayout/test_journalist_account.py b/securedrop/tests/functional/pageslayout/test_journalist_account.py index 376bade7d1..887ff0ba38 100644 --- a/securedrop/tests/functional/pageslayout/test_journalist_account.py +++ b/securedrop/tests/functional/pageslayout/test_journalist_account.py @@ -25,7 +25,7 @@ @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestJournalistLayoutAccount: def test_account_edit_and_set_hotp_secret( self, locale, sd_servers_with_clean_state, firefox_web_driver diff --git a/securedrop/tests/functional/pageslayout/test_journalist_admin.py b/securedrop/tests/functional/pageslayout/test_journalist_admin.py index 90a6f85635..40c44f4dd9 100644 --- a/securedrop/tests/functional/pageslayout/test_journalist_admin.py +++ b/securedrop/tests/functional/pageslayout/test_journalist_admin.py @@ -29,7 +29,7 @@ @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestAdminLayoutAddAndEditUser: def test_admin_adds_user_hotp_and_edits_hotp( self, locale, sd_servers_with_clean_state, firefox_web_driver @@ -224,7 +224,7 @@ def _retry_2fa_pop_ups( @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestAdminLayoutEditConfig: def test_admin_changes_logo(self, locale, sd_servers_with_clean_state, firefox_web_driver): # Given an SD server diff --git a/securedrop/tests/functional/pageslayout/test_journalist_col.py b/securedrop/tests/functional/pageslayout/test_journalist_col.py index dbf692e35d..fd39240ff8 100644 --- a/securedrop/tests/functional/pageslayout/test_journalist_col.py +++ b/securedrop/tests/functional/pageslayout/test_journalist_col.py @@ -37,7 +37,7 @@ def _create_source_and_submission_and_delete_source_key(config_in_use: SecureDro EncryptionManager.get_default().delete_source_key_pair(source_user.filesystem_id) -@pytest.fixture() +@pytest.fixture def sd_servers_with_deleted_source_key( setup_journalist_key_and_gpg_folder: Tuple[str, Path], setup_rqworker: Tuple[str, Path], @@ -64,7 +64,7 @@ def sd_servers_with_deleted_source_key( @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestJournalistLayoutCol: def test_col_with_and_without_documents( self, locale, sd_servers_with_submitted_file, firefox_web_driver diff --git a/securedrop/tests/functional/pageslayout/test_journalist_delete.py b/securedrop/tests/functional/pageslayout/test_journalist_delete.py index e820e611b1..43b5249462 100644 --- a/securedrop/tests/functional/pageslayout/test_journalist_delete.py +++ b/securedrop/tests/functional/pageslayout/test_journalist_delete.py @@ -21,7 +21,7 @@ @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestJournalistLayoutDelete: def test_delete_none(self, locale, sd_servers_with_submitted_file, firefox_web_driver): # Given an SD server with a file submitted by a source diff --git a/securedrop/tests/functional/pageslayout/test_source.py b/securedrop/tests/functional/pageslayout/test_source.py index fd23909800..dc212ce196 100644 --- a/securedrop/tests/functional/pageslayout/test_source.py +++ b/securedrop/tests/functional/pageslayout/test_source.py @@ -21,7 +21,7 @@ @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestSourceLayout: def test(self, locale, sd_servers_with_clean_state, tor_browser_web_driver): # Given a source user accessing the app from their browser diff --git a/securedrop/tests/functional/pageslayout/test_source_layout_torbrowser.py b/securedrop/tests/functional/pageslayout/test_source_layout_torbrowser.py index 18dbe8beec..b90a45b821 100644 --- a/securedrop/tests/functional/pageslayout/test_source_layout_torbrowser.py +++ b/securedrop/tests/functional/pageslayout/test_source_layout_torbrowser.py @@ -22,7 +22,7 @@ @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestSourceLayoutTorBrowser: def test_index_and_logout(self, locale, sd_servers): # Given a source user accessing the app from their browser @@ -31,7 +31,6 @@ def test_index_and_logout(self, locale, sd_servers): source_app_base_url=sd_servers.source_app_base_url, accept_languages=locale_with_commas, ) as navigator: - # And they have disabled JS in their browser disable_js(navigator.driver) diff --git a/securedrop/tests/functional/pageslayout/test_source_session_layout.py b/securedrop/tests/functional/pageslayout/test_source_session_layout.py index e234ef25c6..151e7cffb6 100644 --- a/securedrop/tests/functional/pageslayout/test_source_session_layout.py +++ b/securedrop/tests/functional/pageslayout/test_source_session_layout.py @@ -36,7 +36,7 @@ def sd_servers_with_short_timeout( @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestSourceAppSessionTimeout: def test_source_session_timeout(self, locale, sd_servers_with_short_timeout): # Given an SD server with a very short session timeout diff --git a/securedrop/tests/functional/pageslayout/test_source_static_pages.py b/securedrop/tests/functional/pageslayout/test_source_static_pages.py index 484af40ac2..80b880f4be 100644 --- a/securedrop/tests/functional/pageslayout/test_source_static_pages.py +++ b/securedrop/tests/functional/pageslayout/test_source_static_pages.py @@ -7,7 +7,7 @@ from version import __version__ -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestSourceAppStaticPages: @pytest.mark.parametrize("locale", list_locales()) def test_notfound(self, locale, sd_servers): diff --git a/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py b/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py index 366af4b727..563254c0d4 100644 --- a/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py +++ b/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py @@ -7,7 +7,7 @@ @pytest.mark.parametrize("locale", list_locales()) -@pytest.mark.pagelayout() +@pytest.mark.pagelayout class TestSubmitAndRetrieveFile: def test_submit_and_retrieve_happy_path( self, locale, sd_servers_with_clean_state, tor_browser_web_driver, firefox_web_driver diff --git a/securedrop/tests/functional/test_admin_interface.py b/securedrop/tests/functional/test_admin_interface.py index 4a017ce8fe..8ddd9de8f2 100644 --- a/securedrop/tests/functional/test_admin_interface.py +++ b/securedrop/tests/functional/test_admin_interface.py @@ -169,7 +169,7 @@ def _create_second_journalist(config_in_use: SecureDropConfig) -> None: db_session_for_sd_servers.commit() -@pytest.fixture() +@pytest.fixture def sd_servers_with_second_journalist( setup_journalist_key_and_gpg_folder: Tuple[str, Path], setup_rqworker: Tuple[str, Path], diff --git a/securedrop/tests/functional/test_journalist.py b/securedrop/tests/functional/test_journalist.py index 35fa9b4d84..68002330e2 100644 --- a/securedrop/tests/functional/test_journalist.py +++ b/securedrop/tests/functional/test_journalist.py @@ -375,7 +375,7 @@ def one_source_no_files(): journ_app_nav.nav_helper.wait_for(one_source_no_files) -@pytest.fixture() +@pytest.fixture def sd_servers_with_missing_file( setup_journalist_key_and_gpg_folder: Tuple[str, Path], setup_rqworker: Tuple[str, Path], diff --git a/securedrop/tests/functional/test_source_warnings.py b/securedrop/tests/functional/test_source_warnings.py index 782735be3f..e133327472 100644 --- a/securedrop/tests/functional/test_source_warnings.py +++ b/securedrop/tests/functional/test_source_warnings.py @@ -8,7 +8,7 @@ from tests.functional.web_drivers import _FIREFOX_PATH -@pytest.fixture() +@pytest.fixture def orbot_web_driver(sd_servers): # Create new profile and driver with the orbot user agent orbot_user_agent = "Mozilla/5.0 (Android; Mobile; rv:52.0) Gecko/20100101 Firefox/52.0" diff --git a/securedrop/tests/functional/web_drivers.py b/securedrop/tests/functional/web_drivers.py index ee57ebc7d6..b7685b418e 100644 --- a/securedrop/tests/functional/web_drivers.py +++ b/securedrop/tests/functional/web_drivers.py @@ -41,7 +41,7 @@ def _create_torbrowser_driver( ) -> TorBrowserDriver: logging.info("Creating TorBrowserDriver") log_file = open(_LOGFILE_PATH, "a") - log_file.write("\n\n[%s] Running Functional Tests\n" % str(datetime.now())) + log_file.write(f"\n\n[{datetime.now()}] Running Functional Tests\n") log_file.flush() # Don't use Tor when reading from localhost, and turn off private diff --git a/securedrop/tests/migrations/migration_15ac9509fc68.py b/securedrop/tests/migrations/migration_15ac9509fc68.py index d4ac7755dc..596cd73f2d 100644 --- a/securedrop/tests/migrations/migration_15ac9509fc68.py +++ b/securedrop/tests/migrations/migration_15ac9509fc68.py @@ -33,7 +33,6 @@ def check_upgrade(self): class DowngradeTester: - JOURNO_NUM = 200 SOURCE_NUM = 200 diff --git a/securedrop/tests/migrations/migration_2d0ce3ee5bdc.py b/securedrop/tests/migrations/migration_2d0ce3ee5bdc.py index f71ab97e7b..6d2a1e951a 100644 --- a/securedrop/tests/migrations/migration_2d0ce3ee5bdc.py +++ b/securedrop/tests/migrations/migration_2d0ce3ee5bdc.py @@ -81,7 +81,6 @@ def extract(app): class UpgradeTester(Helper): - JOURNO_NUM = 100 def __init__(self, config): @@ -149,7 +148,6 @@ def add_journalist(): class DowngradeTester(Helper): - JOURNO_NUM = 100 def __init__(self, config): diff --git a/securedrop/tests/migrations/migration_3d91d6948753.py b/securedrop/tests/migrations/migration_3d91d6948753.py index 47dbf41f20..528a9e91ba 100644 --- a/securedrop/tests/migrations/migration_3d91d6948753.py +++ b/securedrop/tests/migrations/migration_3d91d6948753.py @@ -24,7 +24,6 @@ def __init__(self, config): def load_data(self): with self.app.app_context(): - for _ in range(self.SOURCE_NUM): self.add_source() @@ -58,7 +57,6 @@ def check_upgrade(self): class DowngradeTester: - SOURCE_NUM = 200 def __init__(self, config): @@ -67,7 +65,6 @@ def __init__(self, config): def load_data(self): with self.app.app_context(): - for _ in range(self.SOURCE_NUM): self.add_source() diff --git a/securedrop/tests/migrations/migration_60f41bb14d98.py b/securedrop/tests/migrations/migration_60f41bb14d98.py index accf871789..6b4bd03355 100644 --- a/securedrop/tests/migrations/migration_60f41bb14d98.py +++ b/securedrop/tests/migrations/migration_60f41bb14d98.py @@ -86,7 +86,6 @@ def check_upgrade(self): class DowngradeTester: - JOURNO_NUM = 20 def __init__(self, config): diff --git a/securedrop/tests/migrations/migration_6db892e17271.py b/securedrop/tests/migrations/migration_6db892e17271.py index cba9053472..08c27a3c9b 100644 --- a/securedrop/tests/migrations/migration_6db892e17271.py +++ b/securedrop/tests/migrations/migration_6db892e17271.py @@ -126,7 +126,6 @@ def check_upgrade(self): class DowngradeTester: - SOURCE_NUM = 200 JOURNO_NUM = 20 diff --git a/securedrop/tests/migrations/migration_b060f38c0c31.py b/securedrop/tests/migrations/migration_b060f38c0c31.py index bf3c7cd72a..1e158088ac 100644 --- a/securedrop/tests/migrations/migration_b060f38c0c31.py +++ b/securedrop/tests/migrations/migration_b060f38c0c31.py @@ -82,7 +82,6 @@ def add_source(self): def check_upgrade(self): with self.app.app_context(): - # check that the flagged column is gone with pytest.raises(OperationalError, match=".*sources has no column named flagged.*"): self.add_source() diff --git a/securedrop/tests/migrations/migration_e0a525cbab83.py b/securedrop/tests/migrations/migration_e0a525cbab83.py index 37278deed8..da4cab2fa7 100644 --- a/securedrop/tests/migrations/migration_e0a525cbab83.py +++ b/securedrop/tests/migrations/migration_e0a525cbab83.py @@ -124,7 +124,6 @@ def check_upgrade(self): class DowngradeTester: - SOURCE_NUM = 200 JOURNO_NUM = 20 diff --git a/securedrop/tests/migrations/migration_f2833ac34bb6.py b/securedrop/tests/migrations/migration_f2833ac34bb6.py index c5921b355c..0b6cf1baef 100644 --- a/securedrop/tests/migrations/migration_f2833ac34bb6.py +++ b/securedrop/tests/migrations/migration_f2833ac34bb6.py @@ -82,7 +82,6 @@ def check_upgrade(self): class DowngradeTester: - JOURNO_NUM = 20 def __init__(self, config): diff --git a/securedrop/tests/migrations/migration_fccf57ceef02.py b/securedrop/tests/migrations/migration_fccf57ceef02.py index a97f46806b..95f010465f 100644 --- a/securedrop/tests/migrations/migration_fccf57ceef02.py +++ b/securedrop/tests/migrations/migration_fccf57ceef02.py @@ -44,7 +44,6 @@ def __init__(self, config): def load_data(self): with self.app.app_context(): - for _ in range(self.SOURCE_NUM): add_source() @@ -81,7 +80,6 @@ def check_upgrade(self): class DowngradeTester: - SOURCE_NUM = 200 def __init__(self, config): @@ -90,7 +88,6 @@ def __init__(self, config): def load_data(self): with self.app.app_context(): - for _ in range(self.SOURCE_NUM): add_source() diff --git a/securedrop/tests/test_alembic.py b/securedrop/tests/test_alembic.py index d545d0563d..0028ce6f5f 100644 --- a/securedrop/tests/test_alembic.py +++ b/securedrop/tests/test_alembic.py @@ -25,7 +25,7 @@ WHITESPACE_REGEX = re.compile(r"\s+") -@pytest.fixture() +@pytest.fixture def _reset_db(config: SecureDropConfig) -> None: # The config fixture creates all the models in the DB, but most alembic tests expect an # empty DB, so we reset the DB via this fixture diff --git a/securedrop/tests/test_i18n.py b/securedrop/tests/test_i18n.py index 4bceb041b8..4032520ea6 100644 --- a/securedrop/tests/test_i18n.py +++ b/securedrop/tests/test_i18n.py @@ -157,7 +157,6 @@ def verify_i18n(app): ) with app.test_client() as c: - # a request without Accept-Language or "l" argument gets the # default locale page = c.get("/login") diff --git a/securedrop/tests/test_journalist.py b/securedrop/tests/test_journalist.py index ca72bea30c..ae1baaecd7 100644 --- a/securedrop/tests/test_journalist.py +++ b/securedrop/tests/test_journalist.py @@ -1184,7 +1184,6 @@ def test_admin_edits_user_invalid_username_deleted( def test_admin_resets_user_hotp_format_non_hexa(journalist_app, test_admin, test_journo): - with journalist_app.test_client() as app: login_journalist( app, @@ -1225,7 +1224,6 @@ def test_admin_resets_user_hotp_format_non_hexa(journalist_app, test_admin, test def test_admin_resets_user_hotp_format_too_short( journalist_app, test_admin, test_journo, the_secret ): - with journalist_app.test_client() as app: login_journalist( app, @@ -1256,8 +1254,9 @@ def test_admin_resets_user_hotp_format_too_short( assert journo.is_totp ins.assert_message_flashed( - "HOTP secrets are 40 characters long" - " - you have entered {num}.".format(num=len(the_secret.replace(" ", ""))), + "HOTP secrets are 40 characters long" " - you have entered {num}.".format( + num=len(the_secret.replace(" ", "")) + ), "error", ) @@ -1294,7 +1293,6 @@ def test_admin_resets_user_hotp(journalist_app, test_admin, test_journo): def test_admin_resets_user_hotp_error(mocker, journalist_app, test_admin, test_journo): - bad_secret = "0123456789ABCDZZ0123456789ABCDZZ01234567" error_message = "SOMETHING WRONG!" mocked_error_logger = mocker.patch("journalist.app.logger.error") @@ -1716,10 +1714,7 @@ def test_deleted_user_cannot_login(config, journalist_app, locale): ] with xfail_untranslated_messages(config, locale, msgids): ins.assert_message_flashed( - "{} {}".format( - gettext(msgids[0]), - gettext(msgids[1]), - ), + f"{gettext(msgids[0])} {gettext(msgids[1])}", "error", ) @@ -3502,7 +3497,7 @@ def test_download_selected_submissions_previously_downloaded( ) -@pytest.fixture() +@pytest.fixture def selected_missing_files(journalist_app, test_source, app_storage): """Fixture for the download tests with missing files in storage.""" source = Source.query.get(test_source["id"]) diff --git a/securedrop/tests/test_journalist_api.py b/securedrop/tests/test_journalist_api.py index 908833670a..884a06a11e 100644 --- a/securedrop/tests/test_journalist_api.py +++ b/securedrop/tests/test_journalist_api.py @@ -817,9 +817,7 @@ def test_authorized_user_can_add_reply( source = Source.query.get(source_id) - expected_filename = "{}-{}-reply.gpg".format( - source.interaction_count, source.journalist_filename - ) + expected_filename = f"{source.interaction_count}-{source.journalist_filename}-reply.gpg" expected_filepath = Path(app_storage.path(source.filesystem_id, expected_filename)) diff --git a/securedrop/tests/test_journalist_session.py b/securedrop/tests/test_journalist_session.py index 9711a90b62..2d1fa3fed4 100644 --- a/securedrop/tests/test_journalist_session.py +++ b/securedrop/tests/test_journalist_session.py @@ -13,7 +13,7 @@ NEW_PASSWORD = "another correct horse battery staple generic passphrase" -@pytest.fixture() +@pytest.fixture def redis(config): return Redis(**config.REDIS_KWARGS) diff --git a/securedrop/tests/test_manage.py b/securedrop/tests/test_manage.py index c11bff2b28..ae8346a5b5 100644 --- a/securedrop/tests/test_manage.py +++ b/securedrop/tests/test_manage.py @@ -61,11 +61,11 @@ def test_get_yubikey_usage_no(): def test_handle_invalid_secret(journalist_app, config, mocker, capsys): """Regression test for bad secret logic in manage.py""" - mocker.patch("manage._get_username", return_value="ntoll"), - mocker.patch("manage._get_first_name", return_value=""), - mocker.patch("manage._get_last_name", return_value=""), - mocker.patch("manage._get_yubikey_usage", return_value=True), - mocker.patch("manage.obtain_input", side_effect=YUBIKEY_HOTP), + mocker.patch("manage._get_username", return_value="ntoll") + mocker.patch("manage._get_first_name", return_value="") + mocker.patch("manage._get_last_name", return_value="") + mocker.patch("manage._get_yubikey_usage", return_value=True) + mocker.patch("manage.obtain_input", side_effect=YUBIKEY_HOTP) with journalist_app.app_context() as context: # We will try to provide one invalid and one valid secret diff --git a/securedrop/tests/test_remove_pending_sources.py b/securedrop/tests/test_remove_pending_sources.py index 11e012ab40..3b75d247e2 100644 --- a/securedrop/tests/test_remove_pending_sources.py +++ b/securedrop/tests/test_remove_pending_sources.py @@ -16,7 +16,7 @@ def test_remove_pending_sources_none_pending(n, m, source_app, config, app_stora with source_app.app_context(): sources = [] - for i in range(0, n): + for i in range(n): source_user = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default().generate_passphrase(), @@ -53,7 +53,7 @@ def test_remove_pending_sources_all_pending(n, m, source_app, config, app_storag with source_app.app_context(): sources = [] - for i in range(0, n): + for i in range(n): source_user = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default().generate_passphrase(), diff --git a/securedrop/tests/test_source.py b/securedrop/tests/test_source.py index 2dfa8fa75e..7410870776 100644 --- a/securedrop/tests/test_source.py +++ b/securedrop/tests/test_source.py @@ -812,9 +812,8 @@ def test_login_with_overly_long_codename(source_app): assert resp.status_code == 200 text = resp.data.decode("utf-8") assert ( - "Field must be between 1 and {} characters long.".format( - PassphraseGenerator.MAX_PASSPHRASE_LENGTH - ) + f"Field must be between 1 and {PassphraseGenerator.MAX_PASSPHRASE_LENGTH} characters " + "long." ) in text diff --git a/securedrop/tests/test_source_utils.py b/securedrop/tests/test_source_utils.py index 8d15987fa0..d29e6103a3 100644 --- a/securedrop/tests/test_source_utils.py +++ b/securedrop/tests/test_source_utils.py @@ -9,7 +9,6 @@ def test_check_url_file(config): - assert check_url_file("nosuchfile", "whatever") is None try: diff --git a/securedrop/tests/test_store.py b/securedrop/tests/test_store.py index 380c5368fc..5673796ff8 100644 --- a/securedrop/tests/test_store.py +++ b/securedrop/tests/test_store.py @@ -20,7 +20,7 @@ from tests import utils -@pytest.fixture() +@pytest.fixture def test_storage() -> Generator[Storage, None, None]: # Setup the filesystem for the storage object with TemporaryDirectory() as data_dir_name: @@ -169,7 +169,6 @@ def test_verify_regular_submission_in_sourcedir_returns_true(test_storage): def test_verify_invalid_file_extension_in_sourcedir_raises_exception(test_storage): - source_directory, file_path = create_file_in_source_dir( test_storage.storage_path, "example-filesystem-id", "not_valid.txt" ) @@ -181,7 +180,6 @@ def test_verify_invalid_file_extension_in_sourcedir_raises_exception(test_storag def test_verify_invalid_filename_in_sourcedir_raises_exception(test_storage): - source_directory, file_path = create_file_in_source_dir( test_storage.storage_path, "example-filesystem-id", "NOTVALID.gpg" ) diff --git a/securedrop/tests/test_two_factor.py b/securedrop/tests/test_two_factor.py index 741a40cbfd..bb447c0536 100644 --- a/securedrop/tests/test_two_factor.py +++ b/securedrop/tests/test_two_factor.py @@ -5,7 +5,6 @@ class TestHOTP: - _SECRET = "YQTEGUTJCMBETH3KUUZZMRWZAVBKGT5O" _VALID_TOKEN = "464263" _VALID_TOKEN_COUNTER = 12 @@ -67,7 +66,6 @@ def test_verify_but_token_invalid(self, token: str, counter: int) -> None: class TestTOTP: - _SECRET = "JHCOGO7VCER3EJ4L" _VALID_TOKEN = "705334" _VALID_TOKEN_GENERATED_AT = 1666515039 diff --git a/securedrop/tests/test_worker.py b/securedrop/tests/test_worker.py index 285e7dcded..3785eea912 100644 --- a/securedrop/tests/test_worker.py +++ b/securedrop/tests/test_worker.py @@ -28,7 +28,7 @@ def start_rq_worker(config, queue_name): "rq_config", queue_name, ], - preexec_fn=os.setsid, + preexec_fn=os.setsid, # noqa: PLW1509 ) diff --git a/securedrop/tests/test_wsgi.py b/securedrop/tests/test_wsgi.py index c2704a4e5e..86ba7fdb76 100644 --- a/securedrop/tests/test_wsgi.py +++ b/securedrop/tests/test_wsgi.py @@ -7,7 +7,7 @@ import pytest -@pytest.fixture() +@pytest.fixture def _journalist_pubkey(): """provision a valid public key for the JI startup check""" path = Path("/tmp/securedrop/journalist.pub") diff --git a/securedrop/tests/utils/instrument.py b/securedrop/tests/utils/instrument.py index cad1109467..e4bd49c7f0 100644 --- a/securedrop/tests/utils/instrument.py +++ b/securedrop/tests/utils/instrument.py @@ -122,8 +122,8 @@ def assert_redirects(self, response, expected_location, message=None): """ valid_status_codes = (301, 302, 303, 305, 307) valid_status_code_str = ", ".join([str(code) for code in valid_status_codes]) - not_redirect = "HTTP Status {} expected but got {}".format( - valid_status_code_str, response.status_code + not_redirect = ( + f"HTTP Status {valid_status_code_str} expected but got {response.status_code}" ) assert response.status_code in (valid_status_codes, message) or not_redirect assert response.location == expected_location, message diff --git a/securedrop/two_factor.py b/securedrop/two_factor.py index b9a915624d..0c00ac328b 100644 --- a/securedrop/two_factor.py +++ b/securedrop/two_factor.py @@ -38,7 +38,6 @@ class OtpTokenInvalid(Exception): class HOTP: - # Current parameters for HOTP _LENGTH = 6 @@ -91,7 +90,6 @@ def verify(self, token: str, counter: int) -> int: class TOTP: - # Current parameters for TOTP _LENGTH = 6 _TIME_STEP = 30 diff --git a/securedrop/upload-screenshots.py b/securedrop/upload-screenshots.py index bfef998af2..8eff57145b 100755 --- a/securedrop/upload-screenshots.py +++ b/securedrop/upload-screenshots.py @@ -85,7 +85,6 @@ def __init__( request_limit: int, canonicalization_rules: List[Tuple[str, str]], ) -> None: - if len(token) != 40: raise BadOrMissingTokenError( "API token is not in expected 40 character format.", base_url diff --git a/utils/backport.py b/utils/backport.py index 95f5fdb7f8..a86c586e70 100755 --- a/utils/backport.py +++ b/utils/backport.py @@ -6,6 +6,7 @@ The `gh` CLI must be set up and already authenticated. """ + import argparse import json import subprocess