This is an automated email from the ASF dual-hosted git repository. sbp pushed a commit to branch ssh-audit-677 in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit a281800dd7dddd3af03f93034561b5543b5610ad Author: Sean B. Palmer <[email protected]> AuthorDate: Tue Feb 17 15:17:56 2026 +0000 Use the intersection of algorithms from asyncssh and ssh-audit --- atr/ssh.py | 18 ++++++++++++++++++ atr/util.py | 39 +++++++++++++++++++++++---------------- pyproject.toml | 1 + requirements-for-pip-audit.txt | 2 ++ uv.lock | 13 ++++++++++++- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/atr/ssh.py b/atr/ssh.py index b04031a7..673d2203 100644 --- a/atr/ssh.py +++ b/atr/ssh.py @@ -30,6 +30,10 @@ from typing import Any, Final import aiofiles import aiofiles.os import asyncssh +import asyncssh.encryption as encryption +import asyncssh.kex as kex +import asyncssh.mac as mac +import ssh_audit.builtin_policies as builtin_policies import atr.attestable as attestable import atr.config as config @@ -43,6 +47,17 @@ import atr.util as util _CONFIG: Final = config.get() +_SSH_AUDIT_POLICY: Final = builtin_policies.BUILTIN_POLICIES["Hardened OpenSSH Server v9.9 (version 1)"] + +_ASYNCSSH_SUPPORTED_ENC: Final = {bytes(a) for a in encryption.get_encryption_algs()} +_ASYNCSSH_SUPPORTED_KEX: Final = {bytes(a) for a in kex.get_kex_algs()} +_ASYNCSSH_SUPPORTED_MAC: Final = {bytes(a) for a in mac.get_mac_algs()} + + +_APPROVED_CIPHERS: Final = util.intersect_algs(_SSH_AUDIT_POLICY, "ciphers", _ASYNCSSH_SUPPORTED_ENC) +_APPROVED_KEX: Final = util.intersect_algs(_SSH_AUDIT_POLICY, "kex", _ASYNCSSH_SUPPORTED_KEX) +_APPROVED_MACS: Final = util.intersect_algs(_SSH_AUDIT_POLICY, "macs", _ASYNCSSH_SUPPORTED_MAC) + class RsyncArgsError(Exception): """Exception raised when the rsync arguments are invalid.""" @@ -178,6 +193,9 @@ async def server_start() -> asyncssh.SSHAcceptor: host=_CONFIG.SSH_HOST, port=_CONFIG.SSH_PORT, encoding=None, + encryption_algs=_APPROVED_CIPHERS, + kex_algs=_APPROVED_KEX, + mac_algs=_APPROVED_MACS, ) log.info(f"SSH server started on {_CONFIG.SSH_HOST}:{_CONFIG.SSH_PORT}") diff --git a/atr/util.py b/atr/util.py index e03ef41e..3104fefa 100644 --- a/atr/util.py +++ b/atr/util.py @@ -190,22 +190,6 @@ async def async_temporary_directory( log.exception(f"Failed to remove temporary directory {temp_dir_path}") -async def atomic_write_file(file_path: pathlib.Path, content: str, encoding: str = "utf-8") -> None: - """Atomically write content to a file using a temporary file.""" - await aiofiles.os.makedirs(file_path.parent, exist_ok=True) - temp_path = file_path.parent / f".{file_path.name}.{uuid.uuid4()}.tmp" - try: - async with aiofiles.open(temp_path, "w", encoding=encoding) as f: - await f.write(content) - await f.flush() - await asyncio.to_thread(os.fsync, f.fileno()) - await aiofiles.os.rename(temp_path, file_path) - except Exception: - with contextlib.suppress(FileNotFoundError): - await aiofiles.os.remove(temp_path) - raise - - async def atomic_modify_file( file_path: pathlib.Path, modify: Callable[[str], str], @@ -227,6 +211,22 @@ async def atomic_modify_file( await asyncio.to_thread(os.close, lock_fd) +async def atomic_write_file(file_path: pathlib.Path, content: str, encoding: str = "utf-8") -> None: + """Atomically write content to a file using a temporary file.""" + await aiofiles.os.makedirs(file_path.parent, exist_ok=True) + temp_path = file_path.parent / f".{file_path.name}.{uuid.uuid4()}.tmp" + try: + async with aiofiles.open(temp_path, "w", encoding=encoding) as f: + await f.write(content) + await f.flush() + await asyncio.to_thread(os.fsync, f.fileno()) + await aiofiles.os.rename(temp_path, file_path) + except Exception: + with contextlib.suppress(FileNotFoundError): + await aiofiles.os.remove(temp_path) + raise + + def chmod_directories(path: pathlib.Path, permissions: int = DIRECTORY_PERMISSIONS) -> None: # codeql[py/overly-permissive-file] os.chmod(path, permissions) @@ -621,6 +621,13 @@ async def has_files(release: sql.Release) -> bool: return False +def intersect_algs(policy: dict[str, Any], policy_key: str, supported: set[bytes]) -> list[str]: + algs = policy[policy_key] + if not isinstance(algs, list): + raise TypeError(f"ssh-audit policy '{policy_key}' is not a list") + return [a for a in algs if isinstance(a, str) and (a.encode("ascii") in supported)] + + def is_dev_environment() -> bool: conf = config.get() for development_host in ("127.0.0.1", "atr", "atr-dev", "localhost.apache.org"): diff --git a/pyproject.toml b/pyproject.toml index e9baf395..776ffe55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dependencies = [ "rich~=14.0.0", "semver>=3.0.4", "sqlmodel~=0.0.24", + "ssh-audit>=3.3.0", "standard-imghdr>=3.13.0", "strictyaml>=1.7.3", "structlog>=25.5.0", diff --git a/requirements-for-pip-audit.txt b/requirements-for-pip-audit.txt index b48a0135..13bb9880 100644 --- a/requirements-for-pip-audit.txt +++ b/requirements-for-pip-audit.txt @@ -326,6 +326,8 @@ sqlalchemy==2.0.46 # sqlmodel sqlmodel==0.0.34 # via tooling-trusted-releases +ssh-audit==3.3.0 + # via tooling-trusted-releases standard-imghdr==3.13.0 # via tooling-trusted-releases strictyaml==1.7.3 diff --git a/uv.lock b/uv.lock index efa461cb..45991c4e 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = "==3.13.*" [options] -exclude-newer = "2026-02-17T14:20:34Z" +exclude-newer = "2026-02-17T14:22:10Z" [[package]] name = "aiofiles" @@ -1837,6 +1837,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/ee/1910f4eee41af4268b0d8cd688a05fb8ea23e9e6c64b8710592df24a8c66/sqlmodel-0.0.34-py3-none-any.whl", hash = "sha256:aeabc8f0de32076a0ed9216e88568459d737fca1e7133bfc6d1c657920789a2d", size = 27445, upload-time = "2026-02-16T19:06:35.709Z" }, ] +[[package]] +name = "ssh-audit" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/ec/e89fdfaaa6f08813e1a5cf926bc0dc155761144ebcac57191b4c8001aae3/ssh_audit-3.3.0.tar.gz", hash = "sha256:b76e36ac9844f45d64986c9f293a4b46766a10412dc29fb43bd52d0f6661a5b0", size = 116958, upload-time = "2024-10-15T21:08:28.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/21/2b3cd275bc3c09fc765691f6c005a6683edf47f47c18d2f9eb78c39ca7e6/ssh_audit-3.3.0-py3-none-any.whl", hash = "sha256:8d2b22b7bb7c20c9d84452c9f13408015ec76eb025379cc70335e5315e4378d6", size = 121789, upload-time = "2024-10-15T21:08:27.182Z" }, +] + [[package]] name = "standard-imghdr" version = "3.13.0" @@ -1920,6 +1929,7 @@ dependencies = [ { name = "rich" }, { name = "semver" }, { name = "sqlmodel" }, + { name = "ssh-audit" }, { name = "standard-imghdr" }, { name = "strictyaml" }, { name = "structlog" }, @@ -1984,6 +1994,7 @@ requires-dist = [ { name = "rich", specifier = "~=14.0.0" }, { name = "semver", specifier = ">=3.0.4" }, { name = "sqlmodel", specifier = "~=0.0.24" }, + { name = "ssh-audit", specifier = ">=3.3.0" }, { name = "standard-imghdr", specifier = ">=3.13.0" }, { name = "strictyaml", specifier = ">=1.7.3" }, { name = "structlog", specifier = ">=25.5.0" }, --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
