diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0ea371b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +# Dependabot security updates were already on for this repo, but without this +# config there were no scheduled version-update PRs — so a dependency could sit +# on a vulnerable-but-unadvised release indefinitely. This adds weekly checks +# for both the Python deps and the GitHub Actions used by CI. +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: ["dependencies"] + groups: + # One PR for routine bumps; security fixes still arrive individually. + python-minor-and-patch: + update-types: ["minor", "patch"] + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: ["dependencies", "ci"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4952df7..95f6d9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python: ["3.10", "3.13"] + python: ["3.10", "3.13"] # matches requires-python floor runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 @@ -20,9 +20,26 @@ jobs: python-version: ${{ matrix.python }} - run: pip install -e '.[dev]' - run: pytest + - name: Lint + run: ruff check abapit tests + - name: Security lint + run: bandit -q -r abapit -ll - name: Demo smoke test run: | abapit export devices --demo | head -1 | grep serialNumber abapit changes --demo 2>&1 | grep -q "Need at least two snapshots" env: ABAPIT_DATA_DIR: ${{ runner.temp }}/abapit-data + + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + # Fails the build on a known-vulnerable dependency. Paired with the + # version floors in pyproject.toml, this is what stops the app shipping + # a CVE-carrying starlette/python-multipart again. + - run: pip install -e . pip-audit + - run: pip-audit --strict diff --git a/README.md b/README.md index 3ecaea4..1124c4f 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,18 @@ it to your Dock. For a standalone window with its own icon, open After pulling new code, run `abapit restart-app` so the service loads it. `abapit uninstall-app` removes both. -`install-app` reuses whatever Python you ran it with (it works against the -3.9 that ships with macOS Command Line Tools — abapit supports 3.9+). +`install-app` reuses whatever Python you ran it with, so it needs **Python +3.10+**. That floor is a security one, not a syntax one: every patched +`starlette` and `python-multipart` release requires 3.10, and the last +3.9-compatible versions carry unfixed CVEs. The 3.9 that ships with macOS +Command Line Tools is therefore not supported — use the self-contained +bundle below, which brings its own interpreter. ### Self-contained app (bundled Python) To get an `abapit.app` that depends on **no** system Python — so it runs on a -clean Mac, and isn't tied to Apple's aging 3.9 — build a bundle with its own -CPython: +clean Mac, with a current interpreter and patched dependencies — build a +bundle with its own CPython: ```sh scripts/build_app.sh # -> ~/Applications/abapit.app + login service @@ -222,6 +226,11 @@ profiles; switch from the header dropdown. - **Egress**: the only hosts ever contacted are `account.apple.com`, `api-business.apple.com` / `api-school.apple.com`, and — for a Mosyle org — `businessapi.mosyle.com` plus `businessapilogs.mosyle.com`. +- **Dependencies**: `pyproject.toml` carries security floors for the packages + that parse or transport untrusted input (`python-multipart`, `starlette`, + `cryptography`, `h2`). CI runs `pip-audit --strict`, `ruff`, and `bandit`, so + a newly-disclosed CVE in a dependency fails the build even if nothing in the + repo changed; Dependabot opens weekly update PRs on top of that. - **Honest limits**: anything running as *your user* can read the config and key files — the same trust model as `~/.ssh`; secrets are not in the Keychain. The Apple API account can read inventory, reassign devices, and diff --git a/pyproject.toml b/pyproject.toml index d766309..7ed5c7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,10 +8,12 @@ version = "0.2.0" description = "Apple Business API Tool — a local web GUI for the Apple Business and Apple School Manager APIs, built for Mac admins." readme = "README.md" license = { text = "MIT" } -# 3.9 is the Python that ships with macOS (Command Line Tools); abapit runs -# on it (no 3.10+ syntax/stdlib), so admins can install against the system -# Python without adding a newer one. -requires-python = ">=3.9" +# 3.10+ is a SECURITY floor, not a syntax one. Every patched starlette and +# python-multipart release requires >=3.10; the last 3.9-compatible versions +# (starlette 0.49.3, python-multipart 0.0.20) carry unfixed CVEs. macOS ships +# 3.9, but abapit no longer needs it — scripts/build_app.sh bundles its own +# CPython, so the supported install has a current interpreter either way. +requires-python = ">=3.10" authors = [{ name = "abapit contributors" }] keywords = ["apple", "abm", "asm", "mdm", "macadmin", "apple-business-manager"] classifiers = [ @@ -20,19 +22,27 @@ classifiers = [ "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: System :: Systems Administration", ] dependencies = [ - "fastapi>=0.110", + "fastapi>=0.121", "uvicorn>=0.29", "jinja2>=3.1", "httpx[http2]>=0.27", "PyJWT[crypto]>=2.8", - "python-multipart>=0.0.9", + # Security floors (pip-audit). These parse or transport untrusted input, so + # they are pinned above the advisories rather than left to transitive luck: + # python-multipart <0.0.31 - multipart form parsing DoS chain + # starlette <1.3.1 - request handling advisories + # cryptography <50.0.0 - PYSEC-2026-3552 + # h2 <4.4.1 - PYSEC-2026-3628 (HTTP/2, enabled in client.py) + "python-multipart>=0.0.31", + "starlette>=1.3.1", + "cryptography>=50.0.0", + "h2>=4.4.1", ] [project.optional-dependencies] @@ -53,7 +63,7 @@ packages = ["abapit"] # for best-effort launchctl calls — are left out on purpose. A linter that # cries wolf gets ignored, which defeats the point of having one. [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 100 [tool.ruff.lint]