Skip to content

Commit ae4b18b

Browse files
committed
initial
added tests and ran ruff ran ruff format ruff fixes ruff format changed file path parameter to positional parameter ran ruff format updated test related to file positional parameter updated test per ruff check updated README Squashed commits for import sbom
1 parent 8e2a160 commit ae4b18b

File tree

14 files changed

+663
-6
lines changed

14 files changed

+663
-6
lines changed

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ This guide walks you through both installation and usage.
5656
6. [Ignoring via a config file](#ignoring-via-a-config-file)
5757
6. [Report command](#report-command)
5858
1. [Generating SBOM Report](#generating-sbom-report)
59-
7. [Scan logs](#scan-logs)
60-
8. [Syntax Help](#syntax-help)
59+
7. [Import command](#import-command)
60+
8. [Scan logs](#scan-logs)
61+
9. [Syntax Help](#syntax-help)
6162

6263
# Prerequisites
6364

@@ -1295,6 +1296,26 @@ To create an SBOM report for a path:\
12951296
For example:\
12961297
`cycode report sbom --format spdx-2.3 --include-vulnerabilities --include-dev-dependencies path /path/to/local/project`
12971298
1299+
# Import Command
1300+
1301+
## Importing SBOM
1302+
1303+
A software bill of materials (SBOM) is an inventory of all constituent components and software dependencies involved in the development and delivery of an application.
1304+
Using this command, you can import an SBOM file from your file system into Cycode.
1305+
1306+
The following options are available for use with this command:
1307+
1308+
| Option | Description | Required | Default |
1309+
|----------------------------------------------------|--------------------------------------------|----------|-------------------------------------------------------|
1310+
| `-n, --name TEXT` | Display name of the SBOM | Yes | |
1311+
| `-v, --vendor TEXT` | Name of the entity that provided the SBOM | Yes | |
1312+
| `-l, --label TEXT` | Attach label to the SBOM | No | |
1313+
| `-o, --owner TEXT` | Email address of the Cycode user that serves as point of contact for this SBOM | No | |
1314+
| `-b, --business-impact [High \| Medium \| Low]` | Business Impact | No | Medium |
1315+
1316+
For example:\
1317+
`cycode import sbom --name example-sbom --vendor cycode -label tag1 -label tag2 --owner [email protected] /path/to/local/project`
1318+
12981319
# Scan Logs
12991320
13001321
All CLI scans are logged in Cycode. The logs can be found under Settings > CLI Logs.

cycode/cli/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typer.completion import install_callback, show_callback
1010

1111
from cycode import __version__
12-
from cycode.cli.apps import ai_remediation, auth, configure, ignore, report, scan, status
12+
from cycode.cli.apps import ai_remediation, auth, configure, ignore, report, report_import, scan, status
1313

1414
if sys.version_info >= (3, 10):
1515
from cycode.cli.apps import mcp
@@ -50,6 +50,7 @@
5050
app.add_typer(configure.app)
5151
app.add_typer(ignore.app)
5252
app.add_typer(report.app)
53+
app.add_typer(report_import.app)
5354
app.add_typer(scan.app)
5455
app.add_typer(status.app)
5556
if sys.version_info >= (3, 10):
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import typer
2+
3+
from cycode.cli.apps.report_import.report_import_command import report_import_command
4+
from cycode.cli.apps.report_import.sbom import sbom_command
5+
6+
app = typer.Typer(name='import', no_args_is_help=True)
7+
app.callback(short_help='Import report. You`ll need to specify which report type to import.')(report_import_command)
8+
app.command(name='sbom', short_help='Import SBOM report from a local path.')(sbom_command)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import typer
2+
3+
from cycode.cli.utils.sentry import add_breadcrumb
4+
5+
6+
def report_import_command(ctx: typer.Context) -> int:
7+
""":bar_chart: [bold cyan]Import security reports.[/]
8+
9+
Example usage:
10+
* `cycode import sbom`: Import SBOM report
11+
"""
12+
add_breadcrumb('import')
13+
return 1
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import typer
2+
3+
from cycode.cli.apps.report_import.sbom.sbom_command import sbom_command
4+
5+
app = typer.Typer(name='sbom')
6+
app.command(name='path', short_help='Import SBOM report from a local path.')(sbom_command)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from pathlib import Path
2+
from typing import Annotated, Optional
3+
4+
import typer
5+
6+
from cycode.cli.cli_types import BusinessImpactOption
7+
from cycode.cli.exceptions.handle_report_sbom_errors import handle_report_exception
8+
from cycode.cli.utils.get_api_client import get_import_sbom_cycode_client
9+
from cycode.cli.utils.sentry import add_breadcrumb
10+
from cycode.cyclient.import_sbom_client import ImportSbomParameters
11+
12+
13+
def sbom_command(
14+
ctx: typer.Context,
15+
path: Annotated[
16+
Path,
17+
typer.Argument(
18+
exists=True, resolve_path=True, dir_okay=False, readable=True, help='Path to SBOM file.', show_default=False
19+
),
20+
],
21+
sbom_name: Annotated[
22+
str, typer.Option('--name', '-n', help='SBOM Name.', case_sensitive=False, show_default=False)
23+
],
24+
vendor: Annotated[
25+
str, typer.Option('--vendor', '-v', help='Vendor Name.', case_sensitive=False, show_default=False)
26+
],
27+
labels: Annotated[
28+
Optional[list[str]],
29+
typer.Option(
30+
'--label', '-l', help='Label, can be specified multiple times.', case_sensitive=False, show_default=False
31+
),
32+
] = None,
33+
owners: Annotated[
34+
Optional[list[str]],
35+
typer.Option(
36+
'--owner',
37+
'-o',
38+
help='Email address of a user in Cycode platform, can be specified multiple times.',
39+
case_sensitive=True,
40+
show_default=False,
41+
),
42+
] = None,
43+
business_impact: Annotated[
44+
BusinessImpactOption,
45+
typer.Option(
46+
'--business-impact',
47+
'-b',
48+
help='Business Impact.',
49+
case_sensitive=True,
50+
show_default=True,
51+
),
52+
] = BusinessImpactOption.MEDIUM,
53+
) -> None:
54+
"""Import SBOM."""
55+
add_breadcrumb('sbom')
56+
57+
client = get_import_sbom_cycode_client(ctx)
58+
59+
import_parameters = ImportSbomParameters(
60+
Name=sbom_name,
61+
Vendor=vendor,
62+
BusinessImpact=business_impact,
63+
Labels=labels,
64+
Owners=owners,
65+
)
66+
67+
try:
68+
if not path.exists():
69+
from errno import ENOENT
70+
from os import strerror
71+
72+
raise FileNotFoundError(ENOENT, strerror(ENOENT), path.absolute())
73+
74+
client.request_sbom_import_execution(import_parameters, path)
75+
except Exception as e:
76+
handle_report_exception(ctx, e)

cycode/cli/cli_types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ class SbomOutputFormatOption(StrEnum):
5252
JSON = 'json'
5353

5454

55+
class BusinessImpactOption(StrEnum):
56+
HIGH = 'High'
57+
MEDIUM = 'Medium'
58+
LOW = 'Low'
59+
60+
5561
class SeverityOption(StrEnum):
5662
INFO = 'info'
5763
LOW = 'low'

cycode/cli/utils/get_api_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import click
44

55
from cycode.cli.user_settings.credentials_manager import CredentialsManager
6-
from cycode.cyclient.client_creator import create_report_client, create_scan_client
6+
from cycode.cyclient.client_creator import create_import_sbom_client, create_report_client, create_scan_client
77

88
if TYPE_CHECKING:
99
import typer
1010

11+
from cycode.cyclient.import_sbom_client import ImportSbomClient
1112
from cycode.cyclient.report_client import ReportClient
1213
from cycode.cyclient.scan_client import ScanClient
1314

@@ -38,6 +39,12 @@ def get_report_cycode_client(ctx: 'typer.Context', hide_response_log: bool = Tru
3839
return _get_cycode_client(create_report_client, client_id, client_secret, hide_response_log)
3940

4041

42+
def get_import_sbom_cycode_client(ctx: 'typer.Context', hide_response_log: bool = True) -> 'ImportSbomClient':
43+
client_id = ctx.obj.get('client_id')
44+
client_secret = ctx.obj.get('client_secret')
45+
return _get_cycode_client(create_import_sbom_client, client_id, client_secret, hide_response_log)
46+
47+
4148
def _get_configured_credentials() -> tuple[str, str]:
4249
credentials_manager = CredentialsManager()
4350
return credentials_manager.get_credentials()

cycode/cyclient/client_creator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from cycode.cyclient.config_dev import DEV_CYCODE_API_URL
33
from cycode.cyclient.cycode_dev_based_client import CycodeDevBasedClient
44
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
5+
from cycode.cyclient.import_sbom_client import ImportSbomClient
56
from cycode.cyclient.report_client import ReportClient
67
from cycode.cyclient.scan_client import ScanClient
78
from cycode.cyclient.scan_config_base import DefaultScanConfig, DevScanConfig
@@ -21,3 +22,8 @@ def create_scan_client(client_id: str, client_secret: str, hide_response_log: bo
2122
def create_report_client(client_id: str, client_secret: str, _: bool) -> ReportClient:
2223
client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
2324
return ReportClient(client)
25+
26+
27+
def create_import_sbom_client(client_id: str, client_secret: str, _: bool) -> ImportSbomClient:
28+
client = CycodeDevBasedClient(DEV_CYCODE_API_URL) if dev_mode else CycodeTokenBasedClient(client_id, client_secret)
29+
return ImportSbomClient(client)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import dataclasses
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
from requests import Response
6+
7+
from cycode.cli.cli_types import BusinessImpactOption
8+
from cycode.cli.exceptions.custom_exceptions import RequestHttpError
9+
from cycode.cyclient import models
10+
from cycode.cyclient.cycode_client_base import CycodeClientBase
11+
12+
13+
@dataclasses.dataclass
14+
class ImportSbomParameters:
15+
Name: str
16+
Vendor: str
17+
BusinessImpact: BusinessImpactOption
18+
Labels: Optional[list[str]]
19+
Owners: Optional[list[str]]
20+
21+
def _owners_to_ids(self) -> list[str]:
22+
return []
23+
24+
def to_request_form(self) -> dict:
25+
form_data = {}
26+
for field in dataclasses.fields(self):
27+
key = field.name
28+
val = getattr(self, key)
29+
if val is None or len(val) == 0:
30+
continue
31+
if isinstance(val, list):
32+
form_data[f'{key}[]'] = val
33+
else:
34+
form_data[key] = val
35+
return form_data
36+
37+
38+
class ImportSbomClient:
39+
IMPORT_SBOM_REQUEST_PATH: str = 'v4/sbom/import'
40+
GET_USER_ID_REQUEST_PATH: str = 'v4/members'
41+
42+
def __init__(self, client: CycodeClientBase) -> None:
43+
self.client = client
44+
45+
def request_sbom_import_execution(self, params: ImportSbomParameters, file_path: Path) -> None:
46+
if params.Owners:
47+
owners_ids = self.get_owners_user_ids(params.Owners)
48+
params.Owners = owners_ids
49+
50+
form_data = params.to_request_form()
51+
52+
with open(file_path.absolute(), 'rb') as f:
53+
request_args = {
54+
'url_path': self.IMPORT_SBOM_REQUEST_PATH,
55+
'data': form_data,
56+
'files': {'File': f},
57+
}
58+
59+
response = self.client.post(**request_args)
60+
61+
if response.status_code != 201:
62+
raise RequestHttpError(response.status_code, response.text, response)
63+
64+
def get_owners_user_ids(self, owners: list[str]) -> list[str]:
65+
return [self._get_user_id_by_email(owner) for owner in owners]
66+
67+
def _get_user_id_by_email(self, email: str) -> str:
68+
request_args = {'url_path': self.GET_USER_ID_REQUEST_PATH, 'params': {'email': email}}
69+
70+
response = self.client.get(**request_args)
71+
member_details = self.parse_requested_member_details_response(response)
72+
73+
if not member_details.items:
74+
raise Exception(
75+
f"Failed to find user with email '{email}'. Verify this email is registered to Cycode platform"
76+
)
77+
return member_details.items.pop(0).external_id
78+
79+
@staticmethod
80+
def parse_requested_member_details_response(response: Response) -> models.MemberDetails:
81+
return models.RequestedMemberDetailsResultSchema().load(response.json())

0 commit comments

Comments
 (0)