Skip to content

Commit d672eaa

Browse files
committed
Merge remote-tracking branch 'origin/main' into lelia/ce-225-cli-bump-socketdev-3.2.0
# Conflicts: # CHANGELOG.md # pyproject.toml # socketsecurity/__init__.py # uv.lock
2 parents 48ebb15 + 7d7ac0c commit d672eaa

12 files changed

Lines changed: 416 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## 2.4.1
3+
## 2.4.3
44

55
### Changed: Bump required SDK version to `>=3.2.0`
66

@@ -9,6 +9,36 @@
99
"Unknown SocketCategory" warning fallback (SDK PR #85).
1010
- No CLI logic changes.
1111

12+
## 2.4.2
13+
14+
### Added: reachability flag and Coana environment alignment with the Node CLI
15+
16+
- New `--reach-disable-external-tool-checks` flag (passes `--disable-external-tool-checks`
17+
to the Coana CLI).
18+
- New `--reach-debug` flag to enable Coana debug output (`--debug`) independently of the
19+
global `--enable-debug`.
20+
- Node-style `--reach-analysis-timeout` and `--reach-analysis-memory-limit` are now the
21+
primary flag names; the previous `--reach-timeout` / `--reach-memory-limit` continue to
22+
work as hidden aliases.
23+
- The Coana subprocess now receives `SOCKET_CLI_VERSION` and `SOCKET_CALLER_USER_AGENT` so
24+
calls are attributed to the Python CLI. Proxies continue to work via the inherited
25+
`HTTPS_PROXY` / `HTTP_PROXY` environment variables, which Coana reads itself.
26+
- `SOCKET_REPO_NAME` / `SOCKET_BRANCH_NAME` are no longer forwarded to Coana when the repo
27+
and branch are the default sentinels, avoiding cross-run reachability cache-bucket
28+
collisions.
29+
- Tier 1 reachability finalize now retries with exponential backoff instead of giving up on
30+
the first transient error.
31+
32+
## 2.4.1
33+
34+
### Added: pyenv in the Docker image
35+
36+
- The `socketdev/cli` Docker image now bundles [pyenv](https://github.com/pyenv/pyenv)
37+
(pinned to `v2.7.1`) along with the Alpine build dependencies needed to compile
38+
CPython from source, so the image can build/install arbitrary Python versions on
39+
demand.
40+
- The CLI itself is unchanged — this release only affects the published Docker image.
41+
1242
## 2.4.0
1343

1444
### Changed: license details are no longer requested on the full-scan diff

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,29 @@ ENV GOPATH="/go"
8888
# Install uv
8989
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
9090

91+
# Install pyenv
92+
# pyenv lets us build/install arbitrary Python versions on demand. We install
93+
# the build dependencies needed to compile CPython on Alpine, then install
94+
# pyenv itself. We deliberately only symlink the `pyenv` binary onto the PATH
95+
# and do NOT add pyenv's shims directory, so its shims don't shadow the system
96+
# Python that the CLI runs on.
97+
RUN apk add --no-cache \
98+
bash \
99+
bzip2-dev \
100+
ca-certificates \
101+
libffi-dev \
102+
libxslt-dev \
103+
linux-headers \
104+
ncurses-dev \
105+
openssl-dev \
106+
readline-dev \
107+
sqlite-dev \
108+
xz-dev \
109+
zlib-dev
110+
RUN curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | PYENV_GIT_TAG="v2.7.1" bash && \
111+
ln -s ~/.pyenv/bin/pyenv /bin/pyenv && \
112+
pyenv --version
113+
91114
# Install CLI based on build mode
92115
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
93116
echo "Using local development install"; \

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.4.1"
9+
version = "2.4.3"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.4.1'
2+
__version__ = '2.4.3'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

socketsecurity/config.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ class CliConfig:
139139
reach_continue_on_install_errors: bool = False
140140
reach_continue_on_missing_lock_files: bool = False
141141
reach_continue_on_no_source_files: bool = False
142+
reach_debug: bool = False
143+
reach_disable_external_tool_checks: bool = False
142144
max_purl_batch_size: int = 5000
143145
enable_commit_status: bool = False
144146
legal: bool = False
@@ -267,6 +269,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
267269
'reach_continue_on_install_errors': args.reach_continue_on_install_errors,
268270
'reach_continue_on_missing_lock_files': args.reach_continue_on_missing_lock_files,
269271
'reach_continue_on_no_source_files': args.reach_continue_on_no_source_files,
272+
'reach_debug': args.reach_debug,
273+
'reach_disable_external_tool_checks': args.reach_disable_external_tool_checks,
270274
'max_purl_batch_size': args.max_purl_batch_size,
271275
'enable_commit_status': args.enable_commit_status,
272276
'legal': args.legal or args.legal_format == "fossa",
@@ -878,18 +882,32 @@ def create_argument_parser() -> argparse.ArgumentParser:
878882
help="Specific version of @coana-tech/cli to use (e.g., '1.2.3')"
879883
)
880884
reachability_group.add_argument(
881-
"--reach-timeout",
885+
"--reach-analysis-timeout",
882886
dest="reach_analysis_timeout",
883887
type=int,
884888
metavar="<seconds>",
885889
help="Timeout for reachability analysis in seconds"
886890
)
891+
# Backwards-compatible alias for the pre-alignment name. Kept working, hidden from help.
887892
reachability_group.add_argument(
888-
"--reach-memory-limit",
893+
"--reach-timeout",
894+
dest="reach_analysis_timeout",
895+
type=int,
896+
help=argparse.SUPPRESS
897+
)
898+
reachability_group.add_argument(
899+
"--reach-analysis-memory-limit",
889900
dest="reach_analysis_memory_limit",
890901
type=int,
891902
metavar="<mb>",
892-
help="Memory limit for reachability analysis in MB"
903+
help="Memory limit for reachability analysis in MB (defaults to the coana CLI's own default, currently 8192)"
904+
)
905+
# Backwards-compatible alias for the pre-alignment name. Kept working, hidden from help.
906+
reachability_group.add_argument(
907+
"--reach-memory-limit",
908+
dest="reach_analysis_memory_limit",
909+
type=int,
910+
help=argparse.SUPPRESS
893911
)
894912
reachability_group.add_argument(
895913
"--reach-ecosystems",
@@ -957,7 +975,7 @@ def create_argument_parser() -> argparse.ArgumentParser:
957975
dest="reach_concurrency",
958976
type=int,
959977
metavar="<number>",
960-
help="Concurrency level for reachability analysis (must be >= 1)"
978+
help="Concurrency level for reachability analysis (must be >= 1; defaults to the coana CLI's own default, currently 1)"
961979
)
962980
reachability_group.add_argument(
963981
"--reach-additional-params",
@@ -1002,6 +1020,20 @@ def create_argument_parser() -> argparse.ArgumentParser:
10021020
action="store_true",
10031021
help=argparse.SUPPRESS
10041022
)
1023+
reachability_group.add_argument(
1024+
"--reach-debug",
1025+
dest="reach_debug",
1026+
action="store_true",
1027+
help="Enable debug output for the reachability analysis (passes --debug to the coana CLI). "
1028+
"Independent of the global --enable-debug flag."
1029+
)
1030+
reachability_group.add_argument(
1031+
"--reach-disable-external-tool-checks",
1032+
dest="reach_disable_external_tool_checks",
1033+
action="store_true",
1034+
help="Disable coana's external tool availability checks during reachability analysis "
1035+
"(passes --disable-external-tool-checks to the coana CLI)."
1036+
)
10051037

10061038
parser.add_argument(
10071039
'--version',

socketsecurity/core/__init__.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
# Stream the facts file in 1 MiB chunks so large files aren't held fully in memory.
7272
SOCKET_FACTS_BROTLI_CHUNK_SIZE = 1024 * 1024
7373

74+
# Tier 1 reachability finalize retry policy. The finalize call links the tier1 scan to the
75+
# full scan and can fail transiently (network/API blips); a few backoff retries make it robust.
76+
TIER1_FINALIZE_MAX_ATTEMPTS = 3
77+
TIER1_FINALIZE_BACKOFF_SECONDS = 1.0
78+
7479

7580
def _humanize_alert_type(alert_type: str) -> str:
7681
"""Convert a camelCase/PascalCase alert type into a Title-Cased label.
@@ -549,20 +554,43 @@ def finalize_tier1_scan(self, full_scan_id: str, facts_file_path: str) -> bool:
549554
log.debug(f"Failed to read tier1ReachabilityScanId from {facts_file_path}: {e}")
550555
return False
551556

552-
# Call the SDK to finalize the tier 1 scan
553-
try:
554-
success = self.sdk.fullscans.finalize_tier1(
555-
full_scan_id=full_scan_id,
556-
tier1_reachability_scan_id=tier1_scan_id,
557-
)
557+
# Call the SDK to finalize the tier 1 scan, retrying transient failures with backoff.
558+
last_error: Optional[Exception] = None
559+
for attempt in range(1, TIER1_FINALIZE_MAX_ATTEMPTS + 1):
560+
try:
561+
success = self.sdk.fullscans.finalize_tier1(
562+
full_scan_id=full_scan_id,
563+
tier1_reachability_scan_id=tier1_scan_id,
564+
)
558565

559-
if success:
560-
log.debug(f"Successfully finalized tier 1 scan {tier1_scan_id} for full scan {full_scan_id}")
561-
return success
566+
if success:
567+
log.debug(f"Successfully finalized tier 1 scan {tier1_scan_id} for full scan {full_scan_id}")
568+
return True
562569

563-
except Exception as e:
564-
log.debug(f"Unable to finalize tier 1 scan: {e}")
565-
return False
570+
log.debug(
571+
f"finalize_tier1 returned a falsy result for scan {tier1_scan_id} "
572+
f"(attempt {attempt}/{TIER1_FINALIZE_MAX_ATTEMPTS})"
573+
)
574+
except Exception as e:
575+
last_error = e
576+
log.debug(
577+
f"Unable to finalize tier 1 scan (attempt {attempt}/{TIER1_FINALIZE_MAX_ATTEMPTS}): {e}"
578+
)
579+
580+
if attempt < TIER1_FINALIZE_MAX_ATTEMPTS:
581+
time.sleep(TIER1_FINALIZE_BACKOFF_SECONDS * (2 ** (attempt - 1)))
582+
583+
if last_error is not None:
584+
log.debug(
585+
f"Giving up finalizing tier 1 scan {tier1_scan_id} after "
586+
f"{TIER1_FINALIZE_MAX_ATTEMPTS} attempts: {last_error}"
587+
)
588+
else:
589+
log.debug(
590+
f"Giving up finalizing tier 1 scan {tier1_scan_id} after "
591+
f"{TIER1_FINALIZE_MAX_ATTEMPTS} attempts"
592+
)
593+
return False
566594

567595
@staticmethod
568596
def _compress_facts_file(source_path: str) -> str:

socketsecurity/core/tools/reachability.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
from socketdev import socketdev
22
from typing import List, Optional, Dict, Any
33
import os
4+
import platform
45
import subprocess
56
import json
67
import pathlib
78
import logging
89
import sys
910

11+
from socketsecurity import __version__
12+
1013
log = logging.getLogger(__name__)
1114

1215

16+
def _build_caller_user_agent() -> str:
17+
"""Build the SOCKET_CALLER_USER_AGENT string forwarded to the coana CLI.
18+
19+
Mirrors the Node CLI's ``<product>/<version> <runtime>/<version> <platform>/<arch>``
20+
shape so the backend can attribute reachability calls to the Python CLI.
21+
"""
22+
return (
23+
f"socket/{__version__} "
24+
f"python/{platform.python_version()} "
25+
f"{platform.system().lower()}/{platform.machine().lower()}"
26+
)
27+
28+
1329
class ReachabilityAnalyzer:
1430
def __init__(self, sdk: socketdev, api_token: str):
1531
self.sdk = sdk
@@ -108,6 +124,8 @@ def run_reachability_analysis(
108124
continue_on_install_errors: bool = False,
109125
continue_on_missing_lock_files: bool = False,
110126
continue_on_no_source_files: bool = False,
127+
reach_debug: bool = False,
128+
disable_external_tool_checks: bool = False,
111129
) -> Dict[str, Any]:
112130
"""
113131
Run reachability analysis.
@@ -147,8 +165,7 @@ def run_reachability_analysis(
147165

148166
# Add required arguments
149167
output_dir = str(pathlib.Path(output_path).parent)
150-
log.warning(f"output_dir: {output_dir}")
151-
log.warning(f"output_path: {output_path}")
168+
log.debug(f"output_dir: {output_dir}, output_path: {output_path}")
152169
cmd.extend([
153170
"--output-dir", output_dir,
154171
"--socket-mode", output_path,
@@ -197,6 +214,12 @@ def run_reachability_analysis(
197214
if enable_debug:
198215
cmd.append("-d")
199216

217+
if reach_debug:
218+
cmd.append("--debug")
219+
220+
if disable_external_tool_checks:
221+
cmd.append("--disable-external-tool-checks")
222+
200223
if use_only_pregenerated_sboms:
201224
cmd.append("--use-only-pregenerated-sboms")
202225

@@ -222,14 +245,25 @@ def run_reachability_analysis(
222245
# Required environment variables for Coana CLI
223246
env["SOCKET_ORG_SLUG"] = org_slug
224247
env["SOCKET_CLI_API_TOKEN"] = self.api_token
225-
226-
# Optional environment variables
248+
249+
# Identify the calling CLI to the coana tool / backend (parity with the Node CLI).
250+
env["SOCKET_CLI_VERSION"] = __version__
251+
env["SOCKET_CALLER_USER_AGENT"] = _build_caller_user_agent()
252+
253+
# NOTE: no proxy env is set here. coana already reads HTTPS_PROXY/HTTP_PROXY itself, and
254+
# we pass the full parent env above, so it inherits them. A SOCKET_CLI_API_PROXY override
255+
# should only be set from an explicit --proxy flag (not yet implemented), since seeding it
256+
# from HTTPS_PROXY would be a no-op (it's the same value coana already resolves).
257+
258+
# Optional environment variables.
259+
# NOTE: repo/branch are intentionally omitted by the caller (passed as None) when they
260+
# are the default sentinels, to avoid polluting coana's per-repo/branch cache buckets.
227261
if repo_name:
228262
env["SOCKET_REPO_NAME"] = repo_name
229-
263+
230264
if branch_name:
231265
env["SOCKET_BRANCH_NAME"] = branch_name
232-
266+
233267
# Set NODE_TLS_REJECT_UNAUTHORIZED=0 if allow_unverified is True
234268
if allow_unverified:
235269
env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"

socketsecurity/socketcli.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ def _write_attribution_file(config, payload: dict) -> None:
9595

9696
DEFAULT_API_TIMEOUT = 1200
9797

98+
# Sentinel repo/branch names used when none can be detected from git or supplied via flags.
99+
# When the repo/branch are these defaults we skip forwarding SOCKET_REPO_NAME/SOCKET_BRANCH_NAME
100+
# to the coana CLI so unrelated default-named runs don't share reachability cache buckets.
101+
DEFAULT_REPO_NAME = "socket-default-repo"
102+
DEFAULT_BRANCH_NAME = "socket-default-branch"
103+
98104

99105
def get_api_request_timeout(config: CliConfig) -> int:
100106
return config.timeout if config.timeout is not None else DEFAULT_API_TIMEOUT
@@ -288,16 +294,21 @@ def main_code():
288294
except NoSuchPathError:
289295
raise Exception(f"Unable to find path {config.target_path}")
290296

297+
# Track whether repo/branch fell back to the default sentinels so reachability can skip
298+
# forwarding them as coana cache-bucket keys (computed before any workspace suffixing).
299+
repo_defaulted = not config.repo
300+
branch_defaulted = not config.branch
301+
291302
if not config.repo:
292-
base_repo_name = "socket-default-repo"
303+
base_repo_name = DEFAULT_REPO_NAME
293304
if config.workspace_name:
294305
config.repo = f"{base_repo_name}-{config.workspace_name}"
295306
else:
296307
config.repo = base_repo_name
297308
log.debug(f"Using default repository name: {config.repo}")
298-
309+
299310
if not config.branch:
300-
config.branch = "socket-default-branch"
311+
config.branch = DEFAULT_BRANCH_NAME
301312
log.debug(f"Using default branch name: {config.branch}")
302313

303314
# Calculate the scan paths - combine target_path with sub_paths if provided
@@ -384,8 +395,8 @@ def main_code():
384395
enable_analysis_splitting=config.reach_enable_analysis_splitting or False,
385396
detailed_analysis_log_file=config.reach_detailed_analysis_log_file or False,
386397
lazy_mode=config.reach_lazy_mode or False,
387-
repo_name=config.repo,
388-
branch_name=config.branch,
398+
repo_name=None if repo_defaulted else config.repo,
399+
branch_name=None if branch_defaulted else config.branch,
389400
version=config.reach_version,
390401
concurrency=config.reach_concurrency,
391402
additional_params=config.reach_additional_params,
@@ -396,6 +407,8 @@ def main_code():
396407
continue_on_install_errors=config.reach_continue_on_install_errors,
397408
continue_on_missing_lock_files=config.reach_continue_on_missing_lock_files,
398409
continue_on_no_source_files=config.reach_continue_on_no_source_files,
410+
reach_debug=config.reach_debug,
411+
disable_external_tool_checks=config.reach_disable_external_tool_checks,
399412
)
400413

401414
log.info("Reachability analysis completed successfully")

0 commit comments

Comments
 (0)