Skip to content

Commit db4007e

Browse files
committed
Reject retired credentials in CLI transports
Refuse the retired credential before the CLI starts HTTP or reachability work. Increment the package version required by the published-release check. Assisted-by: Codex:gpt-6.1-sol
1 parent 90d2b4b commit db4007e

10 files changed

Lines changed: 141 additions & 10 deletions

File tree

‎CHANGELOG.md‎

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

3+
## 2.10.12
4+
5+
### Fixed: reject retired API tokens before use
6+
7+
- The CLI rejects the retired API credential before HTTP or reachability
8+
requests are created.
9+
310
## 2.10.11
411

512
### Changed: bump pinned @coana-tech/cli to 15.12.7

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

‎socketsecurity/config.py‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@
99
from socketdev import INTEGRATION_TYPES, IntegrationType
1010

1111
from socketsecurity import __version__
12+
from socketsecurity.retired_api_token import reject_retired_api_token
13+
14+
API_TOKEN_ENVIRONMENT_VARIABLES = (
15+
"SOCKET_SECURITY_API_KEY",
16+
"SOCKET_SECURITY_API_TOKEN",
17+
"SOCKET_API_KEY",
18+
"SOCKET_API_TOKEN",
19+
)
20+
21+
22+
def select_api_token(
23+
environment: dict[str, str], argument_token: Optional[str]
24+
) -> Optional[str]:
25+
return next(
26+
(
27+
environment[name]
28+
for name in API_TOKEN_ENVIRONMENT_VARIABLES
29+
if environment.get(name)
30+
),
31+
argument_token,
32+
)
1233

1334

1435
def get_plugin_config_from_env(prefix: str) -> dict:
@@ -272,13 +293,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
272293
)
273294

274295
# Get API token from env or args (check multiple env var names)
275-
api_token = (
276-
os.getenv("SOCKET_SECURITY_API_KEY") or
277-
os.getenv("SOCKET_SECURITY_API_TOKEN") or
278-
os.getenv("SOCKET_API_KEY") or
279-
os.getenv("SOCKET_API_TOKEN") or
280-
args.api_token
281-
)
296+
api_token = select_api_token(os.environ, args.api_token)
297+
reject_retired_api_token(api_token)
282298

283299
# --sarif-file implies --enable-sarif
284300
if args.sarif_file:

‎socketsecurity/core/cli_client.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import requests
77

88
from socketsecurity import USER_AGENT
9+
from socketsecurity.retired_api_token import reject_retired_api_token
910

1011
from .exceptions import APIFailure
1112
from .socket_config import SocketConfig
@@ -14,6 +15,7 @@
1415

1516
class CliClient:
1617
def __init__(self, config: SocketConfig):
18+
reject_retired_api_token(config.api_key)
1719
self.config = config
1820
self._encoded_key = self._encode_key(config.api_key)
1921

‎socketsecurity/core/socket_config.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from socketdev.core.issues import AllIssues
77

88
from socketsecurity import __version__
9+
from socketsecurity.retired_api_token import reject_retired_api_token
910

