Skip to content

Commit 30ff2c5

Browse files
committed
Cap the commit message read from the local checkout
commit_message travels in the query string of the full scan request, so an oversized value overflows the edge proxy's request line limit and the scan fails before reaching the API. The 200-character cap already covered --commit-message, but a run that omitted the flag backfilled the value straight from the checkout's HEAD commit, uncapped, so repositories whose commit messages carry generated release notes could not be scanned at all. Make the cap an invariant of the parsed configuration rather than a step in flag parsing, and apply it to the git-derived value as well. The truncation helper and its limit move to module scope so both sites share one definition. Extract the git setup block out of main_code into apply_git_context so the backfill is reachable from a test. Behavior is unchanged: the same fields are filled in the same order, and a path that is not a repository still sets ignore_commit_files. Note that the API has no length validation on the field. The rejection comes from the proxy in front of it, which reports 413 or 431 depending on which layer answers; the comment now covers both rather than naming one.
1 parent f24da9c commit 30ff2c5

7 files changed

Lines changed: 176 additions & 39 deletions

File tree

‎CHANGELOG.md‎

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

3+
## 2.9.7
4+
5+
### Fixed: cap the commit message read from the local checkout
6+
7+
- `commit_message` travels in the query string of the full scan request, so an
8+
oversized value overflows the edge proxy's request line limit and the scan
9+
fails with HTTP 431 before reaching the API. The 200-character cap already
10+
applied to `--commit-message`, but a run that omitted the flag backfilled the
11+
value straight from the checkout's HEAD commit, uncapped. Repositories whose
12+
commit messages carry generated release notes could not be scanned at all.
13+
- The cap is now an invariant of the parsed configuration and is applied to the
14+
git-derived value as well, so every source of `commit_message` lands under the
15+
limit.
16+
317
## 2.9.6
418

519
### Changed: bump pinned @coana-tech/cli to 15.10.48

‎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.9.6"
9+
version = "2.9.7"
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.9.6'
2+
__version__ = '2.9.7'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

‎socketsecurity/config.py‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,25 @@ def get_plugin_config_from_env(prefix: str) -> dict:
1919
return {}
2020

2121

