Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cycode/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def app_callback(
if user_agent:
user_agent_option = UserAgentOptionScheme().loads(user_agent)
CycodeClientBase.enrich_user_agent(user_agent_option.user_agent_suffix)
ctx.obj['plugin_app_name'] = user_agent_option.app_name
ctx.obj['plugin_app_version'] = user_agent_option.app_version

if not no_update_notifier:
ctx.call_on_close(lambda: check_latest_version_on_close(ctx))
46 changes: 46 additions & 0 deletions cycode/cli/apps/activation_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import TYPE_CHECKING, Optional

from cycode import __version__
from cycode.cli.config import configuration_manager
from cycode.cyclient.cli_activation_client import CliActivationClient
from cycode.logger import get_logger

if TYPE_CHECKING:
from cycode.cyclient.cycode_client_base import CycodeClientBase

logger = get_logger('Activation Manager')

_CLI_CLIENT_NAME = 'cli'


def _get_client_and_version(plugin_app_name: Optional[str], plugin_app_version: Optional[str]) -> tuple[str, str]:
return plugin_app_name or _CLI_CLIENT_NAME, plugin_app_version or __version__


def should_report_cli_activation(
plugin_app_name: Optional[str] = None,
plugin_app_version: Optional[str] = None,
) -> bool:
client, version = _get_client_and_version(plugin_app_name, plugin_app_version)
return configuration_manager.get_last_reported_activation_version(client) != version


def report_cli_activation(
cycode_client: 'CycodeClientBase',
plugin_app_name: Optional[str] = None,
plugin_app_version: Optional[str] = None,
) -> None:
"""Report CLI/IDE activation to the backend if the (client, version) pair is new.

Failures are swallowed — activation tracking is non-critical.
"""
try:
client, version = _get_client_and_version(plugin_app_name, plugin_app_version)

if configuration_manager.get_last_reported_activation_version(client) == version:
return

CliActivationClient(cycode_client).report_activation()
configuration_manager.update_last_reported_activation_version(client, version)
except Exception:
logger.debug('Failed to report CLI activation', exc_info=True)
8 changes: 8 additions & 0 deletions cycode/cli/apps/auth/auth_command.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import typer

from cycode.cli.apps.activation_manager import report_cli_activation, should_report_cli_activation
from cycode.cli.apps.auth.auth_manager import AuthManager
from cycode.cli.exceptions.handle_auth_errors import handle_auth_exception
from cycode.cli.logger import logger
from cycode.cli.models import CliResult
from cycode.cli.utils.get_api_client import get_scan_cycode_client


def auth_command(ctx: typer.Context) -> None:
Expand All @@ -23,6 +25,12 @@ def auth_command(ctx: typer.Context) -> None:
auth_manager = AuthManager()
auth_manager.authenticate()

plugin_app_name = ctx.obj.get('plugin_app_name')
plugin_app_version = ctx.obj.get('plugin_app_version')
if should_report_cli_activation(plugin_app_name, plugin_app_version):
scan_client = get_scan_cycode_client(ctx)
report_cli_activation(scan_client.scan_cycode_client, plugin_app_name, plugin_app_version)

result = CliResult(success=True, message='Successfully logged into cycode')
printer.print_result(result)
except Exception as err:
Expand Down
6 changes: 6 additions & 0 deletions cycode/cli/apps/scan/scan_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import click
import typer

from cycode.cli.apps.activation_manager import report_cli_activation, should_report_cli_activation
from cycode.cli.apps.sca_options import (
GradleAllSubProjectsOption,
MavenSettingsFileOption,
Expand Down Expand Up @@ -140,6 +141,11 @@ def scan_command(
scan_client = get_scan_cycode_client(ctx)
ctx.obj['client'] = scan_client

plugin_app_name = ctx.obj.get('plugin_app_name')
plugin_app_version = ctx.obj.get('plugin_app_version')
if should_report_cli_activation(plugin_app_name, plugin_app_version):
report_cli_activation(scan_client.scan_cycode_client, plugin_app_name, plugin_app_version)

# Get remote URL from current working directory
remote_url = _try_get_git_remote_url(os.getcwd())

Expand Down
5 changes: 5 additions & 0 deletions cycode/cli/apps/status/get_cli_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import TYPE_CHECKING

from cycode import __version__
from cycode.cli.apps.activation_manager import report_cli_activation, should_report_cli_activation
from cycode.cli.apps.auth.auth_common import get_authorization_info
from cycode.cli.apps.status.models import CliStatus, CliSupportedModulesStatus
from cycode.cli.consts import PROGRAM_NAME
Expand All @@ -22,7 +23,11 @@ def get_cli_status(ctx: 'Context') -> CliStatus:
supported_modules_status = CliSupportedModulesStatus()
if is_authenticated:
try:
plugin_app_name = ctx.obj.get('plugin_app_name')
plugin_app_version = ctx.obj.get('plugin_app_version')
client = get_scan_cycode_client(ctx)
if should_report_cli_activation(plugin_app_name, plugin_app_version):
report_cli_activation(client.scan_cycode_client, plugin_app_name, plugin_app_version)
supported_modules_preferences = client.get_supported_modules_preferences()

supported_modules_status.secret_scanning = supported_modules_preferences.secret_scanning
Expand Down
11 changes: 11 additions & 0 deletions cycode/cli/user_settings/config_file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ConfigFileManager(BaseFileManager):
SCAN_SECTION_NAME: str = 'scan'

INSTALLATION_ID_FIELD_NAME: str = 'installation_id'
LAST_REPORTED_ACTIVATION_VERSIONS_FIELD_NAME: str = 'last_reported_activation_versions'
API_URL_FIELD_NAME: str = 'cycode_api_url'
APP_URL_FIELD_NAME: str = 'cycode_app_url'
VERBOSE_FIELD_NAME: str = 'verbose'
Expand Down Expand Up @@ -68,6 +69,16 @@ def update_installation_id(self, installation_id: str) -> None:
update_data = {self.ENVIRONMENT_SECTION_NAME: {self.INSTALLATION_ID_FIELD_NAME: installation_id}}
self.write_content_to_file(update_data)

def get_last_reported_activation_versions(self) -> dict[str, str]:
value = self._get_value_from_environment_section(self.LAST_REPORTED_ACTIVATION_VERSIONS_FIELD_NAME)
return value if isinstance(value, dict) else {}

def update_last_reported_activation_version(self, client: str, version: str) -> None:
versions = self.get_last_reported_activation_versions()
versions[client] = version
update_data = {self.ENVIRONMENT_SECTION_NAME: {self.LAST_REPORTED_ACTIVATION_VERSIONS_FIELD_NAME: versions}}
self.write_content_to_file(update_data)

def add_exclusion(self, scan_type: str, exclusion_type: str, new_exclusion: str) -> None:
exclusions = self._get_exclusions_by_exclusion_type(scan_type, exclusion_type)
if new_exclusion in exclusions:
Expand Down
6 changes: 6 additions & 0 deletions cycode/cli/user_settings/configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ def get_or_create_installation_id(self) -> str:

return installation_id

def get_last_reported_activation_version(self, client: str) -> Optional[str]:
return self.global_config_file_manager.get_last_reported_activation_versions().get(client)

def update_last_reported_activation_version(self, client: str, version: str) -> None:
self.global_config_file_manager.update_last_reported_activation_version(client, version)

def get_config_file_manager(self, scope: Optional[str] = None) -> ConfigFileManager:
if scope == 'local':
return self.local_config_file_manager
Expand Down
14 changes: 14 additions & 0 deletions cycode/cyclient/cli_activation_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from cycode.cyclient.cycode_client_base import CycodeClientBase

_CLI_ACTIVATION_PATH = 'scans/api/v4/cli-activation'


class CliActivationClient:
def __init__(self, cycode_client: 'CycodeClientBase') -> None:
self._cycode_client = cycode_client

def report_activation(self) -> None:
self._cycode_client.put(url_path=_CLI_ACTIVATION_PATH)
57 changes: 57 additions & 0 deletions tests/user_settings/test_activation_tracking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from pathlib import Path

import pytest

from cycode.cli.user_settings.config_file_manager import ConfigFileManager


@pytest.fixture
def config_manager(tmp_path: Path) -> ConfigFileManager:
return ConfigFileManager(tmp_path)


def test_get_last_reported_activation_versions_returns_empty_when_not_set(
config_manager: ConfigFileManager,
) -> None:
assert config_manager.get_last_reported_activation_versions() == {}


def test_update_and_get_last_reported_activation_version_cli(config_manager: ConfigFileManager) -> None:
config_manager.update_last_reported_activation_version('cli', '1.10.7')

assert config_manager.get_last_reported_activation_versions() == {'cli': '1.10.7'}


def test_update_and_get_last_reported_activation_version_plugin(config_manager: ConfigFileManager) -> None:
config_manager.update_last_reported_activation_version('vscode_extension', '2.0.0')

assert config_manager.get_last_reported_activation_versions() == {'vscode_extension': '2.0.0'}


def test_update_last_reported_activation_version_multiple_clients(config_manager: ConfigFileManager) -> None:
config_manager.update_last_reported_activation_version('cli', '1.10.7')
config_manager.update_last_reported_activation_version('vscode_extension', '2.0.0')
config_manager.update_last_reported_activation_version('jetbrains_extension', '1.5.0')

assert config_manager.get_last_reported_activation_versions() == {
'cli': '1.10.7',
'vscode_extension': '2.0.0',
'jetbrains_extension': '1.5.0',
}


def test_update_last_reported_activation_version_overwrites_existing(config_manager: ConfigFileManager) -> None:
config_manager.update_last_reported_activation_version('cli', '1.10.7')
config_manager.update_last_reported_activation_version('cli', '1.10.8')

assert config_manager.get_last_reported_activation_versions() == {'cli': '1.10.8'}


def test_update_last_reported_activation_version_does_not_affect_other_clients(
config_manager: ConfigFileManager,
) -> None:
config_manager.update_last_reported_activation_version('cli', '1.10.7')
config_manager.update_last_reported_activation_version('vscode_extension', '2.0.0')
config_manager.update_last_reported_activation_version('cli', '1.10.8')

assert config_manager.get_last_reported_activation_versions()['vscode_extension'] == '2.0.0'
Loading