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.
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/PKG-INFO +1 -1
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/pyproject.toml +1 -1
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/incidents.py +38 -12
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/local.py +11 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/operations.py +11 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/sdk_client.py +26 -10
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/LICENSE +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/README.md +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/__init__.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/client.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/README.md +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/__init__.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/command-styling.md +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/commit.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/context.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/deployment.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/identity.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/insights.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/jobs.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/mobile_agent.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/network.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/objects.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/posture.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/security.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/commands/setup.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/main.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/tests/__init__.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/__init__.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/config.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/context.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/decorators.py +0 -0
- {pan_scm_cli-1.3.2 → pan_scm_cli-1.3.4}/src/scm_cli/utils/validators.py +0 -0
|
@@ -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=
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
if
|
|
140
|
-
typer.echo(
|
|
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
|
-
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
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
|
-
|
|
16344
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|