diff --git a/CHANGELOG.md b/CHANGELOG.md index 1820ee0..c155d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Changed + +- Identify Socket API configuration requests and scan uploads as + `socket-basics/` using the Basics version. + ## [3.4.1] - 2026-10-21 ### Changed diff --git a/socket_basics/core/config.py b/socket_basics/core/config.py index 6b8c0da..767dea5 100644 --- a/socket_basics/core/config.py +++ b/socket_basics/core/config.py @@ -13,6 +13,8 @@ from pathlib import Path from typing import Dict, Any, List, Optional +from ..user_agent import USER_AGENT + logger = logging.getLogger(__name__) TRUE_STRINGS = ('true', '1', 'yes', 'on') @@ -1017,7 +1019,7 @@ def load_socket_basics_config(org_slug_override: str | None = None) -> Dict[str, from socketdev import socketdev # Initialize SDK - sdk = socketdev(token=api_key, timeout=100) + sdk = socketdev(token=api_key, timeout=100, user_agent=USER_AGENT) # Get organizations and find the right one or auto-discover orgs = sdk.org.get() diff --git a/socket_basics/socket_basics.py b/socket_basics/socket_basics.py index 4cb034a..b19a5c4 100644 --- a/socket_basics/socket_basics.py +++ b/socket_basics/socket_basics.py @@ -44,11 +44,13 @@ from .core.config import Config, parse_cli_args, create_config_from_args from .core.connector import ConnectorManager from .core.notification.manager import NotificationManager + from .user_agent import USER_AGENT except ImportError: # Absolute imports for when run as a script (best-effort fallback) from socket_basics.core.config import Config, parse_cli_args, create_config_from_args from socket_basics.core.connector import ConnectorManager from socket_basics.core.notification.manager import NotificationManager + from socket_basics.user_agent import USER_AGENT # Configure logger (basicConfig will be applied in main) logger = logging.getLogger(__name__) @@ -272,7 +274,7 @@ def submit_socket_facts(self, socket_facts_path: Path, results: Dict[str, Any]) # Initialize SDK logger.debug("Initializing socketdev SDK") - sdk = socketdev(token=socket_api_key, timeout=100) + sdk = socketdev(token=socket_api_key, timeout=100, user_agent=USER_AGENT) logger.debug("✓ socketdev SDK initialized") # Prepare full scan parameters with proper defaults diff --git a/socket_basics/user_agent.py b/socket_basics/user_agent.py new file mode 100644 index 0000000..5603642 --- /dev/null +++ b/socket_basics/user_agent.py @@ -0,0 +1,4 @@ +from .version import __version__ + +USER_AGENT_NAME = "socket-basics" +USER_AGENT = f"{USER_AGENT_NAME}/{__version__}" diff --git a/tests/test_user_agent.py b/tests/test_user_agent.py new file mode 100644 index 0000000..3441401 --- /dev/null +++ b/tests/test_user_agent.py @@ -0,0 +1,80 @@ +import json + +import pytest +import requests + +from socket_basics.core.config import Config, load_socket_basics_config +from socket_basics.socket_basics import SecurityScanner +from socket_basics.version import __version__ + + +@pytest.fixture +def captured_requests(monkeypatch): + captured = [] + + def send(session, request, **kwargs): + captured.append(request) + if request.path_url == "/v0/organizations": + organization = {"slug": "test-org", "plan": "enterprise"} + body = {"organizations": {"test-org": organization}} + elif request.method == "POST": + body = { + "id": "test-scan", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "organization_id": "test-org", + "repository_id": "test-repo", + "branch": "main", + "html_report_url": "https://socket.dev/test-scan", + } + else: + body = {} + response = requests.Response() + response.status_code = 201 if request.method == "POST" else 200 + response.headers["Content-Type"] = "application/json" + response._content = json.dumps(body).encode() + return response + + monkeypatch.setattr(requests.sessions.Session, "send", send) + monkeypatch.setenv("SOCKET_SECURITY_API_KEY", "synthetic-test-token") + monkeypatch.delenv("SKIP_SOCKET_SUBMISSION", raising=False) + return captured + + +def test_dashboard_configuration_requests_identify_basics(captured_requests): + config = load_socket_basics_config(org_slug_override="test-org") + + assert config["socket_org"] == "test-org" + assert [request.path_url for request in captured_requests] == [ + "/v0/organizations", + "/v0/orgs/test-org/settings/socket-basics", + ] + assert all( + request.headers["User-Agent"] == f"socket-basics/{__version__}" + for request in captured_requests + ) + + +def test_scan_upload_identifies_basics(captured_requests, tmp_path): + facts = tmp_path / ".socket.facts.json" + components = [{"type": "file", "name": "test"}] + facts.write_text(json.dumps({"components": components})) + config = Config( + { + "workspace": str(tmp_path), + "socket_api_key": "synthetic-test-token", + "socket_org": "test-org", + "repo": "test-repo", + "branch": "main", + } + ) + + SecurityScanner(config).submit_socket_facts(facts, {}) + + assert len(captured_requests) == 1 + request = captured_requests[0] + assert request.method == "POST" + assert request.path_url.startswith("/v0/orgs/test-org/full-scans?") + assert request.headers["User-Agent"] == f"socket-basics/{__version__}" + assert request.headers["Authorization"].startswith("Basic ") + assert request.headers["Content-Type"].startswith("multipart/form-data;")