diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml new file mode 100644 index 0000000..5d37787 --- /dev/null +++ b/.github/workflows/bandit.yml @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileNotice: Part of the Telemetry addon. + +################################################################################ +# # +# © 2026 FreeCAD Project Association # +# # +# This addon is free software: you can redistribute it and/or modify # +# it under the terms of the GNU Lesser General Public License as # +# published by the Free Software Foundation, either version 2.1 # +# of the License, or (at your option) any later version. # +# # +# This addon is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty # +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # +# See the GNU Lesser General Public License for more details. # +# # +# You should have received a copy of the GNU Lesser General Public # +# License along with this addon. If not, see https://www.gnu.org/licenses # +# # +################################################################################ + +# Static security analysis of the Telemetry addon with Bandit. Uses the same +# configuration as the FreeCAD addon quality report (FreeCAD/Addon-Reports). + +name: Bandit security scan + +permissions: + contents: read + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + bandit: + name: Bandit + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install Bandit + run: | + python -m pip install --upgrade pip + pip install bandit + + - name: Run Bandit + run: bandit -c bandit.yaml -r . diff --git a/Resources/translations/run_translation_cycle.py b/Resources/translations/run_translation_cycle.py index 7272a45..23fb1c0 100644 --- a/Resources/translations/run_translation_cycle.py +++ b/Resources/translations/run_translation_cycle.py @@ -34,7 +34,10 @@ import os import shutil import stat -import subprocess + +# Audited: subprocess is only used to run the Qt lupdate/lrelease tools with fixed argument lists +# and no shell (added nosec B404) +import subprocess # nosec B404 import sys import tempfile import time @@ -85,7 +88,8 @@ def _make_api_req(self, url, extra_headers=None, method="GET", data=None): data = json.dumps(data).encode("utf-8") request = Request(url, headers=headers, method=method, data=data) - request_result = urlopen(request) + # Audited: every URL is built on the hardcoded HTTPS CROWDIN_API_URL (added nosec B310) + request_result = urlopen(request) # nosec B310 if request_result.getcode() >= 300: print(f"Failed to make API request {url}: return code {request_result.getcode()}") raise Exception("Failed to make API request") @@ -135,7 +139,11 @@ def status(self): def download(self, build_id): filename = f"{self.project_identifier}.zip" response = self._make_project_api_req(f"/translations/builds/{build_id}/download") - urlretrieve(response["url"], filename) + download_url = response["url"] + if not download_url.startswith("https://"): + raise Exception(f"Refusing to download from non-HTTPS URL {download_url}") + # Audited: only HTTPS URLs reach this point (added nosec B310) + urlretrieve(download_url, filename) # nosec B310 print("download of " + filename + " complete") def build(self): @@ -185,7 +193,8 @@ def process_single_translation_file(source_path: str, target_path: str): print("Generating qm file for", basename, "...") try: - subprocess.run( + # Audited: fixed arguments, no shell; lrelease is expected on PATH (added nosec B603, B607) + subprocess.run( # nosec B603 B607 [ "lrelease", new_path, @@ -348,7 +357,8 @@ def run_and_download_build(crowdin_updater: CrowdinUpdater): "-ts", os.path.join(TS_FILE_PATH, CROWDIN_FILE_NAME), ] - result = subprocess.run( + # Audited: fixed arguments, no shell; lupdate is expected on PATH (added nosec B603) + result = subprocess.run( # nosec B603 args, timeout=30, check=True, diff --git a/TelemetryPreferences.py b/TelemetryPreferences.py index 175c02b..ad66497 100644 --- a/TelemetryPreferences.py +++ b/TelemetryPreferences.py @@ -156,7 +156,8 @@ def _remove_user_data() -> (bool, str): url = f"https://www.freecad.org/deletetelemetry.php?person_id={uuid}" req = urllib.request.Request(url, method="DELETE") try: - with urllib.request.urlopen(req) as response: + # Audited: the URL is a hardcoded HTTPS endpoint (added nosec B310) + with urllib.request.urlopen(req) as response: # nosec B310 if 200 <= response.status < 300: return True, FreeCAD.Qt.translate( "Telemetry", "Your user data was successfully removed from the database." diff --git a/bandit.yaml b/bandit.yaml new file mode 100644 index 0000000..8425c4e --- /dev/null +++ b/bandit.yaml @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileNotice: Part of the Telemetry addon. + +# Configuration for Bandit static security analysis. Run locally with: +# bandit -c bandit.yaml -r . +# +# The settings mirror the ones used by the FreeCAD addon quality report +# (https://github.com/FreeCAD/Addon-Reports), so a clean run here means a clean +# entry there. Any finding must either be fixed or carry an audited "nosec" +# marker explaining why it is safe. +# +# B101 (assert_used) and B110 (try_except_pass) are informational style checks +# rather than security defects, and the addon report skips them as well. +# +# Bandit treats each exclude entry as a plain substring of the file path, so the +# patterns are anchored with path separators. + +exclude_dirs: + - "*/.venv/*" + +skips: + - B101 + - B110 diff --git a/package.xml b/package.xml index eda413f..3240e95 100644 --- a/package.xml +++ b/package.xml @@ -2,7 +2,7 @@ Telemetry Help improve FreeCAD by sending basic metrics to the development team. - 1.0.6 + 1.0.7 2026-6-2 The FreeCAD project association AISBL LGPL-2.1-or-later