Skip to content

Commit eb17457

Browse files
committed
feat(reach): align reachability flags and coana env with Node CLI
Bring the Python CLI's reachability surface to parity with the Node CLI: - G1: --reach-disable-external-tool-checks -> coana --disable-external-tool-checks - G5: forward SOCKET_CLI_VERSION + SOCKET_CALLER_USER_AGENT to coana (proxy is left to coana, which reads/inherits HTTPS_PROXY/HTTP_PROXY itself) - G6: omit SOCKET_REPO_NAME/SOCKET_BRANCH_NAME for the default repo/branch sentinels - G7: default reach memory-limit to 8192 and concurrency to 1 - G8: Node-style --reach-analysis-timeout/--reach-analysis-memory-limit as primary names, --reach-timeout/--reach-memory-limit kept as hidden aliases - G9: --reach-debug -> coana --debug (global --enable-debug -> -d unchanged) - G11: retry tier1 finalize with exponential backoff (3 attempts), never raising Removes stray always-on WARNING logging in the reachability runner. Adds a CHANGELOG 2.4.2 entry and tests for flags/defaults/aliases, the coana command/env builder, and finalize retry.
1 parent dcab444 commit eb17457

11 files changed

Lines changed: 387 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## 2.4.2
4+
5+
### Added: reachability flag and Coana environment alignment with the Node CLI
6+
7+
- New `--reach-disable-external-tool-checks` flag (passes `--disable-external-tool-checks`
8+
to the Coana CLI).
9+
- New `--reach-debug` flag to enable Coana debug output (`--debug`) independently of the
10+
global `--enable-debug`.
11+
- Node-style `--reach-analysis-timeout` and `--reach-analysis-memory-limit` are now the
12+
primary flag names; the previous `--reach-timeout` / `--reach-memory-limit` continue to
13+
work as hidden aliases.
14+
- Reachability defaults now match the Node CLI: memory limit `8192` MB and concurrency `1`.
15+
- The Coana subprocess now receives `SOCKET_CLI_VERSION` and `SOCKET_CALLER_USER_AGENT` so
16+
calls are attributed to the Python CLI. Proxies continue to work via the inherited
17+
`HTTPS_PROXY` / `HTTP_PROXY` environment variables, which Coana reads itself.
18+
- `SOCKET_REPO_NAME` / `SOCKET_BRANCH_NAME` are no longer forwarded to Coana when the repo
19+
and branch are the default sentinels, avoiding cross-run reachability cache-bucket
20+
collisions.
21+
- Tier 1 reachability finalize now retries with exponential backoff instead of giving up on
22+
the first transient error.
23+
324
## 2.4.1
425

526
### Added: pyenv in the Docker image

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.2"
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.2'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

socketsecurity/config.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ class CliConfig:
119119
# Reachability Flags
120120
reach: bool = False
121121
reach_version: Optional[str] = None
122-
reach_analysis_memory_limit: Optional[int] = None
122+
reach_analysis_memory_limit: Optional[int] = 8192
123123
reach_analysis_timeout: Optional[int] = None
124124
reach_disable_analytics: bool = False
125125
reach_disable_analysis_splitting: bool = False # Deprecated, kept for backwards compatibility
@@ -131,14 +131,16 @@ class CliConfig:
131131
reach_skip_cache: bool = False
132132
reach_min_severity: Optional[str] = None
133133
reach_output_file: Optional[str] = None
134-
reach_concurrency: Optional[int] = None
134+
reach_concurrency: Optional[int] = 1
135135
reach_additional_params: Optional[List[str]] = None
136136
only_facts_file: bool = False
137137
reach_use_only_pregenerated_sboms: bool = False
138138
reach_continue_on_analysis_errors: bool = False
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,33 @@ 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,
902+
default=8192,
891903
metavar="<mb>",
892-
help="Memory limit for reachability analysis in MB"
904+
help="Memory limit for reachability analysis in MB (default: 8192)"
905+
)
906+
# Backwards-compatible alias for the pre-alignment name. Kept working, hidden from help.
907+
reachability_group.add_argument(
908+
"--reach-memory-limit",
909+
dest="reach_analysis_memory_limit",
910+
type=int,
911+
help=argparse.SUPPRESS
893912
)
894913
reachability_group.add_argument(
895914
"--reach-ecosystems",
@@ -956,8 +975,9 @@ def create_argument_parser() -> argparse.ArgumentParser:
956975
"--reach-concurrency",
957976
dest="reach_concurrency",
958977
type=int,
978+
default=1,
959979
metavar="<number>",
960-
help="Concurrency level for reachability analysis (must be >= 1)"
980+
help="Concurrency level for reachability analysis (must be >= 1, default: 1)"
961981
)
962982
reachability_group.add_argument(
963983
"--reach-additional-params",
@@ -1002,6 +1022,20 @@ def create_argument_parser() -> argparse.ArgumentParser:
10021022
action="store_true",
10031023
help=argparse.SUPPRESS
10041024
)
1025+
reachability_group.add_argument(
1026+
"--reach-debug",
1027+
dest="reach_debug",
1028+
action="store_true",
1029+
help="Enable debug output for the reachability analysis (passes --debug to the coana CLI). "
1030+
"Independent of the global --enable-debug flag."
1031+
)
1032+
reachability_group.add_argument(
1033+
"--reach-disable-external-tool-checks",
1034+
dest="reach_disable_external_tool_checks",
1035+
action="store_true",
1036+
help="Disable coana's external tool availability checks during reachability analysis "
1037+
"(passes --disable-external-tool-checks to the coana CLI)."
1038+
)
10051039

10061040
parser.add_argument(
10071041
'--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)