Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ To install the Cycode CLI application on your local machine, perform the followi
./cycode
```

3. Finally authenticate the CLI. There are three methods to set the Cycode client ID and client secret:
3. Finally authenticate the CLI. There are three methods to set the Cycode client ID and credentials (client secret or OIDC ID token):

- [cycode auth](#using-the-auth-command) (**Recommended**)
- [cycode configure](#using-the-configure-command)
Expand Down Expand Up @@ -164,11 +164,15 @@ To install the Cycode CLI application on your local machine, perform the followi

`Cycode Client ID []: 7fe5346b-xxxx-xxxx-xxxx-55157625c72d`

5. Enter your Cycode Client Secret value.
5. Enter your Cycode Client Secret value (skip if you plan to use an OIDC ID token).

`Cycode Client Secret []: c1e24929-xxxx-xxxx-xxxx-8b08c1839a2e`

6. If the values were entered successfully, you'll see the following message:
6. Enter your Cycode OIDC ID Token value (optional).

`Cycode ID Token []: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...`

7. If the values were entered successfully, you'll see the following message:

`Successfully configured CLI credentials!`

Expand All @@ -193,6 +197,12 @@ and
export CYCODE_CLIENT_SECRET={your Cycode Secret Key}
```

If your organization uses OIDC authentication, you can provide the ID token instead (or in addition):

```bash
export CYCODE_ID_TOKEN={your Cycode OIDC ID token}
```

#### On Windows

1. From the Control Panel, navigate to the System menu:
Expand All @@ -207,7 +217,7 @@ export CYCODE_CLIENT_SECRET={your Cycode Secret Key}

<img height="30" src="https://raw.githubusercontent.com/cycodehq/cycode-cli/main/images/image3.png" alt="environments variables button"/>

4. Create `CYCODE_CLIENT_ID` and `CYCODE_CLIENT_SECRET` variables with values matching your ID and Secret Key, respectively:
4. Create `CYCODE_CLIENT_ID` and `CYCODE_CLIENT_SECRET` variables with values matching your ID and Secret Key, respectively. If you authenticate via OIDC, add `CYCODE_ID_TOKEN` with your OIDC ID token value as well:

<img height="100" src="https://raw.githubusercontent.com/cycodehq/cycode-cli/main/images/image4.png" alt="environment variables window"/>