22+
# commit_message rides in the query string of POST /v0/orgs/{org}/full-scans, so an
23+
# oversized message overflows the edge proxy's request line limit before the API ever
24+
# sees it. The API itself has no length validation on the field; the rejection comes
25+
# from the proxy, reported as either 413 or 431 depending on which one answers. 200
26+
# chars is a conservative ceiling given URL encoding can 2-3x the raw character count.
27+
MAX_COMMIT_MESSAGE_LENGTH = 200
28+
29+
30+
def truncate_commit_message(commit_message: Optional[str]) -> Optional[str]:
31+
"""Cap commit_message to a length the full-scan request line can carry."""
32+
if commit_message and len(commit_message) > MAX_COMMIT_MESSAGE_LENGTH:
33+
logging.debug(
34+
f"commit_message truncated from {len(commit_message)} to "
35+
f"{MAX_COMMIT_MESSAGE_LENGTH} characters to avoid API request size limits"
36+
)
37+
return commit_message[:MAX_COMMIT_MESSAGE_LENGTH]
38+
return commit_message
39+
40+
2241
def load_cli_config_file(config_path: str) -> dict:
2342
"""
2443
Load CLI defaults from a JSON or TOML file.
@@ -201,7 +220,13 @@ class CliConfig:
201220
legal: bool = False
202221
legal_format: str = "socket"
203222
config_file: Optional[str] = None
204-
223+
224+
def __post_init__(self):
225+
# Capped here rather than at the flag-parsing site so that every source of
226+
# commit_message (the --commit-message flag, a config file, the git backfill in
227+
# socketcli) lands under the limit.
228+
self.commit_message = truncate_commit_message(self.commit_message)
229+
205230
@classmethod
206231
def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
207232
parser = create_argument_parser()
@@ -257,19 +282,6 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
257282
if commit_message and commit_message.startswith('"') and commit_message.endswith('"'):
258283
commit_message = commit_message[1:-1]
259284

260-
# Truncate to avoid 413s from oversized URL query parameters.
261-
# The API has no application-layer length validation on commit_message;
262-
# the 413 originates from an infrastructure-layer URL length limit
263-
# (nginx/Cloudflare). 200 chars chosen as a conservative ceiling given
264-
# URL encoding can 2-3x raw character count.
265-
MAX_COMMIT_MESSAGE_LENGTH = 200
266-
if commit_message and len(commit_message) > MAX_COMMIT_MESSAGE_LENGTH:
267-
logging.debug(
268-
f"commit_message truncated from {len(commit_message)} to "
269-
f"{MAX_COMMIT_MESSAGE_LENGTH} characters to avoid API request size limits"
270-
)
271-
commit_message = commit_message[:MAX_COMMIT_MESSAGE_LENGTH]
272-
273285
config_args = {
274286
'api_token': api_token,
275287
'repo': args.repo,

‎socketsecurity/socketcli.py‎

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from socketdev import socketdev
1313
from socketdev.fullscans import FullScanParams
1414

15-
from socketsecurity.config import CliConfig
15+
from socketsecurity.config import CliConfig, truncate_commit_message
1616
from socketsecurity.core import Core
1717
from socketsecurity.core.classes import Diff
1818
from socketsecurity.core.cli_client import CliClient
@@ -210,6 +210,36 @@ def create_scm_scan(
210210
return diff, False
211211

212212

213+
def apply_git_context(config: CliConfig) -> Tuple[bool, Optional[Git]]:
214+
"""
215+
Fill in any repo details the caller did not pass from the checkout at target_path.
216+
217+
Returns whether target_path is a git repository, along with the Git handle when it is.
218+
"""
219+
try:
220+
git_repo = Git(config.target_path)
221+
except InvalidGitRepositoryError:
222+
log.debug("Not a git repository, setting ignore_commit_files=True")
223+
config.ignore_commit_files = True
224+
return False, None
225+
except NoSuchPathError:
226+
raise Exception(f"Unable to find path {config.target_path}")
227+
228+
if not config.repo:
229+
config.repo = git_repo.repo_name
230+
if not config.commit_sha:
231+
config.commit_sha = git_repo.commit_str
232+
if not config.branch:
233+
config.branch = git_repo.branch
234+
if not config.committers:
235+
config.committers = [git_repo.get_formatted_committer()]
236+
if not config.commit_message:
237+
# Capped like the flag-supplied value: a repository's own commit message is
238+
# unbounded, and it ships in the full-scan query string.
239+
config.commit_message = truncate_commit_message(git_repo.commit_message)
240+
return True, git_repo
241+
242+
213243
def build_socket_sdk(config: CliConfig) -> socketdev:
214244
cli_user_agent_string = f"SocketPythonCLI/{config.version}"
215245
return socketdev(
@@ -402,27 +432,7 @@ def main_code():
402432
discovered_scan_files = None
403433

404434
# Git setup
405-
is_repo = False
406-
git_repo: Git
407-
try:
408-
git_repo = Git(config.target_path)
409-
is_repo = True
410-
if not config.repo:
411-
config.repo = git_repo.repo_name
412-
if not config.commit_sha:
413-
config.commit_sha = git_repo.commit_str
414-
if not config.branch:
415-
config.branch = git_repo.branch
416-
if not config.committers:
417-
config.committers = [git_repo.get_formatted_committer()]
418-
if not config.commit_message:
419-
config.commit_message = git_repo.commit_message
420-
except InvalidGitRepositoryError:
421-
is_repo = False
422-
log.debug("Not a git repository, setting ignore_commit_files=True")
423-
config.ignore_commit_files = True
424-
except NoSuchPathError:
425-
raise Exception(f"Unable to find path {config.target_path}")
435+
is_repo, git_repo = apply_git_context(config)
426436

427437
# Track whether repo/branch fell back to the default sentinels so reachability can skip
428438
# forwarding them as coana cache-bucket keys (computed before any workspace suffixing).
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import subprocess
2+
3+
import pytest
4+
5+
from socketsecurity.config import (
6+
MAX_COMMIT_MESSAGE_LENGTH,
7+
CliConfig,
8+
truncate_commit_message,
9+
)
10+
from socketsecurity.socketcli import apply_git_context
11+
12+
13+
def _git(path, *args):
14+
return subprocess.run(
15+
["git", *args],
16+
cwd=path,
17+
check=True,
18+
capture_output=True,
19+
text=True,
20+
).stdout.strip()
21+
22+
23+
@pytest.fixture
24+
def repo_with_large_commit_message(tmp_path):
25+
"""A checkout whose HEAD commit message is far larger than the cap (~14 KB)."""
26+
path = tmp_path / "repo"
27+
path.mkdir()
28+
_git(path, "init", "-b", "main")
29+
_git(path, "config", "user.name", "Socket Test")
30+
_git(path, "config", "user.email", "socket@example.com")
31+
(path / "package.json").write_text("{}\n", encoding="utf-8")
32+
_git(path, "add", "package.json")
33+
_git(path, "commit", "-m", "Release notes\n\n" + ("- bumped a dependency\n" * 700))
34+
return path
35+
36+
37+
class TestTruncateCommitMessage:
38+
def test_none_passes_through(self):
39+
assert truncate_commit_message(None) is None
40+
41+
def test_empty_passes_through(self):
42+
assert truncate_commit_message("") == ""
43+
44+
def test_under_limit_is_unchanged(self):
45+
msg = "a normal short commit message"
46+
assert truncate_commit_message(msg) == msg
47+
48+
def test_at_limit_is_unchanged(self):
49+
msg = "a" * MAX_COMMIT_MESSAGE_LENGTH
50+
assert truncate_commit_message(msg) == msg
51+
52+
def test_over_limit_is_capped(self):
53+
assert truncate_commit_message("a" * 14_000) == "a" * MAX_COMMIT_MESSAGE_LENGTH
54+
55+
56+
class TestCliConfigInvariant:
57+
def test_direct_construction_is_capped(self):
58+
config = CliConfig(api_token="test", repo="widgets", commit_message="a" * 14_000)
59+
assert config.commit_message == "a" * MAX_COMMIT_MESSAGE_LENGTH
60+
61+
def test_config_file_value_is_capped(self, tmp_path):
62+
config_file = tmp_path / "socketcli.json"
63+
config_file.write_text('{"commit_message": "%s"}' % ("a" * 14_000), encoding="utf-8")
64+
config = CliConfig.from_args(["--api-token", "test", "--config", str(config_file)])
65+
assert config.commit_message == "a" * MAX_COMMIT_MESSAGE_LENGTH
66+
67+
68+
class TestGitBackfill:
69+
def test_message_read_from_git_is_capped(self, repo_with_large_commit_message):
70+
config = CliConfig(api_token="test", repo=None, target_path=str(repo_with_large_commit_message))
71+
assert config.commit_message is None
72+
73+
is_repo, git_repo = apply_git_context(config)
74+
75+
assert is_repo is True
76+
# The repository really does carry an oversized message; the cap is what keeps it
77+
# out of the full-scan query string.
78+
assert len(git_repo.commit_message) > 14_000
79+
assert len(config.commit_message) == MAX_COMMIT_MESSAGE_LENGTH
80+
assert config.commit_message == git_repo.commit_message[:MAX_COMMIT_MESSAGE_LENGTH]
81+
82+
def test_explicit_message_is_not_overwritten_by_git(self, repo_with_large_commit_message):
83+
config = CliConfig(
84+
api_token="test",
85+
repo=None,
86+
target_path=str(repo_with_large_commit_message),
87+
commit_message="explicit message",
88+
)
89+
90+
apply_git_context(config)
91+
92+
assert config.commit_message == "explicit message"
93+
94+
def test_non_repo_path_reports_no_repo(self, tmp_path):
95+
config = CliConfig(api_token="test", repo=None, target_path=str(tmp_path))
96+
97+
is_repo, git_repo = apply_git_context(config)
98+
99+
assert is_repo is False
100+
assert git_repo is None
101+
assert config.ignore_commit_files is True

‎uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)