cylera-cli 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cylera.py ADDED
@@ -0,0 +1,550 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cylera CLI - Command line interface for the Cylera Partner API.
4
+
5
+ Usage: cylera <command> [arguments...]
6
+
7
+ This CLI provides read-only access to the Cylera Partner API for querying
8
+ device inventory, threats, vulnerabilities, and network information.
9
+ """
10
+
11
+ import getpass
12
+ import json
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Annotated, Any, Optional
17
+
18
+ import typer
19
+ from dotenv import load_dotenv
20
+
21
+ from cylera_client import (
22
+ CyleraClient,
23
+ Inventory,
24
+ Network,
25
+ Organization,
26
+ Risk,
27
+ Threat,
28
+ Utilization,
29
+ CyleraAPIError,
30
+ CyleraAuthError,
31
+ )
32
+
33
+ # Available Cylera API endpoints
34
+ CYLERA_URLS = [
35
+ "https://partner.us1.cylera.com/",
36
+ "https://partner.uk1.cylera.com/",
37
+ "https://partner.demo.cylera.com/",
38
+ ]
39
+
40
+ app = typer.Typer(
41
+ name="cylera",
42
+ help="Cylera CLI - Command line interface for the Cylera Partner API",
43
+ add_completion=False,
44
+ no_args_is_help=True,
45
+ )
46
+
47
+
48
+ def print_json(data: Any) -> None:
49
+ """Print data as formatted JSON."""
50
+ print(json.dumps(data, indent=2))
51
+
52
+
53
+ def get_client() -> CyleraClient:
54
+ """Create and return a CyleraClient using environment variables."""
55
+ base_url = os.environ.get("CYLERA_BASE_URL")
56
+ username = os.environ.get("CYLERA_USERNAME")
57
+ password = os.environ.get("CYLERA_PASSWORD")
58
+
59
+ if not base_url or not username or not password:
60
+ print(
61
+ "Error: Missing required environment variables.\n"
62
+ "Please set CYLERA_BASE_URL, CYLERA_USERNAME, and CYLERA_PASSWORD\n"
63
+ "in your environment or .env file.",
64
+ file=sys.stderr,
65
+ )
66
+ raise typer.Exit(1)
67
+
68
+ return CyleraClient(username=username, password=password, base_url=base_url)
69
+
70
+
71
+ def check_environment() -> bool:
72
+ """Check if required environment variables are set."""
73
+ load_dotenv(Path.cwd() / ".env")
74
+ base_url = os.environ.get("CYLERA_BASE_URL")
75
+ username = os.environ.get("CYLERA_USERNAME")
76
+ password = os.environ.get("CYLERA_PASSWORD")
77
+ return bool(base_url and username and password)
78
+
79
+
80
+ def require_config() -> None:
81
+ """Check that CLI is configured, exit with message if not."""
82
+ if not check_environment():
83
+ print(
84
+ f"Cylera CLI is not configured.\n\n"
85
+ f"Current directory: {Path.cwd()}\n\n"
86
+ "Run 'cylera init' to set up your credentials.",
87
+ file=sys.stderr,
88
+ )
89
+ raise typer.Exit(1)
90
+
91
+
92
+ # Common option types
93
+ PageOption = Annotated[Optional[int], typer.Option(
94
+ help="Page number for pagination")]
95
+ PageSizeOption = Annotated[
96
+ Optional[int], typer.Option(
97
+ "--page-size", help="Results per page (max 100)")
98
+ ]
99
+ MacAddressOption = Annotated[
100
+ Optional[str], typer.Option("--mac-address", help="MAC address of device")
101
+ ]
102
+ SeverityOption = Annotated[
103
+ Optional[str],
104
+ typer.Option(help="Severity level: INFO, LOW, MEDIUM, HIGH, CRITICAL"),
105
+ ]
106
+ StatusOption = Annotated[
107
+ Optional[str],
108
+ typer.Option(help="Status: OPEN, IN_PROGRESS, RESOLVED, SUPPRESSED"),
109
+ ]
110
+ DetectedAfterOption = Annotated[
111
+ Optional[int], typer.Option(
112
+ "--detected-after", help="Epoch timestamp filter")
113
+ ]
114
+
115
+
116
+ def _check_existing_config() -> None:
117
+ """Exit with error if Cylera environment variables are already set."""
118
+ var_names = ["CYLERA_BASE_URL", "CYLERA_USERNAME", "CYLERA_PASSWORD"]
119
+ existing_vars = [v for v in var_names if os.environ.get(v)]
120
+ if existing_vars:
121
+ print(
122
+ "Error: The following environment variables are already set:\n"
123
+ f" {', '.join(existing_vars)}\n\n"
124
+ "To reconfigure, unset these variables first or delete the .env file.\n"
125
+ "Example: unset CYLERA_BASE_URL CYLERA_USERNAME CYLERA_PASSWORD",
126
+ file=sys.stderr,
127
+ )
128
+ raise typer.Exit(1)
129
+
130
+
131
+ def _prompt_base_url() -> str:
132
+ """Prompt user to select a Cylera API endpoint and return the URL."""
133
+ print("Select your Cylera API endpoint:")
134
+ for i, url in enumerate(CYLERA_URLS, 1):
135
+ print(f" {i}. {url}")
136
+ print()
137
+
138
+ while True:
139
+ try:
140
+ choice = input(f"Enter choice [1-{len(CYLERA_URLS)}]: ").strip()
141
+ choice_num = int(choice)
142
+ if 1 <= choice_num <= len(CYLERA_URLS):
143
+ return CYLERA_URLS[choice_num - 1]
144
+ print(f"Please enter a number between 1 and {len(CYLERA_URLS)}")
145
+ except ValueError:
146
+ print("Please enter a valid number")
147
+
148
+
149
+ def _test_auth(username: str, password: str, base_url: str) -> None:
150
+ """Test authentication and print the response. Exits on failure."""
151
+ print("Testing authentication...", end=" ", flush=True)
152
+ try:
153
+ client = CyleraClient(
154
+ username=username, password=password, base_url=base_url)
155
+ auth_response = client.test_authenticate()
156
+ client.close()
157
+ except CyleraAuthError as e:
158
+ print("Failed!")
159
+ print(f"\nAuthentication error: {e}", file=sys.stderr)
160
+ print("\nPlease check your credentials and try again.", file=sys.stderr)
161
+ raise typer.Exit(1)
162
+
163
+ print("Success!")
164
+ print()
165
+ print("Authentication response:")
166
+ for key, value in auth_response.items():
167
+ if key != "token":
168
+ print(f" {key}: {value}")
169
+
170
+
171
+ def _save_env_config(base_url: str, username: str, password: str) -> Path:
172
+ """Append Cylera config to the .env file and return the path."""
173
+ env_path = Path.cwd() / ".env"
174
+
175
+ existing_content = ""
176
+ if env_path.exists():
177
+ existing_content = env_path.read_text()
178
+ if existing_content and not existing_content.endswith("\n"):
179
+ existing_content += "\n"
180
+
181
+ cylera_config = (
182
+ f"\n# Cylera CLI Configuration\n"
183
+ f"CYLERA_BASE_URL={base_url}\n"
184
+ f"CYLERA_USERNAME={username}\n"
185
+ f"CYLERA_PASSWORD={password}\n"
186
+ )
187
+
188
+ env_path.write_text(existing_content + cylera_config)
189
+ return env_path
190
+
191
+
192
+ @app.command()
193
+ def init() -> None:
194
+ """Initialize Cylera CLI configuration interactively."""
195
+ load_dotenv()
196
+ _check_existing_config()
197
+
198
+ print("Cylera CLI Configuration")
199
+ print("=" * 40)
200
+ print()
201
+
202
+ base_url = _prompt_base_url()
203
+ print()
204
+
205
+ username = input("Enter your Cylera username (email): ").strip()
206
+ if not username:
207
+ print("Error: Username cannot be empty", file=sys.stderr)
208
+ raise typer.Exit(1)
209
+
210
+ print()
211
+
212
+ password = getpass.getpass("Enter your Cylera password: ")
213
+ if not password:
214
+ print("Error: Password cannot be empty", file=sys.stderr)
215
+ raise typer.Exit(1)
216
+
217
+ print()
218
+ _test_auth(username, password, base_url)
219
+
220
+ env_path = _save_env_config(base_url, username, password)
221
+
222
+ print()
223
+ print(f"Configuration saved to {env_path}")
224
+ print()
225
+ print("You can now use the Cylera CLI. Try:")
226
+ print(" cylera devices --page-size 5")
227
+
228
+
229
+ @app.command()
230
+ def organization() -> None:
231
+ """Get the organization associated with the current credentials."""
232
+ require_config()
233
+ try:
234
+ with get_client() as client:
235
+ result = Organization(client).get_organization()
236
+ print_json(result)
237
+ except CyleraAPIError as e:
238
+ print(f"API error: {e}", file=sys.stderr)
239
+ raise typer.Exit(1)
240
+
241
+
242
+ @app.command()
243
+ def organizations() -> None:
244
+ """List organizations available to switch into."""
245
+ require_config()
246
+ try:
247
+ with get_client() as client:
248
+ result = Organization(client).get_available_organizations()
249
+ print_json(result)
250
+ except CyleraAPIError as e:
251
+ print(f"API error: {e}", file=sys.stderr)
252
+ raise typer.Exit(1)
253
+
254
+
255
+ @app.command()
256
+ def switchorg(
257
+ organization_id: Annotated[str, typer.Argument(help="Organization ID to switch into")],
258
+ ) -> None:
259
+ """Switch to a different organization."""
260
+ require_config()
261
+ try:
262
+ with get_client() as client:
263
+ result = Organization(client).switch_organization(organization_id)
264
+ print_json(result)
265
+ except CyleraAPIError as e:
266
+ print(f"API error: {e}", file=sys.stderr)
267
+ raise typer.Exit(1)
268
+
269
+
270
+ @app.command()
271
+ def resetorg() -> None:
272
+ """Reset organization back to home."""
273
+ require_config()
274
+ try:
275
+ with get_client() as client:
276
+ result = Organization(client).reset_organization()
277
+ print_json(result)
278
+ except CyleraAPIError as e:
279
+ print(f"API error: {e}", file=sys.stderr)
280
+ raise typer.Exit(1)
281
+
282
+
283
+ @app.command()
284
+ def device(
285
+ device_id: Annotated[str, typer.Argument(help="MAC address of the device")],
286
+ ) -> None:
287
+ """Get details for a specific device by MAC address."""
288
+ require_config()
289
+ try:
290
+ with get_client() as client:
291
+ result = Inventory(client).get_device(device_id)
292
+ print_json(result)
293
+ except CyleraAPIError as e:
294
+ print(f"API error: {e}", file=sys.stderr)
295
+ raise typer.Exit(1)
296
+
297
+
298
+ @app.command()
299
+ def devices(
300
+ aetitle: Annotated[Optional[str], typer.Option(
301
+ help="Complete AE Title")] = None,
302
+ device_class: Annotated[
303
+ Optional[str],
304
+ typer.Option(
305
+ "--class", help="Device class (Medical, Infrastructure, etc.)"),
306
+ ] = None,
307
+ hostname: Annotated[Optional[str], typer.Option(
308
+ help="Complete hostname")] = None,
309
+ ip_address: Annotated[
310
+ Optional[str], typer.Option(
311
+ "--ip-address", help="Partial or complete IP")
312
+ ] = None,
313
+ mac_address: MacAddressOption = None,
314
+ model: Annotated[Optional[str], typer.Option(help="Device model")] = None,
315
+ os: Annotated[Optional[str], typer.Option(help="Operating system")] = None,
316
+ page: PageOption = None,
317
+ page_size: PageSizeOption = None,
318
+ serial_number: Annotated[
319
+ Optional[str], typer.Option(
320
+ "--serial-number", help="Complete serial number")
321
+ ] = None,
322
+ since_last_seen: Annotated[
323
+ Optional[int],
324
+ typer.Option("--since-last-seen",
325
+ help="[DEPRECATED] Seconds since last seen"),
326
+ ] = None,
327
+ device_type: Annotated[
328
+ Optional[str], typer.Option(
329
+ "--type", help="Device type (EEG, X-Ray, etc.)")
330
+ ] = None,
331
+ vendor: Annotated[Optional[str], typer.Option(
332
+ help="Device vendor")] = None,
333
+ first_seen_before: Annotated[
334
+ Optional[int], typer.Option(
335
+ "--first-seen-before", help="Epoch timestamp")
336
+ ] = None,
337
+ first_seen_after: Annotated[
338
+ Optional[int], typer.Option(
339
+ "--first-seen-after", help="Epoch timestamp")
340
+ ] = None,
341
+ last_seen_before: Annotated[
342
+ Optional[int], typer.Option(
343
+ "--last-seen-before", help="Epoch timestamp")
344
+ ] = None,
345
+ last_seen_after: Annotated[
346
+ Optional[int], typer.Option(
347
+ "--last-seen-after", help="Epoch timestamp")
348
+ ] = None,
349
+ attribute_label: Annotated[
350
+ Optional[str], typer.Option(
351
+ "--attribute-label", help="Attribute label filter")
352
+ ] = None,
353
+ ) -> None:
354
+ """Get a list of devices with optional filters."""
355
+ require_config()
356
+ try:
357
+ with get_client() as client:
358
+ result = Inventory(client).get_devices(
359
+ aetitle=aetitle,
360
+ device_class=device_class,
361
+ hostname=hostname,
362
+ ip_address=ip_address,
363
+ mac_address=mac_address,
364
+ model=model,
365
+ os=os,
366
+ page=page,
367
+ page_size=page_size,
368
+ serial_number=serial_number,
369
+ since_last_seen=since_last_seen,
370
+ device_type=device_type,
371
+ vendor=vendor,
372
+ first_seen_before=first_seen_before,
373
+ first_seen_after=first_seen_after,
374
+ last_seen_before=last_seen_before,
375
+ last_seen_after=last_seen_after,
376
+ attribute_label=attribute_label,
377
+ )
378
+ print_json(result)
379
+ except CyleraAPIError as e:
380
+ print(f"API error: {e}", file=sys.stderr)
381
+ raise typer.Exit(1)
382
+
383
+
384
+ @app.command()
385
+ def deviceattributes(
386
+ mac_address: Annotated[str, typer.Argument(help="MAC address of the device")],
387
+ ) -> None:
388
+ """Get attributes for a device by MAC address."""
389
+ require_config()
390
+ try:
391
+ with get_client() as client:
392
+ result = Inventory(client).get_device_attributes(mac_address)
393
+ print_json(result)
394
+ except CyleraAPIError as e:
395
+ print(f"API error: {e}", file=sys.stderr)
396
+ raise typer.Exit(1)
397
+
398
+
399
+ @app.command()
400
+ def procedures(
401
+ procedure_name: Annotated[
402
+ Optional[str],
403
+ typer.Option("--procedure-name",
404
+ help="Procedure name (partial match)"),
405
+ ] = None,
406
+ accession_number: Annotated[
407
+ Optional[str], typer.Option(
408
+ "--accession-number", help="Accession number")
409
+ ] = None,
410
+ device_uuid: Annotated[
411
+ Optional[str], typer.Option("--device-uuid", help="Device UUID")
412
+ ] = None,
413
+ completed_after: Annotated[
414
+ Optional[str], typer.Option(
415
+ "--completed-after", help="Date (YYYY/MM/DD)")
416
+ ] = None,
417
+ page: PageOption = None,
418
+ page_size: PageSizeOption = None,
419
+ ) -> None:
420
+ """Get a list of medical procedures."""
421
+ require_config()
422
+ try:
423
+ with get_client() as client:
424
+ result = Utilization(client).get_procedures(
425
+ procedure_name=procedure_name,
426
+ accession_number=accession_number,
427
+ device_uuid=device_uuid,
428
+ completed_after=completed_after,
429
+ page=page,
430
+ page_size=page_size,
431
+ )
432
+ print_json(result)
433
+ except CyleraAPIError as e:
434
+ print(f"API error: {e}", file=sys.stderr)
435
+ raise typer.Exit(1)
436
+
437
+
438
+ @app.command()
439
+ def subnets(
440
+ cidr_range: Annotated[
441
+ Optional[str], typer.Option(
442
+ "--cidr-range", help="CIDR range (partial match)")
443
+ ] = None,
444
+ description: Annotated[
445
+ Optional[str], typer.Option(help="Subnet description")
446
+ ] = None,
447
+ vlan: Annotated[Optional[int], typer.Option(help="VLAN number")] = None,
448
+ page: PageOption = None,
449
+ page_size: PageSizeOption = None,
450
+ ) -> None:
451
+ """Get a list of network subnets."""
452
+ require_config()
453
+ try:
454
+ with get_client() as client:
455
+ result = Network(client).get_subnets(
456
+ cidr_range=cidr_range,
457
+ description=description,
458
+ vlan=vlan,
459
+ page=page,
460
+ page_size=page_size,
461
+ )
462
+ print_json(result)
463
+ except CyleraAPIError as e:
464
+ print(f"API error: {e}", file=sys.stderr)
465
+ raise typer.Exit(1)
466
+
467
+
468
+ @app.command()
469
+ def riskmitigations(
470
+ vulnerability: Annotated[str, typer.Argument(help="Name of the vulnerability")],
471
+ ) -> None:
472
+ """Get mitigations for a specific vulnerability."""
473
+ require_config()
474
+ try:
475
+ with get_client() as client:
476
+ result = Risk(client).get_mitigations(vulnerability)
477
+ print_json(result)
478
+ except CyleraAPIError as e:
479
+ print(f"API error: {e}", file=sys.stderr)
480
+ raise typer.Exit(1)
481
+
482
+
483
+ @app.command()
484
+ def vulnerabilities(
485
+ confidence: Annotated[
486
+ Optional[str], typer.Option(help="Confidence: LOW, MEDIUM, HIGH")
487
+ ] = None,
488
+ detected_after: DetectedAfterOption = None,
489
+ mac_address: MacAddressOption = None,
490
+ name: Annotated[
491
+ Optional[str], typer.Option(help="Vulnerability name (partial match)")
492
+ ] = None,
493
+ page: PageOption = None,
494
+ page_size: PageSizeOption = None,
495
+ severity: SeverityOption = None,
496
+ status: StatusOption = None,
497
+ ) -> None:
498
+ """Get a list of vulnerabilities."""
499
+ require_config()
500
+ try:
501
+ with get_client() as client:
502
+ result = Risk(client).get_vulnerabilities(
503
+ confidence=confidence,
504
+ detected_after=detected_after,
505
+ mac_address=mac_address,
506
+ name=name,
507
+ page=page,
508
+ page_size=page_size,
509
+ severity=severity,
510
+ status=status,
511
+ )
512
+ print_json(result)
513
+ except CyleraAPIError as e:
514
+ print(f"API error: {e}", file=sys.stderr)
515
+ raise typer.Exit(1)
516
+
517
+
518
+ @app.command()
519
+ def threats(
520
+ detected_after: DetectedAfterOption = None,
521
+ mac_address: MacAddressOption = None,
522
+ name: Annotated[
523
+ Optional[str], typer.Option(help="Threat name (partial match)")
524
+ ] = None,
525
+ page: PageOption = None,
526
+ page_size: PageSizeOption = None,
527
+ severity: SeverityOption = None,
528
+ status: StatusOption = None,
529
+ ) -> None:
530
+ """Get a list of detected threats."""
531
+ require_config()
532
+ try:
533
+ with get_client() as client:
534
+ result = Threat(client).get_threats(
535
+ detected_after=detected_after,
536
+ mac_address=mac_address,
537
+ name=name,
538
+ page=page,
539
+ page_size=page_size,
540
+ severity=severity,
541
+ status=status,
542
+ )
543
+ print_json(result)
544
+ except CyleraAPIError as e:
545
+ print(f"API error: {e}", file=sys.stderr)
546
+ raise typer.Exit(1)
547
+
548
+
549
+ if __name__ == "__main__":
550
+ app()
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.4
2
+ Name: cylera-cli
3
+ Version: 1.1.0
4
+ Summary: Command-line interface for the Cylera Partner API
5
+ Author-email: Cylera <support@cylera.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Cylera/cylera-cli
8
+ Project-URL: Repository, https://github.com/Cylera/cylera-cli
9
+ Project-URL: Bug Tracker, https://github.com/Cylera/cylera-cli/issues
10
+ Keywords: cylera,medical devices,iot security,healthcare
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Intended Audience :: Healthcare Industry
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Security
21
+ Classifier: Topic :: System :: Systems Administration
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: cylera-client>=1.1.0
26
+ Requires-Dist: python-dotenv>=1.2.1
27
+ Requires-Dist: requests>=2.32.5
28
+ Requires-Dist: typer>=0.21.1
29
+ Dynamic: license-file
30
+
31
+ # Cylera CLI
32
+
33
+ A command-line interface for the [Cylera Partner API](https://github.com/Cylera/cylera-partner-api), providing read-only access to device inventory, threats, vulnerabilities, and network information.
34
+
35
+ It can be used stand-alone or integrated into an AI workflow using the bundled Claude Code skill (see below).
36
+
37
+ Alternatively, if you are looking to incorporate the power of Cylera into your AI workflows, you may also consider the [Cylera MCP Server](https://github.com/Cylera/cylera-mcp-server).
38
+
39
+ This [Cylera AI Integration Options document](CLI_VS_MCP.md) may help you decide which one may make sense depending on your AI workflows.
40
+
41
+ ## Demo
42
+ ![Demo][demo]
43
+
44
+ ## Requirements
45
+
46
+ - [uv](https://docs.astral.sh/uv/)
47
+ - Credentials for accessing Cylera
48
+
49
+ ## Installation
50
+
51
+ Install using [uv](https://docs.astral.sh/uv/):
52
+
53
+ ```bash
54
+ uv tool install .
55
+ ```
56
+
57
+ Then run commands directly:
58
+
59
+ ```bash
60
+ cylera --help
61
+ ```
62
+
63
+ To uninstall:
64
+
65
+ ```bash
66
+ uv tool uninstall cylera
67
+ ```
68
+
69
+ Alternatively, run without installing:
70
+
71
+ ```bash
72
+ uvx cylera --help
73
+ ```
74
+
75
+ ## Configuration
76
+
77
+ Run the interactive setup to configure your credentials:
78
+
79
+ ```bash
80
+ cylera init
81
+ ```
82
+
83
+ This will prompt you to:
84
+ 1. Select your Cylera API endpoint (US, UK, or Demo)
85
+ 2. Enter your username (email)
86
+ 3. Enter your password
87
+
88
+ Credentials are stored in a `.env` file in the current directory.
89
+
90
+ ### Manual Configuration
91
+
92
+ Alternatively, set environment variables directly:
93
+
94
+ ```bash
95
+ export CYLERA_BASE_URL="https://partner.us1.cylera.com/"
96
+ export CYLERA_USERNAME="your-email@example.com"
97
+ export CYLERA_PASSWORD="your-password"
98
+ ```
99
+
100
+ ### Doppler Secrets Management
101
+
102
+ Instead of storing the secrets in a .env file, you may choose to use a secrets
103
+ management solution such as [Doppler](https://www.doppler.com).
104
+
105
+ To use [Doppler](https://www.doppler.com), simply add the following prefix to all commands.
106
+
107
+ doppler run --
108
+
109
+ For example, to run "cylera devices" accessing secrets from Doppler, you would
110
+ run the following:
111
+
112
+ doppler run -- cylera devices
113
+
114
+ ### 1Password Secrets Management
115
+
116
+ Alternatively, you can use [1Password CLI](https://developer.1password.com/docs/cli/get-started/)
117
+ for secrets management. Set your environment ID and prefix commands with `op run`:
118
+
119
+ ```bash
120
+ export OP_ENVIRONMENT_ID=<your-environment-id>
121
+ op run --environment "$OP_ENVIRONMENT_ID" -- cylera devices
122
+ ```
123
+
124
+ ## Usage
125
+
126
+ ```bash
127
+ cylera <command> [options]
128
+ ```
129
+
130
+ ### Available Commands
131
+
132
+ | Command | Description |
133
+ |---------|-------------|
134
+ | `init` | Initialize Cylera CLI configuration interactively |
135
+ | `organization` | Get the organization associated with the current credentials |
136
+ | `organizations` | List organizations available to switch into |
137
+ | `switchorg` | Switch to a different organization |
138
+ | `resetorg` | Reset organization back to home |
139
+ | `device` | Get details for a specific device by MAC address |
140
+ | `devices` | Get a list of devices with optional filters |
141
+ | `deviceattributes` | Get attributes for a device by MAC address |
142
+ | `procedures` | Get a list of medical procedures |
143
+ | `subnets` | Get a list of network subnets |
144
+ | `riskmitigations` | Get mitigations for a specific vulnerability |
145
+ | `vulnerabilities` | Get a list of vulnerabilities |
146
+ | `threats` | Get a list of detected threats |
147
+
148
+ #### Organization
149
+
150
+ **Get organization info:**
151
+ ```bash
152
+ cylera organization
153
+ ```
154
+
155
+ **List available organizations:**
156
+ ```bash
157
+ cylera organizations
158
+ ```
159
+
160
+ **Switch to a different organization:**
161
+ ```bash
162
+ cylera switchorg <organization-id>
163
+ ```
164
+
165
+ **Reset back to home organization:**
166
+ ```bash
167
+ cylera resetorg
168
+ ```
169
+
170
+ #### Device Inventory
171
+
172
+ **List devices:**
173
+ ```bash
174
+ cylera devices --page-size 10
175
+ cylera devices --vendor Philips --class Medical
176
+ cylera devices --ip-address 10.40
177
+ ```
178
+
179
+ **Get a specific device:**
180
+ ```bash
181
+ cylera device 7f:14:22:72:00:e5
182
+ ```
183
+
184
+ **Get device attributes:**
185
+ ```bash
186
+ cylera deviceattributes 7f:14:22:72:00:e5
187
+ ```
188
+
189
+ #### Vulnerabilities & Risk
190
+
191
+ **List vulnerabilities:**
192
+ ```bash
193
+ cylera vulnerabilities --severity CRITICAL
194
+ cylera vulnerabilities --status OPEN --page-size 20
195
+ ```
196
+
197
+ **Get mitigations for a vulnerability:**
198
+ ```bash
199
+ cylera riskmitigations "Ripple20 (ICSA-20-168-01)"
200
+ ```
201
+
202
+ #### Threats
203
+
204
+ **List threats:**
205
+ ```bash
206
+ cylera threats --severity HIGH
207
+ cylera threats --mac-address bb:b0:71:cf:30:0a
208
+ ```
209
+
210
+ #### Network
211
+
212
+ **List subnets:**
213
+ ```bash
214
+ cylera subnets
215
+ cylera subnets --vlan 477
216
+ ```
217
+
218
+ #### Medical Procedures
219
+
220
+ **List procedures:**
221
+ ```bash
222
+ cylera procedures --page-size 10
223
+ cylera procedures --completed-after 2025/01/01
224
+ ```
225
+
226
+ ### Common Options
227
+
228
+ Most list commands support these options:
229
+
230
+ | Option | Description |
231
+ |--------|-------------|
232
+ | `--page` | Page number for pagination |
233
+ | `--page-size` | Results per page (max 100) |
234
+ | `--mac-address` | Filter by device MAC address |
235
+ | `--severity` | Filter by severity (INFO, LOW, MEDIUM, HIGH, CRITICAL) |
236
+ | `--status` | Filter by status (OPEN, IN_PROGRESS, RESOLVED, SUPPRESSED) |
237
+
238
+ ### Output
239
+
240
+ All commands output JSON to stdout, which can be piped to tools like `jq`:
241
+
242
+ ```bash
243
+ cylera devices --page-size 5 | jq '.devices[].hostname'
244
+ ```
245
+
246
+ ## Debugging
247
+
248
+ Enable debug output to see request details:
249
+
250
+ ```bash
251
+ DEBUG=1 cylera devices --page-size 1
252
+ ```
253
+
254
+ ## API Endpoints
255
+
256
+ The CLI supports these Cylera Partner API regions:
257
+
258
+ - US: `https://partner.us1.cylera.com/`
259
+ - UK: `https://partner.uk1.cylera.com/`
260
+ - Demo: `https://partner.demo.cylera.com/`
261
+
262
+ ## Claude Code Skills
263
+
264
+ This repo includes Claude Code skills for querying Cylera data and exporting results to CSV directly from a conversation — no scripting required.
265
+
266
+ | Skill | Description |
267
+ |-------|-------------|
268
+ | `/cylera-attributes` | Find devices by attribute label (e.g. TeamViewer, end of life) |
269
+ | `/cylera-vulnerabilities` | Export vulnerabilities filtered by severity and/or status |
270
+ | `/cylera-threats` | Export threats filtered by severity and/or status |
271
+ | `/cylera-devices` | Export device inventory filtered by class, vendor, type, or OS |
272
+
273
+ See [CODING_AGENT_SKILLS.md](CODING_AGENT_SKILLS.md) for full installation instructions and usage examples.
274
+
275
+ ## License
276
+
277
+ See LICENSE file for details.
278
+
279
+ [demo]: demo.webp
@@ -0,0 +1,7 @@
1
+ cylera.py,sha256=qUPdFvVQz1e-R6gkI8OlcgQmO39X-OsgxFMRCAKR6aQ,17128
2
+ cylera_cli-1.1.0.dist-info/licenses/LICENSE,sha256=z5MXeo8SX4SGe9m8d32Y_xk3qmlxZN7qR1wBNkgoafI,1063
3
+ cylera_cli-1.1.0.dist-info/METADATA,sha256=9PhCXB2Hw8i95eW9I9wsreE6_jYYi86_sCa4biPsRaM,7261
4
+ cylera_cli-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ cylera_cli-1.1.0.dist-info/entry_points.txt,sha256=faFTlCYuXBa9t9YPxLDDZ8REjP253QhWuvnatVyLs-0,38
6
+ cylera_cli-1.1.0.dist-info/top_level.txt,sha256=-zMu-_6aNhoqo5R-MIX843HvPdGG5VA4Q5Pfvc9aQ68,7
7
+ cylera_cli-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cylera = cylera:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Cylera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cylera