Skip to content

Commit 1541915

Browse files
committed
Reject retired credentials in CLI transports
1 parent 90d2b4b commit 1541915

6 files changed

Lines changed: 132 additions & 7 deletions

File tree

‎socketsecurity/config.py‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@
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+
15+
API_TOKEN_ENVIRONMENT_VARIABLES = (
16+
"SOCKET_SECURITY_API_KEY",
17+
"SOCKET_SECURITY_API_TOKEN",
18+
"SOCKET_API_KEY",
19+
"SOCKET_API_TOKEN",
20+
)
21+
22+
23+
def select_api_token(
24+
environment: dict[str, str], argument_token: Optional[str]
25+
) -> Optional[str]:
26+
return next(
27+
(
28+
environment[name]
29+
for name in API_TOKEN_ENVIRONMENT_VARIABLES
30+
if environment.get(name)
31+
),
32+
argument_token,
33+
)
1234

1335

1436
def get_plugin_config_from_env(prefix: str) -> dict:
@@ -272,13 +294,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
272294
)
273295

274296
# 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-
)
297+
api_token = select_api_token(os.environ, args.api_token)
298+
reject_retired_api_token(api_token)
282299

283300
# --sarif-file implies --enable-sarif
284301
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"

0 commit comments

Comments
 (0)