Skip to content

fix(security): patch AppleScript injection and path traversal; add parser tests#175

Merged
docdyhr merged 1 commit into
mainfrom
fix/release-workflow-failures
May 29, 2026
Merged

fix(security): patch AppleScript injection and path traversal; add parser tests#175
docdyhr merged 1 commit into
mainfrom
fix/release-workflow-failures

Conversation

@docdyhr
Copy link
Copy Markdown
Owner

@docdyhr docdyhr commented May 29, 2026

Summary

  • AppleScript injection fix (macos_integration.py): message, title, and subtitle are now escaped ("\") before interpolation into the osascript string literal — a raw " in any value could previously break out of the string and inject arbitrary AppleScript commands
  • Path traversal fix (ui.py): all three filter file operations (save_filter, load_filter, delete_filter) now assert path.resolve().is_relative_to(filters_dir.resolve()) — the previous replace("/","_") left .. sequences intact, allowing names like ../../etc/passwd to escape the filters directory
  • 92 new tests for versiontracker/version/parser.py covering every public and internal function; parser branch coverage is now 93.45%
  • CI coverage gate raised from 50% → 80% in coverage.yml to match actual project coverage (86%) and prevent silent regressions

Test plan

  • pytest tests/test_version_parser.py — 92 passed
  • Full suite (pytest) — 2477 passed, 16 skipped
  • All pre-commit hooks pass (ruff, mypy, bandit, pydocstyle, detect-secrets)

🤖 Generated with Claude Code

Summary by Sourcery

Patch security vulnerabilities in macOS notifications and filter file handling, expand version parser test coverage, and tighten CI coverage enforcement.

Bug Fixes:

  • Escape user-provided AppleScript notification fields to prevent command injection in macOS integrations.
  • Validate filter file paths against the filters directory to prevent path traversal in save, load, and delete operations.

Enhancements:

  • Add comprehensive tests for the version parser to cover public and internal parsing utilities and edge cases.

CI:

  • Increase pytest coverage failure threshold from 50% to 80% in the coverage workflow to align with current project coverage.

…rser tests

Escape double-quotes in macos_integration.py before interpolating
message/title/subtitle into the osascript string literal — a raw `"`
in any value could break out and inject arbitrary AppleScript commands.

Guard all three filter file operations in ui.py (save/load/delete) with
`path.resolve().is_relative_to(filters_dir.resolve())` — the previous
`replace("/","_")` left `..` sequences intact, allowing names like
`../../etc/passwd` to escape the filters directory.

Add 92 tests for versiontracker/version/parser.py covering every public
and internal function; parser branch coverage is now 93.45%.

Raise the CI coverage gate from 50% to 80% to match actual project
coverage (86%) and prevent silent regressions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 29, 2026

Reviewer's Guide

Fixes two security issues (AppleScript injection and filter path traversal), expands test coverage for the version parser with a comprehensive new test module, and tightens CI coverage thresholds to match current project coverage.

Sequence diagram for escaped AppleScript notifications

sequenceDiagram
    participant App
    participant MacOSIntegration
    participant osascript

    App->>MacOSIntegration: send_notification(title, message, subtitle)
    MacOSIntegration->>MacOSIntegration: message.replace
    MacOSIntegration->>MacOSIntegration: title.replace
    alt subtitle provided
        MacOSIntegration->>MacOSIntegration: subtitle.replace
    end
    MacOSIntegration->>osascript: subprocess.run(cmd)
    osascript-->>MacOSIntegration: result
    MacOSIntegration-->>App: bool
Loading

File-Level Changes

Change Details Files
Harden macOS notification integration against AppleScript injection by escaping user-controlled strings before embedding them in osascript commands.
  • Introduce safe_message, safe_title, and safe_subtitle variables with double-quote escaping prior to command construction.
  • Update the osascript command string to interpolate the escaped values instead of raw input.
  • Retain existing subprocess.run invocation and security annotation while feeding it the sanitized command list.
versiontracker/macos_integration.py
Prevent filter file path traversal in the UI by validating that computed filter paths stay within the filters directory.
  • After computing safe_name and filter_path for save_filter, assert via resolve().is_relative_to() that the path is under filters_dir, raising ValueError on violation.
  • Apply the same path validation in load_filter before existence checks and file reads.
  • Apply the same path validation in delete_filter before existence checks and file deletion operations.
versiontracker/ui.py
Document security fixes, new tests, and CI coverage threshold updates in the changelog.
  • Add a Security section describing the AppleScript escaping change and filter path traversal mitigation.
  • Add a Tests section summarizing the new version parser test coverage.
  • Add a CI section noting the increased coverage threshold and rationale.
CHANGELOG.md
Increase CI coverage enforcement to 80% by updating pytest-cov thresholds in the coverage workflow.
  • Change --cov-fail-under from 50 to 80 in the primary coverage job pytest invocation.
  • Apply the same --cov-fail-under change in the matrix/secondary coverage job configuration to keep behavior consistent.
.github/workflows/coverage.yml
Add a comprehensive test suite for versiontracker.version.parser covering public and internal parsing helpers.
  • Introduce parametrized tests for parse_version covering normal, prefixed, prerelease, build-metadata, special, and edge-case formats.
  • Add unit tests for internal helpers such as _clean_version_string, _extract_build_metadata, _handle_special_beta_format, _extract_prerelease_info, _parse_numeric_parts, and normalization/tuple-building utilities.
  • Verify behavior for mixed-format versions, multi-component detection, prerelease tuple construction, and final tuple assembly, raising branch/line coverage of parser module into the 90% range.
tests/test_version_parser.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions
Copy link
Copy Markdown

🔒 Security Analysis Report

Security Analysis Report

Generated: Fri May 29 11:12:19 UTC 2026
Repository: docdyhr/versiontracker
Commit: 1dfd00c

Bandit Security Scan

�[?25l
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m  0%�[0m �[36m-:--:--�[0m
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m  0%�[0m �[36m-:--:--�[0m
�[2KWorking... �[91m━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m  6%�[0m �[36m-:--:--�[0m
�[2KWorking... �[91m━━━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m 13%�[0m �[36m0:00:02�[0m
�[2KWorking... �[91m━━━━━━━━�[0m�[91m╸�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m 22%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m 33%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m 43%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━�[0m�[91m╸�[0m�[90m━━━━━━━━━━━━━━━━━━━�[0m �[35m 52%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━━━━�[0m �[35m 69%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m�[90m╺�[0m�[90m━━━━━━━━━�[0m �[35m 76%�[0m �[36m0:00:01�[0m
�[2KWorking... �[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m�[91m╸�[0m�[90m━━━━�[0m �[35m 89%�[0m �[36m0:00:01�[0m
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m100%�[0m �[33m0:00:01�[0m
�[2KWorking... �[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━�[0m �[35m100%�[0m �[33m0:00:01�[0m
�[?25hRun started:2026-05-29 11:12:20.353320+00:00

Test results:
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
   Severity: Medium   Confidence: Low
   CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b608_hardcoded_sql_expressions.html
   Location: versiontracker/advanced_cache.py:610:24
609	                # Use f-string for better readability
610	                msg = f"Failed to delete from cache {key}: {e}"
611	                raise CacheError(msg) from e

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/config.py:174:16
173	                cmd = f"{path} --version"
174	                import subprocess
175	

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/config.py:177:29
176	                try:
177	                    result = subprocess.run(cmd.split(), capture_output=True, timeout=2, check=False)
178	                    returncode = result.returncode

--------------------------------------------------
>> Issue: [B110:try_except_pass] Try, Except, Pass detected.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b110_try_except_pass.html
   Location: versiontracker/experimental/analytics.py:645:16
644	                    self.peak_cpu = max(self.peak_cpu, cpu_percent)
645	                except Exception:
646	                    pass
647	                time.sleep(0.05)

--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b101_assert_used.html
   Location: versiontracker/handlers/outdated_handlers.py:493:8
492	        # Type assertion: apps and brews cannot be None here due to exit_code checks above
493	        assert apps is not None
494	        assert brews is not None

--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b101_assert_used.html
   Location: versiontracker/handlers/outdated_handlers.py:494:8
493	        assert apps is not None
494	        assert brews is not None
495	        apps = _filter_applications(apps, brews, include_brews)

--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
   Severity: Low   Confidence: High
   CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b101_assert_used.html
   Location: versiontracker/handlers/outdated_handlers.py:511:8
510	        # Type assertion: outdated_info cannot be None here due to exit_code check
511	        assert outdated_info is not None
512	        # Type cast: outdated_info cannot be None here due to exit_code check above

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/homebrew.py:12:0
11	import re
12	import subprocess
13	import time

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/macos_integration.py:11:0
10	import os
11	import subprocess
12	from pathlib import Path

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/macos_integration.py:250:21
249	            # nosec B603 - osascript with controlled arguments
250	            result = subprocess.run(cmd, capture_output=True, text=True)
251	

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/menubar_app.py:8:0
7	import logging
8	import subprocess
9	import sys

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/plugins/example_plugins.py:322:16
321	            try:
322	                import subprocess
323	

--------------------------------------------------
>> Issue: [B607:start_process_with_partial_path] Starting a process with a partial executable path
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b607_start_process_with_partial_path.html
   Location: versiontracker/plugins/example_plugins.py:324:25
323	
324	                result = subprocess.run(["brew", "--version"], capture_output=True, text=True, timeout=5)
325	                if result.returncode == 0:

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/plugins/example_plugins.py:324:25
323	
324	                result = subprocess.run(["brew", "--version"], capture_output=True, text=True, timeout=5)
325	                if result.returncode == 0:

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/utils.py:15:0
14	import shutil
15	import subprocess
16	import sys

--------------------------------------------------
>> Issue: [B607:start_process_with_partial_path] Starting a process with a partial executable path
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b607_start_process_with_partial_path.html
   Location: versiontracker/utils.py:784:17
783	    try:
784	        result = subprocess.run(["which", "brew"], capture_output=True, text=True, timeout=5)
785	        return result.returncode == 0

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/utils.py:784:17
783	    try:
784	        result = subprocess.run(["which", "brew"], capture_output=True, text=True, timeout=5)
785	        return result.returncode == 0

--------------------------------------------------
>> Issue: [B607:start_process_with_partial_path] Starting a process with a partial executable path
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b607_start_process_with_partial_path.html
   Location: versiontracker/utils.py:800:17
799	    try:
800	        result = subprocess.run(["brew", "--prefix"], capture_output=True, text=True, timeout=5)
801	        if result.returncode == 0:

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/utils.py:800:17
799	    try:
800	        result = subprocess.run(["brew", "--prefix"], capture_output=True, text=True, timeout=5)
801	        if result.returncode == 0:

--------------------------------------------------
>> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b603_subprocess_without_shell_equals_true.html
   Location: versiontracker/utils.py:833:15
832	    try:
833	        return subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=check)
834	    except subprocess.TimeoutExpired as e:

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/version/batch.py:11:0
10	import logging
11	import subprocess
12	from concurrent.futures import ThreadPoolExecutor

--------------------------------------------------
>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.
   Severity: Low   Confidence: High
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_imports.html#b404-import-subprocess
   Location: versiontracker/version/homebrew.py:18:0
17	import logging
18	import subprocess
19	

--------------------------------------------------

Code scanned:
	Total lines of code: 15030
	Total lines skipped (#nosec): 0
	Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 33

Run metrics:
	Total issues (by severity):
		Undefined: 0
		Low: 21
		Medium: 1
		High: 0
	Total issues (by confidence):
		Undefined: 0
		Low: 1
		Medium: 0
		High: 21
Files skipped (0):

Safety Check Results



�[33m�[1m+===========================================================================================================================================================================================+�[0m


�[31m�[1mDEPRECATED: �[0m�[33m�[1mthis command (`check`) has been DEPRECATED, and will be unsupported beyond 01 June 2024.�[0m


�[32mWe highly encourage switching to the new �[0m�[32m�[1m`scan`�[0m�[32m command which is easier to use, more powerful, and can be set up to mimic the deprecated command if required.�[0m


�[33m�[1m+===========================================================================================================================================================================================+�[0m


+==============================================================================+

                               /$$$$$$            /$$
                              /$$__  $$          | $$
           /$$$$$$$  /$$$$$$ | $$  \__//$$$$$$  /$$$$$$   /$$   /$$
          /$$_____/ |____  $$| $$$$   /$$__  $$|_  $$_/  | $$  | $$
         |  $$$$$$   /$$$$$$$| $$_/  | $$$$$$$$  | $$    | $$  | $$
          \____  $$ /$$__  $$| $$    | $$_____/  | $$ /$$| $$  | $$
          /$$$$$$$/|  $$$$$$$| $$    |  $$$$$$$  |  $$$$/|  $$$$$$$
         |_______/  \_______/|__/     \_______/   \___/   \____  $$
                                                          /$$  | $$
                                                         |  $$$$$$/
  by safetycli.com                                        \______/

+==============================================================================+

 �[1mREPORT�[0m 

  Safety �[1mv3.8.0�[0m is scanning for �[1mVulnerabilities�[0m�[1m...�[0m
�[1m  Scanning dependencies�[0m in your �[1menvironment:�[0m

  -> /opt/hostedtoolcache/Python/3.13.13/x64/lib/python3.13/site-packages

  Using �[1mopen-source vulnerability database�[0m
�[1m  Found and scanned 110 packages�[0m
  Timestamp �[1m2026-05-29 11:12:21�[0m
�[1m  0�[0m�[1m vulnerabilities reported�[0m
�[1m  0�[0m�[1m vulnerabilities ignored�[0m
+==============================================================================+

 �[32m�[1mNo known security vulnerabilities reported.�[0m 

+==============================================================================+�[0m


�[33m�[1m+===========================================================================================================================================================================================+�[0m


�[31m�[1mDEPRECATED: �[0m�[33m�[1mthis command (`check`) has been DEPRECATED, and will be unsupported beyond 01 June 2024.�[0m


�[32mWe highly encourage switching to the new �[0m�[32m�[1m`scan`�[0m�[32m command which is easier to use, more powerful, and can be set up to mimic the deprecated command if required.�[0m


�[33m�[1m+===========================================================================================================================================================================================+�[0m


Pip-Audit Results

�[?25l�[32m-�[0m Collecting inputs
�[2K�[32m-�[0m Collecting inputs
�[2K�[32m-�[0m Collecting inputs
�[2K�[32m-�[0m Collecting inputs
�[2K�[32m\�[0m Collecting inputs
�[2K�[32m\�[0m Collecting inputs
�[2K�[32m\�[0m Collecting inputs
�[2K�[32m\�[0m Collecting inputs
�[2K�[32m\�[0m Collecting aiohappyeyeballs (2.6.2)
�[2K�[32m\�[0m Auditing aiohappyeyeballs (2.6.2)
�[2K�[32m\�[0m Collecting aiohttp (3.13.5)
�[2K�[32m\�[0m Auditing aiohttp (3.13.5)
�[2K�[32m\�[0m Collecting aiosignal (1.4.0)
�[2K�[32m\�[0m Auditing aiosignal (1.4.0)
�[2K�[32m\�[0m Collecting annotated-doc (0.0.4)
�[2K�[32m\�[0m Auditing annotated-doc (0.0.4)
�[2K�[32m\�[0m Collecting annotated-types (0.7.0)
�[2K�[32m\�[0m Auditing annotated-types (0.7.0)
�[2K�[32m\�[0m Collecting anyio (4.13.0)
�[2K�[32m\�[0m Auditing anyio (4.13.0)
�[2K�[32m\�[0m Collecting ast_serialize (0.5.0)
�[2K�[32m|�[0m Auditing ast_serialize (0.5.0)
�[2K�[32m|�[0m Collecting attrs (26.1.0)
�[2K�[32m|�[0m Auditing attrs (26.1.0)
�[2K�[32m|�[0m Collecting Authlib (1.7.2)
�[2K�[32m|�[0m Auditing Authlib (1.7.2)
�[2K�[32m|�[0m Collecting bandit (1.9.4)
�[2K�[32m|�[0m Auditing bandit (1.9.4)
�[2K�[32m|�[0m Collecting black (26.5.1)
�[2K�[32m|�[0m Auditing black (26.5.1)
�[2K�[32m|�[0m Collecting boolean.py (5.0)
�[2K�[32m|�[0m Auditing boolean.py (5.0)
�[2K�[32m|�[0m Collecting build (1.5.0)
�[2K�[32m|�[0m Auditing build (1.5.0)
�[2K�[32m|�[0m Collecting CacheControl (0.14.4)
�[2K�[32m|�[0m Auditing CacheControl (0.14.4)
�[2K�[32m|�[0m Collecting certifi (2026.5.20)
�[2K�[32m|�[0m Collecting certifi (2026.5.20)
�[2K�[32m|�[0m Auditing certifi (2026.5.20)
�[2K�[32m|�[0m Collecting cffi (2.0.0)
�[2K�[32m|�[0m Auditing cffi (2.0.0)
�[2K�[32m|�[0m Collecting charset-normalizer (3.4.7)
�[2K�[32m|�[0m Auditing charset-normalizer (3.4.7)
�[2K�[32m|�[0m Collecting click (8.4.1)
�[2K�[32m|�[0m Auditing click (8.4.1)
�[2K�[32m|�[0m Collecting coverage (7.14.1)
�[2K�[32m|�[0m Auditing coverage (7.14.1)
�[2K�[32m|�[0m Collecting cryptography (48.0.0)
�[2K�[32m|�[0m Auditing cryptography (48.0.0)
�[2K�[32m|�[0m Collecting cyclonedx-python-lib (11.7.0)
�[2K�[32m|�[0m Auditing cyclonedx-python-lib (11.7.0)
�[2K�[32m|�[0m Collecting defusedxml (0.7.1)
�[2K�[32m|�[0m Auditing defusedxml (0.7.1)
�[2K�[32m|�[0m Collecting docutils (0.23)
�[2K�[32m|�[0m Auditing docutils (0.23)
�[2K�[32m|�[0m Collecting dparse (0.6.4)
�[2K�[32m|�[0m Auditing dparse (0.6.4)
�[2K�[32m|�[0m Collecting filelock (3.29.0)
�[2K�[32m|�[0m Auditing filelock (3.29.0)
�[2K�[32m|�[0m Collecting frozenlist (1.8.0)
�[2K�[32m|�[0m Auditing frozenlist (1.8.0)
�[2K�[32m|�[0m Collecting h11 (0.16.0)
�[2K�[32m|�[0m Auditing h11 (0.16.0)
�[2K�[32m|�[0m Collecting httpcore (1.0.9)
�[2K�[32m|�[0m Auditing httpcore (1.0.9)
�[2K�[32m|�[0m Collecting httpx (0.28.1)
�[2K�[32m|�[0m Auditing httpx (0.28.1)
�[2K�[32m|�[0m Collecting id (1.6.1)
�[2K�[32m|�[0m Auditing id (1.6.1)
�[2K�[32m|�[0m Collecting idna (3.17)
�[2K�[32m|�[0m Auditing idna (3.17)
�[2K�[32m|�[0m Collecting iniconfig (2.3.0)
�[2K�[32m|�[0m Auditing iniconfig (2.3.0)
�[2K�[32m|�[0m Collecting jaraco.classes (3.4.0)
�[2K�[32m|�[0m Auditing jaraco.classes (3.4.0)
�[2K�[32m|�[0m Collecting jaraco.context (6.1.2)
�[2K�[32m|�[0m Auditing jaraco.context (6.1.2)
�[2K�[32m|�[0m Collecting jaraco.functools (4.5.0)
�[2K�[32m|�[0m Auditing jaraco.functools (4.5.0)
�[2K�[32m|�[0m Collecting jeepney (0.9.0)
�[2K�[32m|�[0m Auditing jeepney (0.9.0)
�[2K�[32m|�[0m Collecting Jinja2 (3.1.6)
�[2K�[32m|�[0m Auditing Jinja2 (3.1.6)
�[2K�[32m|�[0m Collecting joblib (1.5.3)
�[2K�[32m|�[0m Auditing joblib (1.5.3)
�[2K�[32m|�[0m Collecting joserfc (1.6.8)
�[2K�[32m|�[0m Auditing joserfc (1.6.8)
�[2K�[32m|�[0m Collecting keyring (25.7.0)
�[2K�[32m|�[0m Collecting keyring (25.7.0)
�[2K�[32m|�[0m Auditing keyring (25.7.0)
�[2K�[32m|�[0m Collecting librt (0.11.0)
�[2K�[32m|�[0m Auditing librt (0.11.0)
�[2K�[32m|�[0m Collecting license-expression (30.4.4)
�[2K�[32m|�[0m Auditing license-expression (30.4.4)
�[2K�[32m|�[0m Collecting macversiontracker (1.0.1)
�[2K�[32m|�[0m Auditing macversiontracker (1.0.1)
�[2K�[32m|�[0m Collecting markdown-it-py (4.2.0)
�[2K�[32m|�[0m Auditing markdown-it-py (4.2.0)
�[2K�[32m|�[0m Collecting MarkupSafe (3.0.3)
�[2K�[32m|�[0m Auditing MarkupSafe (3.0.3)
�[2K�[32m|�[0m Collecting marshmallow (4.3.0)
�[2K�[32m|�[0m Auditing marshmallow (4.3.0)
�[2K�[32m|�[0m Collecting mdurl (0.1.2)
�[2K�[32m|�[0m Auditing mdurl (0.1.2)
�[2K�[32m|�[0m Collecting more-itertools (11.1.0)
�[2K�[32m|�[0m Auditing more-itertools (11.1.0)
�[2K�[32m|�[0m Collecting msgpack (1.1.2)
�[2K�[32m|�[0m Auditing msgpack (1.1.2)
�[2K�[32m|�[0m Collecting multidict (6.7.1)
�[2K�[32m|�[0m Auditing multidict (6.7.1)
�[2K�[32m|�[0m Collecting mypy (2.1.0)
�[2K�[32m|�[0m Auditing mypy (2.1.0)
�[2K�[32m|�[0m Collecting mypy_extensions (1.1.0)
�[2K�[32m|�[0m Auditing mypy_extensions (1.1.0)
�[2K�[32m|�[0m Collecting nh3 (0.3.5)
�[2K�[32m|�[0m Auditing nh3 (0.3.5)
�[2K�[32m|�[0m Collecting nltk (3.9.4)
�[2K�[32m|�[0m Auditing nltk (3.9.4)
�[2K�[32m|�[0m Collecting packageurl-python (0.17.6)
�[2K�[32m|�[0m Auditing packageurl-python (0.17.6)
�[2K�[32m|�[0m Collecting packaging (26.2)
�[2K�[32m|�[0m Auditing packaging (26.2)
�[2K�[32m|�[0m Collecting pathspec (1.1.1)
�[2K�[32m|�[0m Auditing pathspec (1.1.1)
�[2K�[32m|�[0m Collecting pip (26.1.1)
�[2K�[32m|�[0m Auditing pip (26.1.1)
�[2K�[32m|�[0m Collecting pip-api (0.0.34)
�[2K�[32m|�[0m Auditing pip-api (0.0.34)
�[2K�[32m|�[0m Collecting pip_audit (2.10.0)
�[2K�[32m|�[0m Auditing pip_audit (2.10.0)
�[2K�[32m|�[0m Collecting pip-requirements-parser (32.0.1)
�[2K�[32m|�[0m Auditing pip-requirements-parser (32.0.1)
�[2K�[32m|�[0m Collecting platformdirs (4.10.0)
�[2K�[32m|�[0m Auditing platformdirs (4.10.0)
�[2K�[32m|�[0m Collecting pluggy (1.6.0)
�[2K�[32m|�[0m Auditing pluggy (1.6.0)
�[2K�[32m|�[0m Collecting propcache (0.5.2)
�[2K�[32m|�[0m Collecting propcache (0.5.2)
�[2K�[32m|�[0m Auditing propcache (0.5.2)
�[2K�[32m|�[0m Collecting psutil (7.2.2)
�[2K�[32m|�[0m Auditing psutil (7.2.2)
�[2K�[32m|�[0m Collecting py-serializable (2.1.0)
�[2K�[32m|�[0m Auditing py-serializable (2.1.0)
�[2K�[32m|�[0m Collecting pycparser (3.0)
�[2K�[32m|�[0m Auditing pycparser (3.0)
�[2K�[32m|�[0m Collecting pydantic (2.13.4)
�[2K�[32m|�[0m Auditing pydantic (2.13.4)
�[2K�[32m|�[0m Collecting pydantic_core (2.46.4)
�[2K�[32m|�[0m Auditing pydantic_core (2.46.4)
�[2K�[32m|�[0m Collecting Pygments (2.20.0)
�[2K�[32m|�[0m Auditing Pygments (2.20.0)
�[2K�[32m|�[0m Collecting pyparsing (3.3.2)
�[2K�[32m|�[0m Auditing pyparsing (3.3.2)
�[2K�[32m|�[0m Collecting pyproject_hooks (1.2.0)
�[2K�[32m|�[0m Auditing pyproject_hooks (1.2.0)
�[2K�[32m|�[0m Collecting pytest (9.0.3)
�[2K�[32m|�[0m Auditing pytest (9.0.3)
�[2K�[32m|�[0m Collecting pytest-asyncio (1.4.0)
�[2K�[32m|�[0m Auditing pytest-asyncio (1.4.0)
�[2K�[32m|�[0m Collecting pytest-cov (7.1.0)
�[2K�[32m|�[0m Auditing pytest-cov (7.1.0)
�[2K�[32m|�[0m Collecting pytest-mock (3.15.1)
�[2K�[32m|�[0m Auditing pytest-mock (3.15.1)
�[2K�[32m|�[0m Collecting pytest-timeout (2.4.0)
�[2K�[32m|�[0m Auditing pytest-timeout (2.4.0)
�[2K�[32m|�[0m Collecting pytokens (0.4.1)
�[2K�[32m|�[0m Auditing pytokens (0.4.1)
�[2K�[32m|�[0m Collecting PyYAML (6.0.3)
�[2K�[32m|�[0m Auditing PyYAML (6.0.3)
�[2K�[32m|�[0m Collecting readme_renderer (44.0)
�[2K�[32m|�[0m Auditing readme_renderer (44.0)
�[2K�[32m|�[0m Collecting regex (2026.5.9)
�[2K�[32m|�[0m Auditing regex (2026.5.9)
�[2K�[32m|�[0m Collecting requests (2.34.2)
�[2K�[32m|�[0m Auditing requests (2.34.2)
�[2K�[32m|�[0m Collecting requests-toolbelt (1.0.0)
�[2K�[32m|�[0m Auditing requests-toolbelt (1.0.0)
�[2K�[32m|�[0m Collecting rfc3986 (2.0.0)
�[2K�[32m|�[0m Auditing rfc3986 (2.0.0)
�[2K�[32m|�[0m Collecting rich (15.0.0)
�[2K�[32m|�[0m Auditing rich (15.0.0)
�[2K�[32m|�[0m Collecting ruamel.yaml (0.19.1)
�[2K�[32m|�[0m Auditing ruamel.yaml (0.19.1)
�[2K�[32m|�[0m Collecting ruff (0.15.15)
�[2K�[32m|�[0m Auditing ruff (0.15.15)
�[2K�[32m|�[0m Collecting safety (3.8.0)
�[2K�[32m|�[0m Auditing safety (3.8.0)
�[2K�[32m|�[0m Collecting safety-schemas (0.0.16)
�[2K�[32m|�[0m Auditing safety-schemas (0.0.16)
�[2K�[32m|�[0m Collecting SecretStorage (3.5.0)
�[2K�[32m|�[0m Collecting SecretStorage (3.5.0)
�[2K�[32m|�[0m Auditing SecretStorage (3.5.0)
�[2K�[32m|�[0m Collecting setuptools (82.0.1)
�[2K�[32m|�[0m Auditing setuptools (82.0.1)
�[2K�[32m|�[0m Collecting shellingham (1.5.4)
�[2K�[32m|�[0m Auditing shellingham (1.5.4)
�[2K�[32m|�[0m Collecting sortedcontainers (2.4.0)
�[2K�[32m|�[0m Auditing sortedcontainers (2.4.0)
�[2K�[32m|�[0m Collecting stevedore (5.8.0)
�[2K�[32m|�[0m Auditing stevedore (5.8.0)
�[2K�[32m|�[0m Collecting tabulate (0.10.0)
�[2K�[32m|�[0m Auditing tabulate (0.10.0)
�[2K�[32m|�[0m Collecting tenacity (9.1.4)
�[2K�[32m|�[0m Auditing tenacity (9.1.4)
�[2K�[32m|�[0m Collecting termcolor (3.3.0)
�[2K�[32m|�[0m Auditing termcolor (3.3.0)
�[2K�[32m|�[0m Collecting tomli (2.4.1)
�[2K�[32m|�[0m Auditing tomli (2.4.1)
�[2K�[32m|�[0m Collecting tomli_w (1.2.0)
�[2K�[32m|�[0m Auditing tomli_w (1.2.0)
�[2K�[32m|�[0m Collecting tomlkit (0.15.0)
�[2K�[32m|�[0m Auditing tomlkit (0.15.0)
�[2K�[32m|�[0m Collecting tqdm (4.67.3)
�[2K�[32m|�[0m Auditing tqdm (4.67.3)
�[2K�[32m|�[0m Collecting truststore (0.10.4)
�[2K�[32m|�[0m Auditing truststore (0.10.4)
�[2K�[32m|�[0m Collecting twine (6.2.0)
�[2K�[32m|�[0m Auditing twine (6.2.0)
�[2K�[32m|�[0m Collecting typer (0.25.1)
�[2K�[32m|�[0m Auditing typer (0.25.1)
�[2K�[32m|�[0m Collecting types-PyYAML (6.0.12.20260518)
�[2K�[32m/�[0m Auditing types-PyYAML (6.0.12.20260518)
�[2K�[32m/�[0m Collecting typing_extensions (4.15.0)
�[2K�[32m/�[0m Auditing typing_extensions (4.15.0)
�[2K�[32m/�[0m Collecting typing-inspection (0.4.2)
�[2K�[32m/�[0m Auditing typing-inspection (0.4.2)
�[2K�[32m/�[0m Collecting urllib3 (2.7.0)
�[2K�[32m/�[0m Auditing urllib3 (2.7.0)
�[2K�[32m/�[0m Collecting wheel (0.47.0)
�[2K�[32m/�[0m Auditing wheel (0.47.0)
�[2K�[32m/�[0m Collecting yarl (1.24.2)
�[2K�[32m/�[0m Auditing yarl (1.24.2)
�[2K�[32m/�[0m Auditing yarl (1.24.2)
�[?25h
�[1A�[2K```

@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 0% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
versiontracker/ui.py 0.00% 3 Missing and 3 partials ⚠️
versiontracker/macos_integration.py 0.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b9d548d72

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

try:
# Escape double-quotes so user-controlled values cannot break out of
# the AppleScript string literal and inject arbitrary commands.
safe_message = message.replace('"', '\\"')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape backslashes before AppleScript quotes

When a user-controlled value contains a backslash immediately before a quote, this replacement still builds an unsafe AppleScript string: for example a message containing \" sound name "Funk" -- becomes \\" at the first quote, so AppleScript consumes the doubled backslash as a literal backslash and the quote can terminate the string. That leaves the injection issue reachable for message values (and the same escaping pattern is used for title/subtitle), so escape backslashes before quotes or avoid interpolating these values into AppleScript source.

Useful? React with 👍 / 👎.

@docdyhr docdyhr merged commit ed33828 into main May 29, 2026
23 of 24 checks passed
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The path traversal guard logic in save_filter, load_filter, and delete_filter is duplicated; consider extracting a small helper that takes a filter name and returns a validated Path, so future changes to the validation rules stay consistent in one place.
  • The AppleScript string escaping currently only replaces "; it would be safer and more maintainable to centralize this into an _escape_applescript_string helper that also handles backslashes and control characters, ensuring all osascript calls use the same robust escaping.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The path traversal guard logic in `save_filter`, `load_filter`, and `delete_filter` is duplicated; consider extracting a small helper that takes a filter name and returns a validated `Path`, so future changes to the validation rules stay consistent in one place.
- The AppleScript string escaping currently only replaces `"`; it would be safer and more maintainable to centralize this into an `_escape_applescript_string` helper that also handles backslashes and control characters, ensuring all osascript calls use the same robust escaping.

## Individual Comments

### Comment 1
<location path="versiontracker/ui.py" line_range="503-505" />
<code_context>
             safe_name = name.replace(" ", "-").replace("/", "_").lower()
             filter_path = self.filters_dir / f"{safe_name}.json"

+            # Guard against path traversal (e.g. "../../etc/passwd" survives the
+            # replace above because ".." contains no "/" or " ").
+            if not filter_path.resolve().is_relative_to(self.filters_dir.resolve()):
+                raise ValueError(f"Invalid filter name: {name!r}")
+
</code_context>
<issue_to_address>
**suggestion:** The path traversal guard logic is repeated across save/load/delete and could be factored into a helper.

The `safe_name` and `resolve().is_relative_to(...)` logic is repeated in `save_filter`, `load_filter`, and `delete_filter`. Consider extracting it into a helper (e.g. `_filter_path_for(name)` that returns a validated `Path` or raises) to keep the logic centralized and easier to evolve.

Suggested implementation:

```python
            filter_path = self._filter_path_for(name)

            # Write the filter data to a JSON file
            with open(filter_path, "w") as f:
                json.dump(filter_data, f, indent=2)

```

To fully implement the refactor across the file, you should:

1. **Add the `_filter_path_for` helper** as an instance method on the same class that owns `self.filters_dir`, for example:

   ```python
   def _filter_path_for(self, name: str) -> Path:
       """Return a validated filter path for the given filter name.

       This centralizes the safe name generation and path traversal guard.
       """
       safe_name = name.replace(" ", "-").replace("/", "_").lower()
       filter_path = self.filters_dir / f"{safe_name}.json"

       # Guard against path traversal (e.g. "../../etc/passwd" survives the
       # replace above because ".." contains no "/" or " ").
       if not filter_path.resolve().is_relative_to(self.filters_dir.resolve()):
           raise ValueError(f"Invalid filter name: {name!r}")

       return filter_path
   ```

   Place this method near the other filter-related methods to keep things organized.

2. **Update other call sites** (e.g. in `load_filter` and `delete_filter`) that currently duplicate:

   ```python
   safe_name = name.replace(" ", "-").replace("/", "_").lower()
   filter_path = self.filters_dir / f"{safe_name}.json"
   # and the associated resolve().is_relative_to(...) guard
   ```

   Replace those blocks with:

   ```python
   filter_path = self._filter_path_for(name)
   ```

This will ensure all filter path computation and validation logic is centralized in `_filter_path_for`, making future changes (e.g. different sanitization rules) straightforward.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread versiontracker/ui.py
Comment on lines +503 to +505
# Guard against path traversal (e.g. "../../etc/passwd" survives the
# replace above because ".." contains no "/" or " ").
if not filter_path.resolve().is_relative_to(self.filters_dir.resolve()):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The path traversal guard logic is repeated across save/load/delete and could be factored into a helper.

The safe_name and resolve().is_relative_to(...) logic is repeated in save_filter, load_filter, and delete_filter. Consider extracting it into a helper (e.g. _filter_path_for(name) that returns a validated Path or raises) to keep the logic centralized and easier to evolve.

Suggested implementation:

            filter_path = self._filter_path_for(name)

            # Write the filter data to a JSON file
            with open(filter_path, "w") as f:
                json.dump(filter_data, f, indent=2)

To fully implement the refactor across the file, you should:

  1. Add the _filter_path_for helper as an instance method on the same class that owns self.filters_dir, for example:

    def _filter_path_for(self, name: str) -> Path:
        """Return a validated filter path for the given filter name.
    
        This centralizes the safe name generation and path traversal guard.
        """
        safe_name = name.replace(" ", "-").replace("/", "_").lower()
        filter_path = self.filters_dir / f"{safe_name}.json"
    
        # Guard against path traversal (e.g. "../../etc/passwd" survives the
        # replace above because ".." contains no "/" or " ").
        if not filter_path.resolve().is_relative_to(self.filters_dir.resolve()):
            raise ValueError(f"Invalid filter name: {name!r}")
    
        return filter_path

    Place this method near the other filter-related methods to keep things organized.

  2. Update other call sites (e.g. in load_filter and delete_filter) that currently duplicate:

    safe_name = name.replace(" ", "-").replace("/", "_").lower()
    filter_path = self.filters_dir / f"{safe_name}.json"
    # and the associated resolve().is_relative_to(...) guard

    Replace those blocks with:

    filter_path = self._filter_path_for(name)

This will ensure all filter path computation and validation logic is centralized in _filter_path_for, making future changes (e.g. different sanitization rules) straightforward.

@docdyhr docdyhr deleted the fix/release-workflow-failures branch May 29, 2026 11:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant