From ff1cff84339febc902743bc93b95ac98a9aafc62 Mon Sep 17 00:00:00 2001 From: Oskar Haarklou Veileborg Date: Thu, 22 Oct 2026 10:44:49 +0200 Subject: [PATCH 1/3] Resolve the reachability facts file against the target path Coana runs with its working directory set to --target-path, so a relative --reach-output-file lands there, but the CLI looked for it relative to its own working directory. When the two differed (as in the Docker image), reading the scan ID logged a spurious "Facts file not found" warning, --only-facts-file submitted a file that didn't exist, and --reach-use-only-pregenerated-sboms silently left the facts file out of the upload. Resolve the path once in run_reachability_analysis and use the returned report_path everywhere. --- socketsecurity/core/tools/reachability.py | 9 ++++---- socketsecurity/socketcli.py | 7 +++---- tests/unit/test_reachability.py | 25 ++++++++++++++++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/socketsecurity/core/tools/reachability.py b/socketsecurity/core/tools/reachability.py index fa04efb..c3032a9 100644 --- a/socketsecurity/core/tools/reachability.py +++ b/socketsecurity/core/tools/reachability.py @@ -132,7 +132,7 @@ def run_reachability_analysis( org_slug: Socket organization slug target_directory: Directory to analyze tar_hash: Tar hash from manifest upload or existing scan (optional) - output_path: Output file path for results + output_path: Output file path for results, relative to target_directory unless absolute timeout: Analysis timeout, forwarded verbatim to coana --analysis-timeout (coana parses the units, e.g. '90s', '10m', '1h'; a bare number is seconds) memory_limit: Memory limit, forwarded verbatim to coana --memory-limit @@ -282,8 +282,9 @@ def run_reachability_analysis( log.error(f"Reachability analysis failed with exit code {returncode}") raise Exception(f"Reachability analysis failed with exit code {returncode}") - # Extract scan ID from output file - scan_id = self._extract_scan_id(output_path) + # Coana runs in target_directory, so a relative output_path lands there. + report_path = os.path.join(target_directory, output_path) + scan_id = self._extract_scan_id(report_path) log.info("Reachability analysis completed successfully") if scan_id: @@ -291,7 +292,7 @@ def run_reachability_analysis( return { "scan_id": scan_id, - "report_path": output_path, + "report_path": report_path, "tar_hash_used": tar_hash } diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index d73e6ce..4100b4b 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -561,7 +561,7 @@ def main_code(): # If only-facts-file mode, mark the facts file for submission if config.only_facts_file: - facts_file_to_submit = os.path.abspath(output_path) + facts_file_to_submit = os.path.abspath(result['report_path']) log.info(f"Only-facts-file mode: will submit only {facts_file_to_submit}") # If reach-use-only-pregenerated-sboms mode, submit CDX, SPDX, and facts file @@ -570,9 +570,8 @@ def main_code(): sbom_files_to_submit = [] for scan_path in scan_paths: sbom_files_to_submit.extend(core.find_sbom_files(scan_path)) - # Use relative path for facts file - if os.path.exists(output_path): - sbom_files_to_submit.append(output_path) + if os.path.exists(result['report_path']): + sbom_files_to_submit.append(result['report_path']) log.info(f"Pre-generated SBOMs mode: will submit {len(sbom_files_to_submit)} files (CDX, SPDX, and facts file)") except Exception as e: diff --git a/tests/unit/test_reachability.py b/tests/unit/test_reachability.py index e2c855d..f2c3709 100644 --- a/tests/unit/test_reachability.py +++ b/tests/unit/test_reachability.py @@ -20,12 +20,14 @@ VERSION = "1.2.3" -# Records what it was run with in $FAKE_COANA_RUN, or fails when passed --fail. +# Records what it was run with in $FAKE_COANA_RUN, or fails when passed --fail. Like Coana, it +# writes the facts file to the --socket-mode path, resolved against its cwd. FAKE_CLI_WRAPPER = """ import { writeFileSync } from 'node:fs' const args = process.argv.slice(2) if (args.includes('--fail')) process.exit(1) writeFileSync(process.env.FAKE_COANA_RUN, JSON.stringify({ args, cwd: process.cwd(), env: process.env })) +writeFileSync(args[args.indexOf('--socket-mode') + 1], JSON.stringify({ tier1ReachabilityScanId: 'reach-scan-id' })) """ @@ -84,6 +86,27 @@ def test_runs_the_cached_cli_wrapper_with_the_args_and_cwd(fake_run_path): assert run["cwd"] == str(fake_run_path.parent) +@pytest.mark.parametrize("absolute", [False, True]) +def test_report_path_is_where_coana_wrote_the_facts_file(fake_run_path, tmp_path, monkeypatch, absolute): + target = tmp_path / "project" + target.mkdir() + monkeypatch.chdir(tmp_path) + output_path = str(tmp_path / "out" / "facts.json") if absolute else ".socket.facts.json" + if absolute: + (tmp_path / "out").mkdir() + + result = _analyzer().run_reachability_analysis( + org_slug="my-org", + target_directory=str(target), + tar_hash="tar-hash-abc123", + output_path=output_path, + version=VERSION, + ) + + assert result["report_path"] == (output_path if absolute else str(target / ".socket.facts.json")) + assert result["scan_id"] == "reach-scan-id" + + def test_reach_debug_appends_debug_long_flag(fake_run_path): """G9: --reach-debug -> coana --debug; does not emit the global -d.""" args = _run(fake_run_path, reach_debug=True)["args"] From c0925dce0db2dd0713fd56db90b372d5daf466d4 Mon Sep 17 00:00:00 2001 From: Oskar Haarklou Veileborg Date: Thu, 22 Oct 2026 10:57:44 +0200 Subject: [PATCH 2/3] Add a changelog entry --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64021eb..b5538e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## 2.11.4 +### Fixed: find the reachability facts file when `--target-path` isn't the working directory + +- The CLI looked for `.socket.facts.json` (or a relative `--reach-output-file`) + in its own working directory, but the reachability engine writes it inside + `--target-path`. When the two differed, as in the Docker image, + `--only-facts-file` submitted a file that didn't exist, + `--reach-use-only-pregenerated-sboms` left the facts file out of the upload, + and every `--reach` run logged a spurious `Facts file not found` warning. + ### Changed: update the Docker image's PyPy to 3.12 - The amd64 Docker image now ships PyPy `8.0.1` (Python 3.12), up from PyPy From 2c5b6d0c48c0323a7c4624ed87f77d3996adc6eb Mon Sep 17 00:00:00 2001 From: Oskar Haarklou Veileborg Date: Thu, 22 Oct 2026 11:00:12 +0200 Subject: [PATCH 3/3] Shorten the changelog entry --- CHANGELOG.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5538e6..dcd2348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,8 @@ ### Fixed: find the reachability facts file when `--target-path` isn't the working directory -- The CLI looked for `.socket.facts.json` (or a relative `--reach-output-file`) - in its own working directory, but the reachability engine writes it inside - `--target-path`. When the two differed, as in the Docker image, - `--only-facts-file` submitted a file that didn't exist, - `--reach-use-only-pregenerated-sboms` left the facts file out of the upload, - and every `--reach` run logged a spurious `Facts file not found` warning. +- `--only-facts-file` and `--reach-use-only-pregenerated-sboms` now submit the + facts file, and `--reach` no longer warns `Facts file not found`. ### Changed: update the Docker image's PyPy to 3.12