Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## v3.8.5 (2026-10-21)

### Security

- Added `certifi>=2026.10.19` as a declared dependency so the CA bundle used by `requests` for stackql binary downloads no longer trusts `emSign ECC Root CA - G3`, the eMudhra root whose eight intermediate CA private keys were found to be recoverable in October 2026 (Mozilla Bugzilla 2083449, certifi advisory GHSA-6q48-6c7w-9gw2). pystackql ships no CA material of its own; this floor ensures fresh installs and upgrades resolve a patched bundle.
- Added `tests/test_tls_trust.py` to assert the resolved certifi bundle excludes the compromised root (by certificate fingerprint) and that downloads never disable certificate verification.

## v3.8.4 (2026-06-29)

### Updates
Expand Down
29 changes: 29 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
**Note**:

- All contributions to `pystackql`, through any means, are subject to the [license](/LICENSE).
- This document itself and the patterns specified herein, are heavily influenced by [redis](https://github.com/redis/redis). We profusely thank the maintainers of same, from which we draw inspiration.
- For useful information contributors **may** want to consider in developing `pystackql`, please see the "Building the package", "Testing Locally" and "Building the docs" sections of the [README](/README.rst), the [tests README](/tests/README.md), and the published documentation at [pystackql.readthedocs.io](https://pystackql.readthedocs.io).
- All contributions to `pystackql` and actions related, through any means, are subject to the StackQL [CODE OF CONDUCT](https://github.com/stackql/stackql/blob/main/CODE_OF_CONDUCT.md).

# Contribution flow

Before beginning implementation work, please raise an issue (using the bug report, feature request or question template as appropriate) and receive a consensus that work should proceed.

Please raise pull requests (PRs) from a fork of this repository. The richer the detail / evidence supplied, the easier the review process. A usefully named feature branch for the PR source is appreciated.

PRs against `main` run the test suite on Linux, macOS and Windows across Python 3.9 to 3.13. Please keep changes working on all of them.

## Development notes

- `pystackql` is a Python wrapper around the `stackql` engine. Changes to the engine itself belong in [stackql/stackql](https://github.com/stackql/stackql), and provider definitions belong in the [stackql-provider-registry](https://github.com/stackql/stackql-provider-registry).
- Run the non-server tests with `python run_tests.py`. Server-mode tests need a running `stackql srv` process (see `start-stackql-server.sh` or `start-stackql-server.ps1`), then `python run_server_tests.py`.
- Tests use the no-auth Homebrew provider and provider-agnostic literal queries. Please do not add tests that require cloud credentials.
- Runtime dependencies are declared in `pyproject.toml` as minimum versions rather than pins. Please keep it that way and avoid unrelated dependency bumps.
- `README.rst` is reStructuredText and doubles as the PyPI readme. The documentation in `docs/` is built with Sphinx from the docstrings, so update the `StackQL.__init__` docstring when changing constructor parameters.
- Releases bump `version` in `pyproject.toml` and add an entry to `CHANGELOG.md`.

Finally, in respect of all your efforts; we thank you and ask that you please give us a star while you are at it.

Many Thanks,

The StackQL team.
42 changes: 42 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Security Policy

The StackQL team takes the security of `pystackql` seriously. We appreciate everyone who reports vulnerabilities responsibly, and we will do our best to acknowledge and address valid reports promptly.

## Reporting a Vulnerability

Please do not open a public GitHub issue for security reports. A public issue discloses the problem before a fix is available and puts users at risk.

Instead, report privately using GitHub private vulnerability reporting:

1. Open the [new advisory form](https://github.com/stackql/pystackql/security/advisories/new) (Security tab -> Advisories -> "Report a vulnerability").
2. Provide as much detail as you can: affected `pystackql` version(s), Python version and operating system, reproduction steps, impact, and any suggested remediation.
3. Submit. This opens a private advisory thread visible only to you and the maintainers.

If you cannot use GitHub private vulnerability reporting, email us at [info@stackql.io](mailto:info@stackql.io). Please put "SECURITY" in the subject line and treat the contents as confidential.

## Supported Versions

Security fixes are applied to the latest released minor line, published to [PyPI](https://pypi.org/project/pystackql/). Older lines are supported on a best-effort basis only; we recommend upgrading to the latest release with `pip install --upgrade pystackql`. See the [changelog](/CHANGELOG.md) and the [PyPI release history](https://pypi.org/project/pystackql/#history) for the current version.

| Version | Supported |
| ------- | ------------ |
| 3.8.x | Yes |
| < 3.8 | Best-effort |

`pystackql` supports Python 3.9 to 3.13 on Linux, macOS and Windows.

## Response Expectations

These are targets, not contractual guarantees:

- We aim to acknowledge a report within a few business days.
- We will keep you updated as we investigate and work on a fix.
- We will coordinate disclosure timing with you once a fix or mitigation is ready.

## Disclosure Policy

We follow coordinated (responsible) disclosure. Please give us a reasonable opportunity to release a fix before any public disclosure. We are happy to credit reporters in the advisory and release notes, unless you prefer to remain anonymous.

## Scope

This policy covers the `pystackql` Python package in this repository as published to PyPI, including its Jupyter magic extensions. `pystackql` downloads and runs the `stackql` engine binary in local mode and connects to a `stackql` server in server mode; the engine, its binaries and its Docker images are maintained in [stackql/stackql](https://github.com/stackql/stackql) and covered by that repository's [security policy](https://github.com/stackql/stackql/blob/main/SECURITY.md). Provider definitions are maintained separately in the [stackql-provider-registry](https://github.com/stackql/stackql-provider-registry). Please report issues specific to those in their respective repositories.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pystackql"
version = "3.8.4"
version = "3.8.5"
description = "A Python interface for StackQL"
readme = "README.rst"
authors = [
Expand All @@ -25,6 +25,7 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"requests",
"certifi>=2026.10.19",
"pandas",
"IPython",
"psycopg[binary]>=3.1.0",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Core dependencies
pandas>=1.3.0
requests>=2.25.0
certifi>=2026.10.19 # CA bundle without the compromised emSign ECC Root CA - G3 (eMudhra incident, Oct 2026)
IPython>=7.0.0
termcolor>=1.1.0
PyYAML>=5.4.0
Expand Down
3 changes: 2 additions & 1 deletion run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def main():
"tests/test_output_formats.py",
"tests/test_magic.py",
"tests/test_async.py",
"tests/test_download.py"
"tests/test_download.py",
"tests/test_tls_trust.py"
])

# Run pytest with the arguments
Expand Down
110 changes: 110 additions & 0 deletions tests/test_tls_trust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# tests/test_tls_trust.py

"""
TLS trust store tests for PyStackQL.

PyStackQL makes one outbound HTTPS call (downloading the stackql binary from
releases.stackql.io via `requests`), and `requests` validates server
certificates against the CA bundle shipped by `certifi`. These tests guard
against resolving a certifi bundle that still trusts a compromised root.

Background: in October 2026 the private keys of eight eMudhra intermediate CAs
chaining to "emSign ECC Root CA - G3" were found to be trivially recoverable
(Mozilla Bugzilla 2083449). certifi removed that root in 2026.10.18 and
restored the unaffected emSign roots in 2026.10.19 (GHSA-6q48-6c7w-9gw2).
"""

import hashlib
import os
import re
import ssl
import sys
from unittest.mock import patch, MagicMock

import certifi
import requests

# Add the parent directory to the path so we can import from pystackql
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from pystackql.utils import download

# Minimum certifi release that distrusts the compromised root while keeping the
# unaffected emSign roots. Must match the floor declared in pyproject.toml.
MIN_CERTIFI_VERSION = (2026, 10, 19)

# SHA-256 fingerprint of "emSign ECC Root CA - G3", the root that signed the
# compromised eMudhra intermediates.
DISTRUSTED_ROOT_SHA256 = (
"86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b"
)

_PEM_BLOCK = re.compile(
r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", re.S
)


def _bundle_fingerprints(path):
"""Return the set of SHA-256 fingerprints (hex) of every cert in a PEM bundle."""
with open(path, "r", encoding="utf-8") as f:
data = f.read()
fingerprints = set()
for block in _PEM_BLOCK.findall(data):
der = ssl.PEM_cert_to_DER_cert(block)
fingerprints.add(hashlib.sha256(der).hexdigest())
return fingerprints


class TestCertifiTrustStore:
"""The resolved certifi bundle must not trust the compromised root."""

def test_certifi_meets_minimum_version(self):
"""certifi must be at least the first release that distrusts the root
while retaining the unaffected emSign roots."""
installed = tuple(int(p) for p in certifi.__version__.split("."))
assert installed >= MIN_CERTIFI_VERSION, (
f"certifi {certifi.__version__} is older than "
f"{'.'.join(map(str, MIN_CERTIFI_VERSION))}; its CA bundle still "
"trusts emSign ECC Root CA - G3"
)

def test_bundle_is_non_empty_and_parseable(self):
"""Sanity check so the fingerprint test cannot pass on an empty bundle."""
fingerprints = _bundle_fingerprints(certifi.where())
assert len(fingerprints) > 50

def test_bundle_excludes_compromised_emsign_root(self):
"""emSign ECC Root CA - G3 must be absent from the bundle, checked by
certificate fingerprint rather than by the human-readable label."""
fingerprints = _bundle_fingerprints(certifi.where())
assert DISTRUSTED_ROOT_SHA256 not in fingerprints, (
"certifi bundle still contains emSign ECC Root CA - G3 "
f"({certifi.where()})"
)


class TestRequestsUsesCertifi:
"""The HTTPS client used for downloads must validate against certifi."""

def test_requests_default_ca_bundle_is_certifi(self):
"""requests must resolve its default CA bundle from certifi, so the
certifi floor is what governs the download's trust store."""
assert os.path.realpath(requests.certs.where()) == \
os.path.realpath(certifi.where())

def test_download_file_does_not_disable_verification(self, tmp_path):
"""download_file must never opt out of certificate verification."""
fake_response = MagicMock()
fake_response.headers = {'content-length': '4'}
fake_response.iter_content.return_value = [b'data']
fake_response.raise_for_status.return_value = None

dest = os.path.join(str(tmp_path), 'out.bin')
with patch('pystackql.utils.download.requests.get',
return_value=fake_response) as mock_get:
download.download_file(
'https://releases.stackql.io/stackql/latest/x.zip',
dest, showprogress=False)

assert mock_get.called
assert mock_get.call_args.kwargs.get('verify', True) is not False
Loading