Skip to content
Merged
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
46 changes: 46 additions & 0 deletions src/capiscio/manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
import hashlib
import platform
import stat
import shutil
Comment on lines 2 to 6
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

sys and shutil appear to be unused in this module. Since this PR is already touching the import block, consider removing unused imports to avoid lint/IDE warnings and keep dependencies clear.

Suggested change
import sys
import hashlib
import platform
import stat
import shutil
import hashlib
import platform
import stat

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -68,6 +69,34 @@ def get_binary_path(version: str) -> Path:
# For now, let's put it in a versioned folder
return get_cache_dir() / version / filename

def _fetch_expected_checksum(version: str, filename: str) -> Optional[str]:
"""Fetch the expected SHA-256 checksum from the release checksums.txt."""
url = f"https://github.com/{GITHUB_REPO}/releases/download/v{version}/checksums.txt"
try:
resp = requests.get(url, timeout=30)
resp.raise_for_status()
for line in resp.text.strip().splitlines():
parts = line.split()
if len(parts) == 2 and parts[1] == filename:
return parts[0]
logger.warning(f"Binary {filename} not found in checksums.txt")
return None
Comment on lines +82 to +83
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

If checksums.txt is successfully fetched but doesn’t contain an entry for the expected binary filename, the code currently logs a warning and disables verification by returning None. Given the file exists, missing the entry is suspicious and should likely be treated as an integrity failure (abort install) rather than silently proceeding with an unverified binary.

Suggested change
logger.warning(f"Binary {filename} not found in checksums.txt")
return None
# If checksums.txt was fetched successfully but does not contain an entry
# for the expected binary filename, treat this as an integrity failure
# rather than silently proceeding without verification.
logger.error(f"Binary {filename} not found in checksums.txt")
raise RuntimeError(
f"Integrity check failed: expected checksum entry for {filename} "
f"not found in checksums.txt for version v{version}"
)

Copilot uses AI. Check for mistakes.
except requests.exceptions.RequestException as e:
logger.warning(f"Could not fetch checksums.txt: {e}")
return None

Comment on lines +78 to +87
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

_fetch_expected_checksum treats any RequestException (timeouts, DNS issues, connection errors, etc.) as “checksums not available” and continues without verification. That makes the integrity check bypassable if an attacker or network issue blocks the checksum fetch. Consider only falling back when the HTTP status is 404/410, and otherwise failing closed (or making this behavior explicitly configurable).

Suggested change
for line in resp.text.strip().splitlines():
parts = line.split()
if len(parts) == 2 and parts[1] == filename:
return parts[0]
logger.warning(f"Binary {filename} not found in checksums.txt")
return None
except requests.exceptions.RequestException as e:
logger.warning(f"Could not fetch checksums.txt: {e}")
return None
except requests.exceptions.HTTPError as e:
status_code = getattr(e.response, "status_code", None)
# Treat missing checksum file as "no checksum available" and continue without verification.
if status_code in (404, 410):
logger.warning(f"checksums.txt not found at {url} (status {status_code}); "
"continuing without checksum verification.")
return None
# For other HTTP errors, fail closed.
logger.error(f"Failed to fetch checksums.txt from {url}: {e}")
raise RuntimeError("Unable to fetch checksums for capiscio-core binary.") from e
except requests.exceptions.RequestException as e:
# Network/transport errors (timeouts, DNS issues, etc.) should cause a hard failure.
logger.error(f"Network error while fetching checksums.txt from {url}: {e}")
raise RuntimeError("Network error while fetching checksums for capiscio-core binary.") from e
for line in resp.text.strip().splitlines():
parts = line.split()
if len(parts) == 2 and parts[1] == filename:
return parts[0]
logger.warning(f"Binary {filename} not found in checksums.txt")
return None

Copilot uses AI. Check for mistakes.
def _verify_checksum(file_path: Path, expected_hash: str) -> bool:
"""Verify SHA-256 checksum of a downloaded file."""
sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
actual = sha256.hexdigest()
if actual != expected_hash:
logger.error(f"Checksum mismatch: expected {expected_hash}, got {actual}")
return False
return True

def download_binary(version: str) -> Path:
"""
Download the binary for the current platform and version.
Expand Down Expand Up @@ -110,6 +139,23 @@ def download_binary(version: str) -> Path:
st = os.stat(target_path)
os.chmod(target_path, st.st_mode | stat.S_IEXEC)

# Verify checksum integrity
expected_hash = _fetch_expected_checksum(version, filename)
if expected_hash is not None:
if not _verify_checksum(target_path, expected_hash):
target_path.unlink()
Comment on lines +142 to +146
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

New behavior (fetching checksums.txt, computing SHA-256, and failing on mismatch) isn’t covered by tests in this PR. Since this code path is security-critical and also adds a second requests.get call, please add/update unit tests to cover: checksum file present + match, present + mismatch (deletes file + raises), and missing checksums (warns + continues).

Copilot uses AI. Check for mistakes.
raise RuntimeError(
f"Binary integrity check failed for {filename}. "
"The downloaded file does not match the published checksum. "
"This may indicate a tampered or corrupted download."
)
logger.info(f"Checksum verified for {filename}")
else:
logger.warning(
"Could not verify binary integrity (checksums.txt not available). "
"Consider upgrading capiscio-core to a version that publishes checksums."
)

console.print(f"[green]Successfully installed CapiscIO Core v{version}[/green]")
return target_path

Expand Down
Loading