Expand Down Expand Up @@ -321,6 +331,7 @@ The following are the options and commands available with the Cycode CLI applica
| `-o`, `--output [rich\|text\|json\|table]` | Specify the output type. The default is `rich`. |
| `--client-id TEXT` | Specify a Cycode client ID for this specific scan execution. |
| `--client-secret TEXT` | Specify a Cycode client secret for this specific scan execution. |
| `--id-token TEXT` | Specify a Cycode OIDC ID token for this specific scan execution. |
| `--install-completion` | Install completion for the current shell.. |
| `--show-completion [bash\|zsh\|fish\|powershell\|pwsh]` | Show completion for the specified shell, to copy it or customize the installation. |
| `-h`, `--help` | Show options for given command. |
Expand Down
8 changes: 8 additions & 0 deletions cycode/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ def app_callback(
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
id_token: Annotated[
Optional[str],
typer.Option(
help='Specify a Cycode OIDC ID token for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
_: Annotated[
Optional[bool],
typer.Option(
Expand Down Expand Up @@ -152,6 +159,7 @@ def app_callback(

ctx.obj['client_id'] = client_id
ctx.obj['client_secret'] = client_secret
ctx.obj['id_token'] = id_token

ctx.obj['progress_bar'] = get_progress_bar(hidden=no_progress_meter, sections=SCAN_PROGRESS_BAR_SECTIONS)

Expand Down
39 changes: 37 additions & 2 deletions cycode/cli/apps/auth/auth_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError
from cycode.cli.user_settings.credentials_manager import CredentialsManager
from cycode.cli.utils.jwt_utils import get_user_and_tenant_ids_from_access_token
from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient

if TYPE_CHECKING:
Expand All @@ -13,9 +14,23 @@
def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
printer = ctx.obj.get('console_printer')

client_id, client_secret = ctx.obj.get('client_id'), ctx.obj.get('client_secret')
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
id_token = ctx.obj.get('id_token')

credentials_manager = CredentialsManager()

auth_info = _try_oidc_authorization(ctx, printer, client_id, id_token)
if auth_info:
return auth_info

if not client_id or not client_secret:
client_id, client_secret = CredentialsManager().get_credentials()
stored_client_id, stored_id_token = credentials_manager.get_oidc_credentials()
auth_info = _try_oidc_authorization(ctx, printer, stored_client_id, stored_id_token)
if auth_info:
return auth_info

client_id, client_secret = credentials_manager.get_credentials()

if not client_id or not client_secret:
return None
Expand All @@ -32,3 +47,23 @@ def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
printer.print_exception()

return None


def _try_oidc_authorization(
ctx: 'Context', printer: any, client_id: Optional[str], id_token: Optional[str]
) -> Optional[AuthInfo]:
if not client_id or not id_token:
return None

try:
access_token = CycodeOidcBasedClient(client_id, id_token).get_access_token()
if not access_token:
return None

user_id, tenant_id = get_user_and_tenant_ids_from_access_token(access_token)
return AuthInfo(user_id=user_id, tenant_id=tenant_id)
except (RequestHttpError, HttpUnauthorizedError):
if ctx:
printer.print_exception()

return None
11 changes: 10 additions & 1 deletion cycode/cli/apps/configure/configure_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
get_app_url_input,
get_client_id_input,
get_client_secret_input,
get_id_token_input,
)
from cycode.cli.console import console
from cycode.cli.utils.sentry import add_breadcrumb
Expand All @@ -32,6 +33,7 @@ def configure_command() -> None:
* APP URL: The base URL for Cycode's web application (for on-premise or EU installations)
* Client ID: Your Cycode client ID for authentication
* Client Secret: Your Cycode client secret for authentication
* ID Token: Your Cycode ID token for authentication

Example usage:
* `cycode configure`: Start interactive configuration
Expand All @@ -55,15 +57,22 @@ def configure_command() -> None:
config_updated = True

current_client_id, current_client_secret = CREDENTIALS_MANAGER.get_credentials_from_file()
_, current_id_token = CREDENTIALS_MANAGER.get_oidc_credentials_from_file()
client_id = get_client_id_input(current_client_id)
client_secret = get_client_secret_input(current_client_secret)
id_token = get_id_token_input(current_id_token)

credentials_updated = False
if _should_update_value(current_client_id, client_id) or _should_update_value(current_client_secret, client_secret):
credentials_updated = True
CREDENTIALS_MANAGER.update_credentials(client_id, client_secret)

oidc_credentials_updated = False
if _should_update_value(current_client_id, client_id) or _should_update_value(current_id_token, id_token):
oidc_credentials_updated = True
CREDENTIALS_MANAGER.update_oidc_credentials(client_id, id_token)

if config_updated:
console.print(get_urls_update_result_message())
if credentials_updated:
if credentials_updated or oidc_credentials_updated:
console.print(get_credentials_update_result_message())
11 changes: 11 additions & 0 deletions cycode/cli/apps/configure/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,14 @@ def get_api_url_input(current_api_url: Optional[str]) -> str:
default = current_api_url

return typer.prompt(text=prompt_text, default=default, type=str)


def get_id_token_input(current_id_token: Optional[str]) -> Optional[str]:
prompt_text = 'Cycode ID Token'

prompt_suffix = ' []: '
if current_id_token:
prompt_suffix = f' [{obfuscate_text(current_id_token)}]: '

new_id_token = typer.prompt(text=prompt_text, prompt_suffix=prompt_suffix, default='', show_default=False)
return new_id_token or current_id_token
1 change: 1 addition & 0 deletions cycode/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
# env vars
CYCODE_CLIENT_ID_ENV_VAR_NAME = 'CYCODE_CLIENT_ID'
CYCODE_CLIENT_SECRET_ENV_VAR_NAME = 'CYCODE_CLIENT_SECRET'
CYCODE_ID_TOKEN_ENV_VAR_NAME = 'CYCODE_ID_TOKEN'
26 changes: 25 additions & 1 deletion cycode/cli/user_settings/credentials_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
from pathlib import Path
from typing import Optional

from cycode.cli.config import CYCODE_CLIENT_ID_ENV_VAR_NAME, CYCODE_CLIENT_SECRET_ENV_VAR_NAME
from cycode.cli.config import (
CYCODE_CLIENT_ID_ENV_VAR_NAME,
CYCODE_CLIENT_SECRET_ENV_VAR_NAME,
CYCODE_ID_TOKEN_ENV_VAR_NAME,
)
from cycode.cli.user_settings.base_file_manager import BaseFileManager
from cycode.cli.user_settings.jwt_creator import JwtCreator
from cycode.cli.utils.sentry import setup_scope_from_access_token
Expand All @@ -15,6 +19,7 @@ class CredentialsManager(BaseFileManager):

CLIENT_ID_FIELD_NAME: str = 'cycode_client_id'
CLIENT_SECRET_FIELD_NAME: str = 'cycode_client_secret'
ID_TOKEN_FIELD_NAME: str = 'cycode_id_token'
ACCESS_TOKEN_FIELD_NAME: str = 'cycode_access_token'
ACCESS_TOKEN_EXPIRES_IN_FIELD_NAME: str = 'cycode_access_token_expires_in'
ACCESS_TOKEN_CREATOR_FIELD_NAME: str = 'cycode_access_token_creator'
Expand All @@ -38,6 +43,25 @@ def get_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]:
client_secret = file_content.get(self.CLIENT_SECRET_FIELD_NAME)
return client_id, client_secret

def get_oidc_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]:
file_content = self.read_file()
client_id = file_content.get(self.CLIENT_ID_FIELD_NAME)
id_token = file_content.get(self.ID_TOKEN_FIELD_NAME)
return client_id, id_token

def get_oidc_credentials(self) -> tuple[Optional[str], Optional[str]]:
client_id = os.getenv(CYCODE_CLIENT_ID_ENV_VAR_NAME)
id_token = os.getenv(CYCODE_ID_TOKEN_ENV_VAR_NAME)

if client_id is not None and id_token is not None:
return client_id, id_token

return self.get_oidc_credentials_from_file()

def update_oidc_credentials(self, client_id: str, id_token: str) -> None:
file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.ID_TOKEN_FIELD_NAME: id_token}
self.write_content_to_file(file_content_to_update)

def update_credentials(self, client_id: str, client_secret: str) -> None:
file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.CLIENT_SECRET_FIELD_NAME: client_secret}
self.write_content_to_file(file_content_to_update)
Expand Down
32 changes: 27 additions & 5 deletions cycode/cli/utils/get_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,59 @@


def _get_cycode_client(
create_client_func: callable, client_id: Optional[str], client_secret: Optional[str], hide_response_log: bool
create_client_func: callable,
client_id: Optional[str],
client_secret: Optional[str],
hide_response_log: bool,
id_token: Optional[str] = None,
) -> Union['ScanClient', 'ReportClient']:
if client_id and id_token:
return create_client_func(client_id, None, hide_response_log, id_token)

if not client_id or not id_token:
oidc_client_id, oidc_id_token = _get_configured_oidc_credentials()
if oidc_client_id and oidc_id_token:
return create_client_func(oidc_client_id, None, hide_response_log, oidc_id_token)
if oidc_id_token and not oidc_client_id:
raise click.ClickException('Cycode client id needed for OIDC authentication.')

if not client_id or not client_secret:
client_id, client_secret = _get_configured_credentials()
if not client_id:
raise click.ClickException('Cycode client id needed.')
if not client_secret:
raise click.ClickException('Cycode client secret is needed.')

return create_client_func(client_id, client_secret, hide_response_log)
return create_client_func(client_id, client_secret, hide_response_log, None)


def get_scan_cycode_client(ctx: 'typer.Context') -> 'ScanClient':
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
id_token = ctx.obj.get('id_token')
hide_response_log = not ctx.obj.get('show_secret', False)
return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log)
return _get_cycode_client(create_scan_client, client_id, client_secret, hide_response_log, id_token)


def get_report_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ReportClient':
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log)
id_token = ctx.obj.get('id_token')
return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log, id_token)


def get_import_sbom_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ImportSbomClient':
client_id = ctx.obj.get('client_id')
client_secret = ctx.obj.get('client_secret')
return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log)
id_token = ctx.obj.get('id_token')
return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log, id_token)


def _get_configured_credentials() -> tuple[str, str]:
credentials_manager = CredentialsManager()
return credentials_manager.get_credentials()


def _get_configured_oidc_credentials() -> tuple[Optional[str], Optional[str]]:
credentials_manager = CredentialsManager()
return credentials_manager.get_oidc_credentials()
Loading
Loading