PAN-OS_Security_Advisories_Enrichment
This script enriches CVEs published by Palo Alto Networks with detailed vulnerability information from the official Palo Alto Networks Security Advisories website https://security.paloaltonetworks.com.
python · PAN-OS by Palo Alto Networks
Details
| ID | PAN-OS_Security_Advisories_Enrichment |
|---|---|
| Language | python |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
README
PANOSSecurityAdvisories_Enrichment
This script enriches CVEs published by Palo Alto Networks with detailed vulnerability information from the official Palo Alto Networks Security Advisories website https://security.paloaltonetworks.com.
Description
The script retrieves comprehensive vulnerability data including CVSS scores, affected products, version information, exploits, workarounds, and solutions from Palo Alto Networks’ security advisories. It supports both CVE identifiers and PAN-SA advisory IDs, automatically determining the appropriate data source and format.
Inputs
| Argument Name | Description | Required |
|---|---|---|
| cve_id | CVE ID(s) or PAN-SA advisory ID(s) to enrich (array) | Required |
Outputs
| Path | Type | Description |
|---|---|---|
| PANOSSecurityAdvisories.Advisory.cve_id | String | CVE ID |
| PANOSSecurityAdvisories.Advisory.title | String | CVE Title |
| PANOSSecurityAdvisories.Advisory.description | String | Vulnerability description |
| PANOSSecurityAdvisories.Advisory.cve_url | String | Link to the PANW Security Advisories page |
| PANOSSecurityAdvisories.Advisory.cvss_score | Number | Base score of CVE |
| PANOSSecurityAdvisories.Advisory.cvss_severity | String | Base severity of CVE (LOW, MEDIUM, HIGH, CRITICAL) |
| PANOSSecurityAdvisories.Advisory.cvethreatscore | Number | Threat Score of the CVE |
| PANOSSecurityAdvisories.Advisory.cvethreatseverity | String | Threat Severity of CVE (LOW, MEDIUM, HIGH, CRITICAL) |
| PANOSSecurityAdvisories.Advisory.cvss_vector_string | String | CVSS Vector indicating metrics of attack |
| PANOSSecurityAdvisories.Advisory.cvss_table | Unknown | Metrics of the vulnerability |
| PANOSSecurityAdvisories.Advisory.affected_list | Unknown | List of affected products, their versions and changes introduced with fixes |
| PANOSSecurityAdvisories.Advisory.cveproductstatus | Unknown | List of affected products with platform information and fixed versions |
| PANOSSecurityAdvisories.Advisory.cpes | Unknown | Affected products defined by CPE |
| PANOSSecurityAdvisories.Advisory.published_date | Date | Date when it was published to the advisories page |
| PANOSSecurityAdvisories.Advisory.last_updated_date | Date | Date when it was last updated on the advisories page |
| PANOSSecurityAdvisories.Advisory.solution | String | Solution provided for the CVE |
| PANOSSecurityAdvisories.Advisory.workaround | String | Workaround for the CVE |
| PANOSSecurityAdvisories.Advisory.configurations | String | Required configurations for exploit |
| PANOSSecurityAdvisories.Advisory.exploits | String | Known exploits of this vulnerability in the field |
| PANOSSecurityAdvisories.Advisory.impact | String | Impact description of the vulnerability |
| PANOSSecurityAdvisories.Advisory.external_cve_list | Unknown | If input CVE is a PAN-SA advisory then list of related non-PANW CVEs |
Context Example
{
"PANOSSecurityAdvisories": {
"Advisory": {
"cve_id": "CVE-2024-1234",
"title": "OS Command Injection Vulnerability in PAN-OS",
"description": "A command injection vulnerability in PAN-OS allows...",
"cve_url": "https://security.paloaltonetworks.com/CVE-2024-1234",
"cvss_score": 9.8,
"cvss_severity": "CRITICAL",
"cvethreatscore": 9.8,
"cvethreatseverity": "CRITICAL",
"cvss_vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"affected_list": [
{
"product": "PAN-OS",
"platforms": [""],
"versions": [
{
"version": "10.2.0",
"lessThan": "10.2.4-h16",
"status": "affected",
"changes": [
{
"at": "10.2.4-h16",
"status": "unaffected"
}
]
}
]
}
],
"published_date": "2024-04-10T16:00:00.000Z",
"last_updated_date": "2024-04-10T16:00:00.000Z",
"solution": "This issue is fixed in PAN-OS 10.2.4-h16, PAN-OS 11.0.1, and all later PAN-OS versions.",
"workaround": "Enable Threat Prevention on all security rules...",
"external_cve_list": []
}
}
}
Human Readable Output
| Field | Value |
|---|---|
| CVE ID | CVE-2024-1234 |
| Title | OS Command Injection Vulnerability in PAN-OS |
| CVSS Score | 9.8 |
| Severity | CRITICAL |
| Published Date | 2024-04-10T16:00:00.000Z |
| Solution | This issue is fixed in PAN-OS 10.2.4-h16, PAN-OS 11.0.1, and all later PAN-OS versions. |
Notes
- The script automatically handles both CVE and PAN-SA format inputs.
- For PAN-SA advisories, the script attempts to retrieve additional CSAF (Common Security Advisory Framework) data.
- Version information is parsed and sorted to provide clear affected/unaffected status.
- CVSS metrics are prioritized by score (highest first) when multiple metrics are available.
- External CVE references are included for PAN-SA advisories when available.
- The script connects to the Palo Alto Networks Security Advisories page for data retrieval.
import re import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 OUTPUT_PREFIX = "PANOSSecurityAdvisories" BASE_URL = "https://security.paloaltonetworks.com" CVE_JSON = "/json/" CVE_CSAF = "/csaf/" class Client: """The client that connects to the advisories JSON endpoint""" def __init__(self): self.base_url = BASE_URL self.advisories_url = f"{self.base_url}{CVE_JSON}" self.csaf_url = f"{self.base_url}{CVE_CSAF}" def get_cve(self, cve_id: str): """ Gets a specific CVE advisory """ url = f"{self.advisories_url}{cve_id}" response = requests.get(url) if response.status_code != 404 and response.status_code != 200: response.raise_for_status() return response.json() def get_pan_sa_advisories(self, pan_sa_id: str): """ Gets a specific PAN-SA advisory """ url = f"{self.csaf_url}{pan_sa_id}" response = requests.get(url) if response.status_code == 404: demisto.debug(f"CSAF not available for {pan_sa_id}") return f"CSAF not available for {pan_sa_id}" response.raise_for_status() return response.json() def parse_version(version: str) -> tuple[int, ...]: parts = re.match(r"(\d+)\.(\d+)\.(\d+)(?:-h(\d+))?", version) if parts: return tuple(int(p) if p else 0 for p in parts.groups()) return (0, 0, 0, 0) def sort_versions_and_changes(data: List[Dict]) -> List[Dict]: for item in data: if "versions" in item: item["versions"] = sorted(item["versions"], key=lambda x: parse_version(x["version"]), reverse=True) for version in item["versions"]: if "changes" in version: version["changes"] = sorted(version["changes"], key=lambda x: parse_version(x["at"])) return data def create_product_platform_tables(sorted_data: List[Dict]) -> List[Dict]: result = [] for item in sorted_data: product = item.get("product", "") platforms = item.get("platforms", []) default_status = item.get("defaultStatus", "Unknown") for version in item.get("versions", []): start_version = version.get("version", "") end_version = version.get("lessThan") or version.get("lessThanOrEqual", "") status = version.get("status", default_status) for platform in platforms or [""]: version_key = f"{product}{' - ' + platform if platform else ''} - {start_version}" affected_versions = [] unaffected_versions = [] unknown_versions = [] if not end_version: # Single version case if status == "affected": affected_versions.append("All") unaffected_versions.append("None") elif status == "unaffected": unaffected_versions.append("All") affected_versions.append("None") else: unknown_versions.append(start_version) else: # Range case range_str = f"{start_version} - {end_version}" changes = sorted(version.get("changes", []), key=lambda x: parse_version(x["at"])) for change in changes: change_version = change.get("at", "") change_status = change.get("status", "") if change_status == "unaffected": affected_versions.append(f"< {change_version}") unaffected_versions.append(f">= {change_version}") unknown_versions.append("N/A") elif change_status == "affected": affected_versions.append(f">= {change_version}") unaffected_versions.append(f"< {change_version}") unknown_versions.append("N/A") elif change_status == "unknown": unknown_versions.append(f"< {change_version}") unknown_versions.append(f">= {change_version}") # Add the default status for the range if no changes if not changes: if status == "affected": affected_versions.append(range_str) unaffected_versions.append("None") unknown_versions.append("N/A") elif status == "unaffected": unaffected_versions.append(range_str) affected_versions.append("None") unknown_versions.append("N/A") else: unknown_versions.append(range_str) affected_versions.append("N/A") unaffected_versions.append("N/A") result.append( { "Product": version_key, "Affected": affected_versions, "Unaffected": unaffected_versions, "Unknown": unknown_versions, } ) return result def flatten_advisory_dict(advisory_dict: dict, external_cves: list) -> Dict: """Given a dictionary advisory, return a flattened dictionary object with required CVE parameters""" if "cveMetadata" in advisory_dict: # CVE-2012-6602.json format cna = advisory_dict.get("containers", {}).get("cna", {}) metrics = sorted( cna.get("metrics", [{}]), key=lambda x: x.get("cvssV3_1", {}).get("baseScore", 0) or x.get("cvssV4_0", {}).get("baseScore", 0), reverse=True, ) top_metric = metrics[0].get("cvssV3_1", {}) or metrics[0].get("cvssV4_0", {}) affected_list = cna.get("affected", []) return { "cve_id": advisory_dict.get("cveMetadata", {}).get("cveId", ""), "title": cna.get("title", ""), "description": cna.get("descriptions", [{}])[0].get("value", ""), "cvss_score": top_metric.get("baseScore"), "cvethreatscore": top_metric.get("threatScore"), "cvethreatseverity": top_metric.get("threatSeverity"), "cvss_severity": top_metric.get("baseSeverity", ""), "cvss_vector_string": [ metric.get("cvssV3_1", {}).get("vectorString", "") or metric.get("cvssV4_0", {}).get("vectorString", "") for metric in metrics ], "affected_list": sort_versions_and_changes(affected_list), "published_date": advisory_dict.get("cveMetadata", {}).get("datePublished", "") or cna.get("datePublic", ""), "last_updated_date": max((event.get("time") for event in cna.get("timeline", [])), default="") or cna.get("providerMetadata", {}).get("dateUpdated", ""), "workaround": "\n".join([w.get("value", "") for w in cna.get("workarounds", [{}])]), "configurations": "\n".join([cfg.get("value", "") for cfg in cna.get("configurations", [])]), "exploits": "\n".join([e.get("value", "") for e in cna.get("exploits", [])]), "cvss_table": [{"metrics": k, "value": v} for k, v in top_metric.items()], "solution": cna.get("solutions", [{}])[0].get("value", ""), "cve_url": "https://security.paloaltonetworks.com/{}".format(advisory_dict.get("cveMetadata", {}).get("cveId", "")), "impact": cna.get("impacts", [{}])[0].get("descriptions", [{}])[0].get("value", ""), "cpes": [cpe for affected in affected_list if "cpes" in affected for cpe in affected.get("cpes", [])], "cveproductstatus": create_product_platform_tables(affected_list), "external_cve_list": external_cves, } else: return {} def get_external_cves(client: Client, pan_sa_id: str) -> List[Dict[str, str]]: if not re.match(r"^PAN-SA-\d{4}-\d{4}$", pan_sa_id): raise ValueError("Invalid PAN-SA ID format") response = client.get_pan_sa_advisories(pan_sa_id) # If response is a string (meaning CSAF not available), return empty list if isinstance(response, str): demisto.info(response) return [] external_cves = [] if "vulnerabilities" in response: for vuln in response["vulnerabilities"]: cve = vuln.get("cve") if cve and "CVE-" in cve: external_link = next( (ref["url"] for ref in vuln.get("references", []) if ref.get("category") == "external"), None ) cve_text = next((note["text"] for note in vuln.get("notes", []) if note.get("category") == "description"), None) if cve and external_link and cve_text: external_cves.append({"id": cve, "link": external_link, "description": cve_text}) return external_cves def enrich_cve(client: Client, cve_id: str) -> Dict: """ Enriches a specific CVE with data from Palo Alto Networks """ cve_data = client.get_cve(cve_id) if "PAN-SA" in cve_id: external_cves = get_external_cves(client, cve_id) else: external_cves = [] # Initialize as empty list instead of empty dict if "error" not in cve_data: advisory = flatten_advisory_dict(cve_data, external_cves) else: advisory = {"error": "This is not a valid Palo Alto Networks CVE ID"} return { "Type": entryTypes["note"], "EntryContext": {f"{OUTPUT_PREFIX}.Advisory": advisory}, "Contents": advisory, "ContentsFormat": formats["json"], "HumanReadable": tableToMarkdown("CVE Vulnerability Assessment", advisory), "ReadableContentsFormat": formats["markdown"], } def main(): """Main entrypoint for script""" client = Client() args = demisto.args() try: cve_ids = argToList(args.get("cve_id", "")) for cve_id in cve_ids: result = enrich_cve(client, cve_id.upper()) if "error" in result: return_results(result.get("error")) else: return_results(result) except Exception as err: return_error(str(err)) if __name__ in ("__main__", "__builtin__", "builtins"): main()