pan-scm-cli 1.3.2__tar.gz → 1.3.4__tar.gz

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.
Files changed (32) hide show
  1. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/PKG-INFO +1 -1
  2. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/pyproject.toml +1 -1
  3. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/incidents.py +38 -12
  4. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/local.py +11 -0
  5. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/operations.py +11 -0
  6. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/sdk_client.py +26 -10
  7. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/LICENSE +0 -0
  8. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/README.md +0 -0
  9. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/__init__.py +0 -0
  10. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/client.py +0 -0
  11. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/README.md +0 -0
  12. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/__init__.py +0 -0
  13. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/command-styling.md +0 -0
  14. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/commit.py +0 -0
  15. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/context.py +0 -0
  16. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/deployment.py +0 -0
  17. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/identity.py +0 -0
  18. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/insights.py +0 -0
  19. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/jobs.py +0 -0
  20. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/mobile_agent.py +0 -0
  21. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/network.py +0 -0
  22. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/objects.py +0 -0
  23. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/posture.py +0 -0
  24. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/security.py +0 -0
  25. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/setup.py +0 -0
  26. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/main.py +0 -0
  27. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/tests/__init__.py +0 -0
  28. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/__init__.py +0 -0
  29. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/config.py +0 -0
  30. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/context.py +0 -0
  31. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/decorators.py +0 -0
  32. {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pan-scm-cli
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: CICD and Network Engineer-friendly CLI tool for Palo Alto Networks Strata Cloud Manager
5
5
  License-File: LICENSE
6
6
  Author: Calvin Remsburg
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pan-scm-cli"
3
- version = "1.3.2"
3
+ version = "1.3.4"
4
4
  description = "CICD and Network Engineer-friendly CLI tool for Palo Alto Networks Strata Cloud Manager"
5
5
  authors = ["Calvin Remsburg <dev@cdot.io>"]
6
6
  readme = "README.md"
@@ -5,6 +5,7 @@ from the SCM Unified Incident Framework.
5
5
  """
6
6
 
7
7
  import json
8
+ from datetime import datetime, timezone
8
9
 
9
10
  import typer
10
11
  from rich.console import Console
@@ -29,6 +30,17 @@ PRODUCT_OPTION = typer.Option(None, "--product", "-p", help="Filter by product n
29
30
  JSON_OPTION = typer.Option(False, "--json", "-j", help="Output as JSON")
30
31
 
31
32
 
33
+ def _format_epoch(epoch: int | str | None) -> str:
34
+ """Convert epoch timestamp (seconds or ms) to human-readable date."""
35
+ if epoch is None:
36
+ return ""
37
+ if isinstance(epoch, str):
38
+ return epoch
39
+ if epoch > 1_000_000_000_000:
40
+ return datetime.fromtimestamp(epoch / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
41
+ return datetime.fromtimestamp(epoch, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
42
+
43
+
32
44
  # =============================================================================================================================================================================================
33
45
  # INCIDENTS COMMANDS
34
46
  # =============================================================================================================================================================================================
@@ -64,13 +76,13 @@ def list_incidents(
64
76
  typer.echo("No incidents found")
65
77
  return
66
78
 
67
- table = Table(title="Security Incidents")
79
+ table = Table(title="Security Incidents", show_lines=True)
68
80
  table.add_column("ID", style="cyan", no_wrap=True)
69
- table.add_column("Status", style="white")
70
- table.add_column("Severity", style="white")
71
- table.add_column("Product", style="white")
72
- table.add_column("Title", style="dim", max_width=40)
73
- table.add_column("Raised", style="white")
81
+ table.add_column("Status", style="white", no_wrap=True)
82
+ table.add_column("Severity", style="white", no_wrap=True)
83
+ table.add_column("Product", style="white", no_wrap=True)
84
+ table.add_column("Title", style="dim", max_width=50)
85
+ table.add_column("Raised", style="white", no_wrap=True)
74
86
 
75
87
  severity_styles = {"critical": "red bold", "high": "red", "medium": "yellow", "low": "green", "informational": "dim"}
76
88
 
@@ -85,7 +97,7 @@ def list_incidents(
85
97
  f"[{sev_style}]{sev}[/{sev_style}]",
86
98
  str(inc.get("product", "")),
87
99
  str(inc.get("title", "")),
88
- str(inc.get("raised_time", "")),
100
+ _format_epoch(inc.get("raised_time")),
89
101
  )
90
102
 
91
103
  console.print(table)
@@ -122,8 +134,8 @@ def show_incident(
122
134
  typer.echo(f"Status: {incident.get('status', '')}")
123
135
  typer.echo(f"Severity: {incident.get('severity', '')}")
124
136
  typer.echo(f"Product: {incident.get('product', '')}")
125
- typer.echo(f"Raised: {incident.get('raised_time', '')}")
126
- typer.echo(f"Updated: {incident.get('updated_time', '')}")
137
+ typer.echo(f"Raised: {_format_epoch(incident.get('raised_time'))}")
138
+ typer.echo(f"Updated: {_format_epoch(incident.get('updated_time'))}")
127
139
  typer.echo(f"Title: {incident.get('title', '')}")
128
140
 
129
141
  alerts = incident.get("alerts", [])
@@ -135,9 +147,23 @@ def show_incident(
135
147
  state = alert.get("state", "")
136
148
  typer.echo(f" {i}. [{sev}] {title} ({state})")
137
149
 
138
- remediations = incident.get("remediations", "")
139
- if remediations:
140
- typer.echo(f"\nRemediations:\n {remediations}")
150
+ remediations_raw = incident.get("remediations", "")
151
+ if remediations_raw:
152
+ typer.echo("\nRemediation:")
153
+ try:
154
+ parsed = json.loads(remediations_raw) if isinstance(remediations_raw, str) else remediations_raw
155
+ steps = parsed.get("remediations", []) if isinstance(parsed, dict) else []
156
+ for rem in steps:
157
+ dc = rem.get("dynamic_content", {})
158
+ for j, step in enumerate(dc.get("steps", []), 1):
159
+ typer.echo(f" {j}. {step.get('title', '').strip()}")
160
+ desc = step.get("description", "").strip()
161
+ if desc:
162
+ typer.echo(f" {desc}")
163
+ if not steps:
164
+ typer.echo(f" {remediations_raw}")
165
+ except (json.JSONDecodeError, AttributeError):
166
+ typer.echo(f" {remediations_raw}")
141
167
 
142
168
  typer.echo()
143
169
 
@@ -67,6 +67,17 @@ def list_versions(
67
67
 
68
68
  console.print(table)
69
69
 
70
+ except ValueError as e:
71
+ if "Invalid error response format" in str(e):
72
+ typer.echo(
73
+ "Error: The Local Config API returned 404. "
74
+ "This API may not be available for your SCM tenant or device type. "
75
+ "Contact Palo Alto Networks support to verify Local Config API access.",
76
+ err=True,
77
+ )
78
+ else:
79
+ typer.echo(f"Error listing config versions: {e!s}", err=True)
80
+ raise typer.Exit(code=1) from e
70
81
  except Exception as e:
71
82
  typer.echo(f"Error listing config versions: {e!s}", err=True)
72
83
  raise typer.Exit(code=1) from e
@@ -73,6 +73,17 @@ def _run_operation(device: str, operation: str, async_mode: bool, timeout: int)
73
73
 
74
74
  console.print(table)
75
75
 
76
+ except ValueError as e:
77
+ if "Invalid error response format" in str(e):
78
+ typer.echo(
79
+ f"Error: The Operations API returned 404 for {operation}. "
80
+ "This API may not be available for your SCM tenant or device type. "
81
+ "Contact Palo Alto Networks support to verify Operations API access.",
82
+ err=True,
83
+ )
84
+ else:
85
+ typer.echo(f"Error running {operation}: {e!s}", err=True)
86
+ raise typer.Exit(code=1) from e
76
87
  except Exception as e:
77
88
  typer.echo(f"Error running {operation}: {e!s}", err=True)
78
89
  raise typer.Exit(code=1) from e
@@ -16036,10 +16036,10 @@ class SCMClient:
16036
16036
  _SERIAL_PATTERN = __import__("re").compile(r"^\d{14,15}$")
16037
16037
 
16038
16038
  def resolve_device_serial(self, device: str) -> str:
16039
- """Resolve a device name or serial number to a serial number.
16039
+ """Resolve a device name, hostname, or serial number to a serial number.
16040
16040
 
16041
16041
  Args:
16042
- device: Device name or serial number.
16042
+ device: Device hostname, display name, or serial number.
16043
16043
 
16044
16044
  Returns:
16045
16045
  str: The 14-15 digit device serial number.
@@ -16057,12 +16057,14 @@ class SCMClient:
16057
16057
  return "007951000123456"
16058
16058
 
16059
16059
  try:
16060
- result = self.client.device.fetch(name=device)
16061
- if result is None:
16062
- raise ValueError(f"Device '{device}' not found in SCM")
16063
- serial = result.id
16064
- self.logger.info(f"Resolved '{device}' to serial {serial}")
16065
- return serial
16060
+ all_devices = self.client.device.list()
16061
+ search = device.lower()
16062
+ for d in all_devices:
16063
+ if any(search == (getattr(d, field, None) or "").lower() for field in ("hostname", "display_name", "name", "serial_number")):
16064
+ self.logger.info(f"Resolved '{device}' to serial {d.id}")
16065
+ return d.id
16066
+ available = [f" {d.hostname or d.display_name or d.name} ({d.id})" for d in all_devices]
16067
+ raise ValueError(f"Device '{device}' not found. Available devices:\n" + "\n".join(available))
16066
16068
  except ValueError:
16067
16069
  raise
16068
16070
  except Exception as e:
@@ -16340,8 +16342,22 @@ class SCMClient:
16340
16342
  return self._MOCK_INCIDENTS[0]
16341
16343
 
16342
16344
  try:
16343
- result = self.client.incidents.get_details(incident_id=incident_id)
16344
- return json.loads(result.model_dump_json(exclude_unset=True))
16345
+ # Bypass SDK's get_details() which has a parsing bug (passes whole
16346
+ # response to model instead of extracting data[0] from the wrapper).
16347
+ session = self.client.oauth_client.session
16348
+ base = self.client.api_base_url
16349
+ resp = session.get(
16350
+ f"{base}/incidents/v1/details/{incident_id}",
16351
+ headers={"X-PANW-Region": getattr(self.client, "_region", "americas")},
16352
+ )
16353
+ resp.raise_for_status()
16354
+ body = resp.json()
16355
+ items = body.get("data", [])
16356
+ if not items:
16357
+ raise ValueError(f"Incident {incident_id} not found")
16358
+ return items[0]
16359
+ except ValueError:
16360
+ raise
16345
16361
  except Exception as e:
16346
16362
  self._handle_api_exception("fetching", "N/A", f"incident {incident_id}", e)
16347
16363
 
File without changes
File without changes