1011
default_exclude_dirs = {
1112
"node_modules", "bower_components", "jspm_packages", # JS/TS
@@ -39,6 +40,8 @@ def __post_init__(self):
3940
if not self.api_key:
4041
raise ValueError("API key is required")
4142

43+
reject_retired_api_token(self.api_key)
44+
4245
if self.timeout <= 0:
4346
raise ValueError("Timeout must be a positive integer")
4447

‎socketsecurity/core/tools/reachability.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from socketdev import socketdev
1414

1515
from socketsecurity import __version__
16+
from socketsecurity.retired_api_token import reject_retired_api_token
1617

1718
log = logging.getLogger(__name__)
1819

@@ -51,6 +52,7 @@ def _build_caller_user_agent() -> str:
5152

5253
class ReachabilityAnalyzer:
5354
def __init__(self, sdk: socketdev, api_token: str):
55+
reject_retired_api_token(api_token)
5456
self.sdk = sdk
5557
self.api_token = api_token
5658

@@ -137,6 +139,7 @@ def run_reachability_analysis(
137139
Returns:
138140
Dict containing scan_id and report_path
139141
"""
142+
reject_retired_api_token(self.api_token)
140143
# Build the coana CLI arguments (everything after the package spec). The launcher
141144
# (npx, or the npm-install + node fallback) is chosen in _spawn_coana() below.
142145
coana_args = ["run", "."]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import hashlib
2+
import hmac
3+
from typing import Optional
4+
5+
RETIRED_API_TOKEN_SHA256 = (
6+
"9cab58b24140ee12c08e265729d246f9200f8ef9b1d884f90c06c4e6b69adf18"
7+
)
8+
RETIRED_API_TOKEN_ERROR = (
9+
"This API token has been retired. Create a new API token in Socket "
10+
"and update your configuration."
11+
)
12+
13+
14+
def _matches_retired_digest(token: Optional[str], digest: str) -> bool:
15+
if token is None:
16+
return False
17+
token_digest = hashlib.sha256(token.strip().encode("utf-8")).hexdigest()
18+
return hmac.compare_digest(token_digest, digest)
19+
20+
21+
def _reject_retired_api_token(token: Optional[str], digest: str) -> None:
22+
if _matches_retired_digest(token, digest):
23+
raise ValueError(RETIRED_API_TOKEN_ERROR)
24+
25+
26+
def reject_retired_api_token(token: Optional[str]) -> None:
27+
_reject_retired_api_token(token, RETIRED_API_TOKEN_SHA256)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import hashlib
2+
3+
import pytest
4+
from socketdev import socketdev
5+
6+
from socketsecurity.config import (
7+
API_TOKEN_ENVIRONMENT_VARIABLES,
8+
CliConfig,
9+
select_api_token,
10+
)
11+
from socketsecurity.core.cli_client import CliClient
12+
from socketsecurity.core.socket_config import SocketConfig
13+
from socketsecurity.core.tools.reachability import ReachabilityAnalyzer
14+
from socketsecurity.retired_api_token import (
15+
RETIRED_API_TOKEN_ERROR,
16+
_matches_retired_digest,
17+
_reject_retired_api_token,
18+
)
19+
20+
21+
def test_retired_api_token_rejection_redacts_token_and_digest():
22+
token = "synthetic-retired-token"
23+
digest = hashlib.sha256(token.encode()).hexdigest()
24+
25+
with pytest.raises(ValueError) as exc_info:
26+
_reject_retired_api_token(token, digest)
27+
28+
assert str(exc_info.value) == RETIRED_API_TOKEN_ERROR
29+
assert token not in str(exc_info.value)
30+
assert digest not in str(exc_info.value)
31+
assert _matches_retired_digest(f" {token} ", digest)
32+
33+
34+
def test_cli_config_preserves_token_environment_precedence(monkeypatch):
35+
for name in API_TOKEN_ENVIRONMENT_VARIABLES:
36+
monkeypatch.delenv(name, raising=False)
37+
monkeypatch.setenv(API_TOKEN_ENVIRONMENT_VARIABLES[0], "first-env-token")
38+
monkeypatch.setenv(API_TOKEN_ENVIRONMENT_VARIABLES[1], "second-env-token")
39+
40+
config = CliConfig.from_args(["--api-token", "argument-token"])
41+
42+
assert config.api_token == "first-env-token"
43+
44+
45+
def test_cli_config_uses_argument_when_token_environment_is_empty(monkeypatch):
46+
for name in API_TOKEN_ENVIRONMENT_VARIABLES:
47+
monkeypatch.delenv(name, raising=False)
48+
49+
config = CliConfig.from_args(["--api-token", "argument-token"])
50+
51+
assert config.api_token == "argument-token"
52+
53+
54+
def test_cli_http_and_reachability_construction_accept_safe_token_without_network():
55+
token = "synthetic-safe-token"
56+
config = SocketConfig(api_key=token)
57+
client = CliClient(config)
58+
sdk = socketdev(token=token)
59+
analyzer = ReachabilityAnalyzer(sdk, token)
60+
61+
assert client.config.api_key == token
62+
assert analyzer.api_token == token
63+
64+
65+
@pytest.mark.parametrize("token", [None, "different-token"])
66+
def test_nonretired_api_token_is_accepted(token):
67+
digest = hashlib.sha256(b"synthetic-retired-token").hexdigest()
68+
_reject_retired_api_token(token, digest)
69+
70+
71+
@pytest.mark.parametrize("name", API_TOKEN_ENVIRONMENT_VARIABLES)
72+
def test_each_cli_environment_source_is_supported(name):
73+
assert select_api_token({name: "selected-token"}, "argument-token") == "selected-token"

‎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)