Skip to content

Commit

Permalink
CM-30564 - merge
Browse files Browse the repository at this point in the history
  • Loading branch information
saramontif committed Jan 24, 2024
2 parents b8ea49f + ff8016c commit 0d7f330
Show file tree
Hide file tree
Showing 19 changed files with 392 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
run: poetry install

- name: Run linter check
run: poetry run ruff check .
run: poetry run ruff check --output-format=github .

- name: Run code style check
run: poetry run ruff format --check .
164 changes: 164 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/cycodehq/cycode-cli/tests.yml)
![PyPI - Version](https://img.shields.io/pypi/v/cycode)
![GitHub License](https://img.shields.io/github/license/cycodehq/cycode-cli)

## How to contribute to Cycode CLI

The minimum version of Python that we support is 3.7.
We recommend using this version for local development.
But it’s fine to use a higher version without using new features from these versions.
We prefer 3.8 because it comes with the support of Apple Silicon, and it is as low as possible.

The project is under Poetry project management.
To deal with it, you should install it on your system:

Install Poetry (feel free to use Brew, etc):

```shell
curl -sSL https://install.python-poetry.org | python - -y
```

Add Poetry to PATH if required.

Add a plugin to support dynamic versioning from Git Tags:

```shell
poetry self add "poetry-dynamic-versioning[plugin]"
```

Install dependencies of the project:

```shell
poetry install
```

Check that the version is valid (not 0.0.0):

```shell
poetry version
```

You are ready to write code!

To run the project use:

```shell
poetry run cycode
```

or main entry point in an activated virtual environment:

```shell
python cycode/cli/main.py
```

### Code linting and formatting

We use `ruff` and `ruff format`.
It is configured well, so you don’t need to do anything.
You can see all enabled rules in the `pyproject.toml` file.
Both tests and the main codebase are checked.
Try to avoid type annotations like `Any`, etc.

GitHub Actions will check that your code is formatted well. You can run it locally:

```shell
# lint
poetry run ruff .
# format
poetry run ruff format .
```

Many rules support auto-fixing. You can run it with the `--fix` flag.

### Branching and versioning

We use the `main` branch as the main one.
All development should be done in feature branches.
When you are ready create a Pull Request to the `main` branch.

Each commit in the `main` branch will be built and published to PyPI as a pre-release!
Such builds could be installed with the `--pre` flag. For example:

```shell
pip install --pre cycode
```

Also, you can select a specific version of the pre-release:

```shell
pip install cycode==1.7.2.dev6
```

We are using [Semantic Versioning](https://semver.org/) and the version is generated automatically from Git Tags. So,
when you are ready to release a new version, you should create a new Git Tag. The version will be generated from it.

Pre-release versions are generated on distance from the latest Git Tag. For example, if the latest Git Tag is `1.7.2`,
then the next pre-release version will be `1.7.2.dev1`.

We are using GitHub Releases to create Git Tags with changelogs.
For changelogs, we are using a standard template
of [Automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes).

### Testing

We are using `pytest` for testing. You can run tests with:

```shell
poetry run pytest
```

The library used for sending requests is [requests](https://github.com/psf/requests).
To mock requests, we are using the [responses](https://github.com/getsentry/responses) library.
All requests must be mocked.

To see the code coverage of the project, you can run:

```shell
poetry run coverage run -m pytest .
```

To generate the HTML report, you can run:

```shell
poetry run coverage html
```

The report will be generated in the `htmlcov` folder.

### Documentation

Keep [README.md](README.md) up to date.
All CLI commands are documented automatically if you add a docstring to the command.
Clean up the changelog before release.

### Publishing

New versions are published automatically on the new GitHub Release.
It uses the OpenID Connect publishing mechanism to upload on PyPI.

[Homebrew formula](https://formulae.brew.sh/formula/cycode) is updated automatically on the new PyPI release.

The CLI is also distributed as executable files for Linux, macOS, and Windows.
It is powered by [PyInstaller](https://pyinstaller.org/) and the process is automated by GitHub Actions.
These executables are attached to GitHub Releases as assets.

To pack the project locally, you should run:

```shell
poetry build
```

It will create a `dist` folder with the package (sdist and wheel). You can install it locally:

```shell
pip install dist/cycode-{version}-py3-none-any.whl
```

To create an executable file locally, you should run:

```shell
poetry run pyinstaller pyinstaller.spec
```

It will create an executable file for **the current platform** in the `dist` folder.
2 changes: 1 addition & 1 deletion cycode/cli/commands/auth/auth_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def authorization_check(context: click.Context) -> None:
return

try:
if CycodeTokenBasedClient(client_id, client_secret).api_token:
if CycodeTokenBasedClient(client_id, client_secret).get_access_token():
printer.print_result(passed_auth_check_res)
return
except (NetworkError, HttpUnauthorizedError):
Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/commands/auth/auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def get_api_token_polling(self, session_id: str, code_verifier: str) -> 'ApiToke
raise AuthProcessError('session expired')

def save_api_token(self, api_token: 'ApiToken') -> None:
self.credentials_manager.update_credentials_file(api_token.client_id, api_token.secret)
self.credentials_manager.update_credentials(api_token.client_id, api_token.secret)

def _build_login_url(self, code_challenge: str, session_id: str) -> str:
app_url = self.configuration_manager.get_cycode_app_url()
Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/commands/configure/configure_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def configure_command() -> None:
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_file(client_id, client_secret)
_CREDENTIALS_MANAGER.update_credentials(client_id, client_secret)

if config_updated:
click.echo(_get_urls_update_result_message())
Expand Down
23 changes: 14 additions & 9 deletions cycode/cli/commands/scan/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,13 @@ def _get_scan_documents_thread_func(
severity_threshold = context.obj['severity_threshold']
command_scan_type = context.info_name

scan_parameters['aggregation_id'] = str(_generate_unique_id())

def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, LocalScanResult]:
local_scan_result = error = error_message = None
detections_count = relevant_detections_count = zip_file_size = 0

scan_id = str(_get_scan_id())
scan_id = str(_generate_unique_id())
scan_completed = False
should_use_scan_service = _should_use_scan_service(scan_type, scan_parameters)

Expand Down Expand Up @@ -280,6 +282,9 @@ def scan_documents(
is_commit_range: bool = False,
scan_parameters: Optional[dict] = None,
) -> None:
if not scan_parameters:
scan_parameters = get_default_scan_parameters(context)

progress_bar = context.obj['progress_bar']

if not documents_to_scan:
Expand Down Expand Up @@ -320,7 +325,7 @@ def scan_commit_range_documents(

local_scan_result = error_message = None
scan_completed = False
scan_id = str(_get_scan_id())
scan_id = str(_generate_unique_id())
should_use_scan_service = _should_use_scan_service(scan_type, scan_parameters)
from_commit_zipped_documents = InMemoryZip()
to_commit_zipped_documents = InMemoryZip()
Expand Down Expand Up @@ -393,7 +398,6 @@ def scan_commit_range_documents(
zip_file_size,
scan_command_type,
error_message,
should_use_scan_service,
)


Expand Down Expand Up @@ -614,6 +618,11 @@ def get_default_scan_parameters(context: click.Context) -> dict:
def get_scan_parameters(context: click.Context, paths: Tuple[str]) -> dict:
scan_parameters = get_default_scan_parameters(context)

if not paths:
return scan_parameters

scan_parameters['paths'] = paths

if len(paths) != 1:
# ignore remote url if multiple paths are provided
return scan_parameters
Expand All @@ -622,11 +631,7 @@ def get_scan_parameters(context: click.Context, paths: Tuple[str]) -> dict:
if remote_url:
# TODO(MarshalX): remove hardcode in context
context.obj['remote_url'] = remote_url
scan_parameters.update(
{
'remote_url': remote_url,
}
)
scan_parameters['remote_url'] = remote_url

return scan_parameters

Expand Down Expand Up @@ -788,7 +793,7 @@ def _report_scan_status(
logger.debug('Failed to report scan status, %s', {'exception_message': str(e)})


def _get_scan_id() -> UUID:
def _generate_unique_id() -> UUID:
return uuid4()


Expand Down
42 changes: 31 additions & 11 deletions cycode/cli/user_settings/credentials_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

from cycode.cli.config import CYCODE_CLIENT_ID_ENV_VAR_NAME, CYCODE_CLIENT_SECRET_ENV_VAR_NAME
from cycode.cli.user_settings.base_file_manager import BaseFileManager
from cycode.cli.utils.yaml_utils import read_file
from cycode.cli.user_settings.jwt_creator import JwtCreator


class CredentialsManager(BaseFileManager):
HOME_PATH: str = Path.home()
CYCODE_HIDDEN_DIRECTORY: str = '.cycode'
FILE_NAME: str = 'credentials.yaml'

CLIENT_ID_FIELD_NAME: str = 'cycode_client_id'
CLIENT_SECRET_FIELD_NAME: str = 'cycode_client_secret'
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'

def get_credentials(self) -> Tuple[str, str]:
client_id, client_secret = self.get_credentials_from_environment_variables()
Expand All @@ -28,21 +32,37 @@ def get_credentials_from_environment_variables() -> Tuple[str, str]:
return client_id, client_secret

def get_credentials_from_file(self) -> Tuple[Optional[str], Optional[str]]:
credentials_filename = self.get_filename()
try:
file_content = read_file(credentials_filename)
except FileNotFoundError:
return None, None

file_content = self.read_file()
client_id = file_content.get(self.CLIENT_ID_FIELD_NAME)
client_secret = file_content.get(self.CLIENT_SECRET_FIELD_NAME)
return client_id, client_secret

def update_credentials_file(self, client_id: str, client_secret: str) -> None:
credentials = {self.CLIENT_ID_FIELD_NAME: client_id, self.CLIENT_SECRET_FIELD_NAME: client_secret}
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)

def get_access_token(self) -> Tuple[Optional[str], Optional[float], Optional[JwtCreator]]:
file_content = self.read_file()

access_token = file_content.get(self.ACCESS_TOKEN_FIELD_NAME)
expires_in = file_content.get(self.ACCESS_TOKEN_EXPIRES_IN_FIELD_NAME)

creator = None
hashed_creator = file_content.get(self.ACCESS_TOKEN_CREATOR_FIELD_NAME)
if hashed_creator:
creator = JwtCreator(hashed_creator)

return access_token, expires_in, creator

self.get_filename()
self.write_content_to_file(credentials)
def update_access_token(
self, access_token: Optional[str], expires_in: Optional[float], creator: Optional[JwtCreator]
) -> None:
file_content_to_update = {
self.ACCESS_TOKEN_FIELD_NAME: access_token,
self.ACCESS_TOKEN_EXPIRES_IN_FIELD_NAME: expires_in,
self.ACCESS_TOKEN_CREATOR_FIELD_NAME: str(creator) if creator else None,
}
self.write_content_to_file(file_content_to_update)

def get_filename(self) -> str:
return os.path.join(self.HOME_PATH, self.CYCODE_HIDDEN_DIRECTORY, self.FILE_NAME)
24 changes: 24 additions & 0 deletions cycode/cli/user_settings/jwt_creator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from cycode.cli.utils.string_utils import hash_string_to_sha256

_SEPARATOR = '::'


def _get_hashed_creator(client_id: str, client_secret: str) -> str:
return hash_string_to_sha256(_SEPARATOR.join([client_id, client_secret]))


class JwtCreator:
def __init__(self, hashed_creator: str) -> None:
self._hashed_creator = hashed_creator

def __str__(self) -> str:
return self._hashed_creator

@classmethod
def create(cls, client_id: str, client_secret: str) -> 'JwtCreator':
return cls(_get_hashed_creator(client_id, client_secret))

def __eq__(self, other: 'JwtCreator') -> bool:
if not isinstance(other, JwtCreator):
return NotImplemented
return str(self) == str(other)
2 changes: 1 addition & 1 deletion cycode/cyclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .config import logger
from cycode.cyclient.config import logger

__all__ = [
'logger',
Expand Down
5 changes: 2 additions & 3 deletions cycode/cyclient/auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from requests import Response

from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, NetworkError

from . import models
from .cycode_client import CycodeClient
from cycode.cyclient import models
from cycode.cyclient.cycode_client import CycodeClient


class AuthClient:
Expand Down
4 changes: 2 additions & 2 deletions cycode/cyclient/cycode_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from . import config
from .cycode_client_base import CycodeClientBase
from cycode.cyclient import config
from cycode.cyclient.cycode_client_base import CycodeClientBase


class CycodeClient(CycodeClientBase):
Expand Down
Loading

0 comments on commit 0d7f330

Please sign in to comment.