diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7dd46744..15f4a48b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,9 +40,28 @@ jobs: - uses: actions/checkout@v4 - uses: chartboost/ruff-action@v1 + security: + + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install bandit[toml] + - name: Run Bandit + run: | + bandit -r src/borg -c pyproject.toml + linux: - needs: lint + needs: [lint, security] strategy: fail-fast: true matrix: diff --git a/pyproject.toml b/pyproject.toml index ef0015de5..4a0d77d2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,7 @@ ignore_missing_imports = true [tool.tox] requires = ["tox>=4.19", "pkgconfig", "cython", "wheel", "setuptools_scm"] -env_list = ["py{310,311,312,313}-{none,fuse2,fuse3}", "docs", "ruff", "mypy"] +env_list = ["py{310,311,312,313}-{none,fuse2,fuse3}", "docs", "ruff", "mypy", "bandit"] [tool.tox.env_run_base] package = "editable-legacy" # without this it does not find setup_docs when running under fakeroot @@ -195,3 +195,15 @@ commands = [["mypy", "--ignore-missing-imports"]] change_dir = "docs" deps = ["sphinx", "sphinxcontrib-jquery", "guzzle_sphinx_theme"] commands = [["sphinx-build", "-n", "-v", "-W", "--keep-going", "-b", "html", "-d", "{envtmpdir}/doctrees", ".", "{envtmpdir}/html"]] + +[tool.bandit] +exclude_dirs = [".cache", ".eggs", ".git", ".git-rewrite", ".idea", ".mypy_cache", ".ruff_cache", ".tox", "build", "dist", "src/borg/testsuite"] +skips = [ + "B101", # skip assert warnings, we do not allow running borg with assertions disabled. + "B404", # do not warn about just import subprocess +] + +[tool.tox.env.bandit] +skip_install = true +deps = ["bandit[toml]"] +commands = [["bandit", "-r", "src/borg", "-c", "pyproject.toml"]] diff --git a/requirements.d/development.txt b/requirements.d/development.txt index bd25383a0..10d7b55bf 100644 --- a/requirements.d/development.txt +++ b/requirements.d/development.txt @@ -12,3 +12,4 @@ pytest-cov pytest-benchmark Cython pre-commit +bandit[toml] diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index e1b0a8bce..b2c4bfbea 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -73,7 +73,7 @@ class CreateMixIn: try: try: env = prepare_subprocess_env(system=True) - proc = subprocess.Popen( + proc = subprocess.Popen( # nosec B603 args.paths, stdout=subprocess.PIPE, env=env, @@ -97,7 +97,7 @@ class CreateMixIn: if args.paths_from_command: try: env = prepare_subprocess_env(system=True) - proc = subprocess.Popen( + proc = subprocess.Popen( # nosec B603 args.paths, stdout=subprocess.PIPE, env=env, preexec_fn=None if is_win32 else ignore_sigint ) except (FileNotFoundError, PermissionError) as e: diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index 1a9c0051e..dadfe039a 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -22,7 +22,7 @@ class LocksMixIn: env = prepare_subprocess_env(system=True) try: # we exit with the return code we get from the subprocess - rc = subprocess.call([args.command] + args.args, env=env) + rc = subprocess.call([args.command] + args.args, env=env) # nosec B603 set_ec(rc) except (FileNotFoundError, OSError, ValueError) as e: raise CommandError(f"Error while trying to run '{args.command}': {e}") diff --git a/src/borg/conftest.py b/src/borg/conftest.py index 98b70f47b..176003816 100644 --- a/src/borg/conftest.py +++ b/src/borg/conftest.py @@ -57,7 +57,7 @@ def pytest_report_header(config, start_path): def set_env_variables(): os.environ["BORG_CHECK_I_KNOW_WHAT_I_AM_DOING"] = "YES" os.environ["BORG_DELETE_I_KNOW_WHAT_I_AM_DOING"] = "YES" - os.environ["BORG_PASSPHRASE"] = "waytooeasyonlyfortests" + os.environ["BORG_PASSPHRASE"] = "waytooeasyonlyfortests" # nosec B105 os.environ["BORG_SELFTEST"] = "disabled" @@ -103,7 +103,8 @@ def archiver(tmp_path, set_env_variables): os.environ["BORG_KEYS_DIR"] = archiver.keys_path os.environ["BORG_CACHE_DIR"] = archiver.cache_path os.mkdir(archiver.input_path) - os.chmod(archiver.input_path, 0o777) # avoid troubles with fakeroot / FUSE + # avoid troubles with fakeroot / FUSE: + os.chmod(archiver.input_path, 0o777) # nosec B103 os.mkdir(archiver.output_path) os.mkdir(archiver.keys_path) os.mkdir(archiver.cache_path) diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index a9412f7fb..8c0df8a93 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -658,7 +658,7 @@ class FlexiKey: elif self.STORAGE == KeyBlobStorage.REPO: # While the repository is encrypted, we consider a repokey repository with a blank # passphrase an unencrypted repository. - self.logically_encrypted = passphrase != "" + self.logically_encrypted = passphrase != "" # nosec B105 # what we get in target is just a repo location, but we already have the repo obj: target = self.repository @@ -688,7 +688,7 @@ class FlexiKey: fd.write(key_data) fd.write("\n") elif self.STORAGE == KeyBlobStorage.REPO: - self.logically_encrypted = passphrase != "" + self.logically_encrypted = passphrase != "" # nosec B105 key_data = key_data.encode("utf-8") # remote repo: msgpack issue #99, giving bytes target.save_key(key_data) else: diff --git a/src/borg/fslocking.py b/src/borg/fslocking.py index 67b94a4dd..a0c5fc141 100644 --- a/src/borg/fslocking.py +++ b/src/borg/fslocking.py @@ -169,11 +169,11 @@ class ExclusiveLock: # should be cleaned up anyway. Try to clean up, but don't crash. try: os.unlink(temp_unique_name) - except: # noqa + except: # nosec B110 # noqa pass try: os.rmdir(temp_path) - except: # noqa + except: # nosec B110 # noqa pass def release(self): diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py index c4ac07e9d..3ce0d140a 100644 --- a/src/borg/helpers/fs.py +++ b/src/borg/helpers/fs.py @@ -558,9 +558,9 @@ def umount(mountpoint): env = prepare_subprocess_env(system=True) try: - rc = subprocess.call(["fusermount", "-u", mountpoint], env=env) + rc = subprocess.call(["fusermount", "-u", mountpoint], env=env) # nosec B603, B607 except FileNotFoundError: - rc = subprocess.call(["umount", mountpoint], env=env) + rc = subprocess.call(["umount", mountpoint], env=env) # nosec B603, B607 set_ec(rc) diff --git a/src/borg/helpers/passphrase.py b/src/borg/helpers/passphrase.py index 8fe88b7ff..59b5f65c4 100644 --- a/src/borg/helpers/passphrase.py +++ b/src/borg/helpers/passphrase.py @@ -67,7 +67,7 @@ class Passphrase(str): # passcommand is a system command (not inside pyinstaller env) env = prepare_subprocess_env(system=True) try: - passphrase = subprocess.check_output(shlex.split(passcommand), text=True, env=env) + passphrase = subprocess.check_output(shlex.split(passcommand), text=True, env=env) # nosec B603 except (subprocess.CalledProcessError, FileNotFoundError) as e: raise PasscommandFailure(e) return cls(passphrase.rstrip("\n")) diff --git a/src/borg/helpers/process.py b/src/borg/helpers/process.py index 1112f9807..84cd69758 100644 --- a/src/borg/helpers/process.py +++ b/src/borg/helpers/process.py @@ -286,7 +286,7 @@ def popen_with_error_handling(cmd_line: str, log_prefix="", **kwargs): return logger.debug("%scommand line: %s", log_prefix, command) try: - return subprocess.Popen(command, **kwargs) + return subprocess.Popen(command, **kwargs) # nosec B603 except FileNotFoundError: logger.error("%sexecutable not found: %s", log_prefix, command[0]) return diff --git a/src/borg/legacyremote.py b/src/borg/legacyremote.py index 4496c2319..8e412af66 100644 --- a/src/borg/legacyremote.py +++ b/src/borg/legacyremote.py @@ -275,7 +275,9 @@ class LegacyRemoteRepository: borg_cmd = self.ssh_cmd(location) + borg_cmd logger.debug("SSH command line: %s", borg_cmd) # we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg. - self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint) + self.p = Popen( + borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint + ) # nosec B603 self.stdin_fd = self.p.stdin.fileno() self.stdout_fd = self.p.stdout.fileno() self.stderr_fd = self.p.stderr.fileno() diff --git a/src/borg/platform/base.py b/src/borg/platform/base.py index c415985d9..70cc933f5 100644 --- a/src/borg/platform/base.py +++ b/src/borg/platform/base.py @@ -269,7 +269,7 @@ def getfqdn(name=""): An empty argument is interpreted as meaning the local host. """ name = name.strip() - if not name or name == "0.0.0.0": + if not name or name == "0.0.0.0": # nosec B104:hardcoded_bind_all_interfaces name = socket.gethostname() try: addrs = socket.getaddrinfo(name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME) diff --git a/src/borg/remote.py b/src/borg/remote.py index 4f315c78a..11cde91ae 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -576,7 +576,9 @@ class RemoteRepository: borg_cmd = self.ssh_cmd(location) + borg_cmd logger.debug("SSH command line: %s", borg_cmd) # we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg. - self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint) + self.p = Popen( + borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint + ) # nosec B603 self.stdin_fd = self.p.stdin.fileno() self.stdout_fd = self.p.stdout.fileno() self.stderr_fd = self.p.stderr.fileno() diff --git a/src/borg/storelocking.py b/src/borg/storelocking.py index 2e8e9aad5..af48e82ab 100644 --- a/src/borg/storelocking.py +++ b/src/borg/storelocking.py @@ -201,7 +201,9 @@ class Lock: logger.debug("LOCK-ACQUIRE: exclusive locks detected, deleting our shared lock.") self._delete_lock(key, ignore_not_found=True, update_last_refresh=True) # wait a random bit before retrying - time.sleep(self.retry_delay_min + (self.retry_delay_max - self.retry_delay_min) * random.random()) + time.sleep( + self.retry_delay_min + (self.retry_delay_max - self.retry_delay_min) * random.random() # nosec B311 + ) logger.debug("LOCK-ACQUIRE: timeout while trying to acquire a lock.") raise LockTimeout(str(self.store)) diff --git a/src/borg/xattr.py b/src/borg/xattr.py index b914c5046..009d1f11d 100644 --- a/src/borg/xattr.py +++ b/src/borg/xattr.py @@ -28,7 +28,7 @@ if sys.platform.startswith("linux"): for preload in preloads: if preload.startswith("libfakeroot"): env = prepare_subprocess_env(system=True) - fakeroot_output = subprocess.check_output(["fakeroot", "-v"], env=env) + fakeroot_output = subprocess.check_output(["fakeroot", "-v"], env=env) # nosec B603, B607 fakeroot_version = parse_version(fakeroot_output.decode("ascii").split()[-1]) if fakeroot_version >= parse_version("1.20.2"): # 1.20.2 has been confirmed to have xattr support