Details
| ID | CIRCL CVE Search |
|---|---|
| Provider | Open Source |
| Category | Vulnerability Management |
| From Version | 6.5.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Searches for CVE information using circl.lu.
Configure CIRCL CVE Search in Cortex
| Parameter | Description | Required |
|---|---|---|
| Source Reliability | Reliability of the source providing the intelligence data. | True |
| Server URL | True | |
| Use system proxy settings | False | |
| Trust any certificate (not secure) | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
cve-latest
Deprecated. use NVD feed 2.0 instead.
Returns the latest updated CVEs.
Base Command
cve-latest
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | The maximum number of CVEs to display. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CVE.ID | String | The ID of the CVE. |
| CVE.CVSS | Number | The CVSS score of the CVE. |
| CVE.Published | Date | The date the CVE was published. |
| CVE.Modified | Date | The date that the CVE was last modified. |
| CVE.Description | String | The description of the CVE. |
| DBotScore.Indicator | String | The indicator value. |
| DBotScore.Score | Number | The indicator score. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor reporting the score of the indicator. |
Human Readable Output
| CVSS | The CVE CVSS Score |
| Description | CVE Description |
| ID | CVE ID |
| Modified | The date the CVE was modified |
| Published | The date the CVE was published |
cve
Returns CVE information by CVE ID.
Base Command
cve
Input
| Argument Name | Description | Required |
|---|---|---|
| cve | A comma-separated list of CVE IDs to search | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| CVE.ID | String | The ID of the CVE. |
| CVE.CVSS.Score | Number | The CVSS score of the CVE. |
| CVE.CVSS.Vector | String | The CVSS vector of the CVE. |
| CVE.CVSS.Table | Dict | The CVSS table of the CVE. |
| CVE.Published | Date | The date the CVE was published. |
| CVE.Modified | Date | The date that the CVE was last modified. |
| CVE.Description | String | The description of the CVE. |
| CVE.vulnerableconfigurations | Dict | Vulnerable configurations in CPE format |
| CVE.vulnerableproduct | Dict | Vulnerable products in CPE format |
| CVE.Tags | List | A list of tags |
| CVE.Relationships | List | List of relationships for the CVE |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
Command Example
!cve cve=CVE-2014-1234
Context Example
{
"CVE": [
{
"ID": "CVE-2014-1234",
"Published": "2014-01-10T12:02:00",
"CVSS": 2.1,
"Modified": "2014-01-10T17:57:00",
"Description": "The paratrooper-newrelic gem 1.0.1 for Ruby allows local users to obtain the X-Api-Key value by listing the curl process."
}
]
}
Human Readable Output
| CVSS | 4.3 |
| Description | XSS in livehelperchat in GitHub repository livehelperchat/livehelperchat prior to 3.97. This vulnerability has the potential to deface websites, result in compromised user accounts, and can run malicious code on web pages, which can lead to a compromise of the user’s device. |
| ID | CVE-2022-1234 |
| Modified | 2022-04-13T15:03:00 |
| Published | 2022-04-06T04:15:00 |
Configuration parameters
integration_reliability— Source Reliability (required)url— Server URL (required)proxy— Use system proxy settingsinsecure— Trust any certificate (not secure)
Commands (2)
-
cveReturns CVE information by CVE ID.
-
cve-latestDeprecatedReturns the latest updated CVEs. Deprecated. Use the NVD Feed v2.0 integration instead
import urllib3 from typing import Any import re from CommonServerPython import * import contextlib # Disable insecure warnings urllib3.disable_warnings() class Client(BaseClient): """ Client will implement the service API, and should not contain any Demisto logic. Should only do requests and return data. """ def __init__(self, base_url: str, verify: bool, proxy: bool): headers = {"Content-Type": "application/json", "Accept": "application/json"} super().__init__(base_url=base_url, headers=headers, verify=verify, proxy=proxy) def cve(self, cve_id) -> dict[str, Any]: return self._http_request(method="GET", url_suffix=f"cve/{cve_id}") def cve_latest(self, limit) -> list[dict[str, Any]]: return self._http_request(method="GET", url_suffix=f"/last/{limit}") def detect_format(cve_data: dict) -> str: """ Detects the format of a given CVE data dictionary. Supports detection of: - CVE 5.1 format (e.g., NVD, VulDB) - Legacy format (old CIRCL style) - CSAF format - GHSA format Args: cve_data: The CVE data as a dictionary. Returns: A string representing the CVE format: 'cve_5_1' or 'legacy'. Raises: ValueError: If the format is unrecognized. """ if "cveMetadata" in cve_data: demisto.debug("CVE 5.1 format") return "cve_5_1" elif "document" in cve_data and "vulnerabilities" in cve_data: demisto.debug("CVE CSAF format") return "csaf" elif "schema_version" in cve_data and "id" in cve_data: demisto.debug("CVE GHSA format") return "ghsa" elif "sourceIdentifier" in cve_data: demisto.debug("CVE NVD 5.1 format") return "nvd_cve_5_1" elif "id" in cve_data and "summary" in cve_data: demisto.debug("CVE legacy format") return "legacy" else: return "Unknown" def process_cve_data(cve: dict) -> dict | None: """ Normalizes any supported CVE format into a unified structure expected by the `generate_indicator()` function. Supported formats include: - CVE 5.1 - Legacy CVE format (older format used by CIRCL) Args: cve: The raw CVE data dictionary, from any supported source. Returns: A normalized dictionary with unified keys for downstream processing or None """ format_type = detect_format(cve) if format_type == "cve_5_1": return handle_cve_5_1(cve) elif format_type == "legacy": return cve else: demisto.debug(f"Unsupported CVE format type: {format_type}") return None def handle_cve_5_1(cve: dict) -> dict | None: """ Converts a CVE 5.1 formatted record into a normalized legacy-like dictionary format used throughout the integration. Args: cve: The CVE data in CVE 5.1 format as a dictionary. Returns: A normalized dictionary containing common CVE attributes including ID, CVSS data, CWE, vulnerable products, references, and other relevant fields. """ try: metadata = cve.get("cveMetadata", {}) cna = cve.get("containers", {}).get("cna", {}) cwe = next( ( d.get("cweId") for p in cna.get("problemTypes", []) for d in p.get("descriptions", []) if d.get("lang") == "en" and d.get("cweId") ), "NVD-CWE-noinfo", ) legacy = { "id": metadata.get("cveId", ""), "Published": metadata.get("datePublished", ""), "Modified": metadata.get("dateUpdated", ""), "summary": next((d.get("value") for d in cna.get("descriptions", []) if d.get("lang") in ("en", "en-US")), ""), "cvss": "N\\A", "cvss-vector": "", "cwe": cwe, "references": [r.get("url") for r in cna.get("references", []) if r.get("url")], "vulnerable_product": [], "vulnerable_configuration": [], "access": {}, "impact": {}, } vector_str = "" for m in cna.get("metrics", []): for key in ["cvssV3_1", "cvssV3_0", "cvssV2_0"]: if key in m: cvss = m[key] legacy["cvss"] = cvss.get("baseScore") vector_str = cvss.get("vectorString", "") legacy["cvss-vector"] = vector_str break if vector_str: parts = vector_str.split("/") vector_map = {p.split(":")[0]: p.split(":")[1] for p in parts if ":" in p} legacy["access"] = { "vector": vector_map.get("AV", ""), "complexity": vector_map.get("AC", ""), "authentication": vector_map.get("Au", "NONE"), } legacy["impact"] = { "confidentiality": vector_map.get("C", ""), "integrity": vector_map.get("I", ""), "availability": vector_map.get("A", ""), } for affected in cna.get("affected", []): vendor = affected.get("vendor", "").lower().replace(" ", "_") product = affected.get("product", "").lower().replace(" ", "_") versions = affected.get("versions", []) for version_entry in versions: version = version_entry.get("version") if vendor and product and version: cpe = f"cpe:2.3:a:{vendor}:{product}:{version}:*:*:*:*:*:*:*" legacy["vulnerable_product"].append(cpe) legacy["vulnerable_configuration"].append({"id": cpe, "title": cpe}) for cpe in affected.get("cpes", []): legacy["vulnerable_product"].append(cpe) legacy["vulnerable_configuration"].append({"id": cpe, "title": cpe}) return legacy except Exception as e: demisto.debug(f"Failed to parse CVE 5.1 data: {e}") return None def create_cve_summary(cve: dict) -> dict: """ Extracts and summarizes the key fields from the normalized CVE data for presentation or context. Args: cve: A normalized CVE dictionary. Returns: A dictionary with simplified fields: ID, CVSS score, publish and modification dates, and a short description. """ return { "ID": cve.get("id", ""), "CVSS": cve.get("cvss", "N/A"), "Published": cve.get("Published", "").rstrip("Z"), "Modified": cve.get("Modified", "").rstrip("Z"), "Description": cve.get("summary", ""), } def test_module(client: Client): """ Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. Returns: 'ok' if test passed, anything else will fail the test. """ cve_command(client, {"cve": "CVE-2023-3982"}) return "ok" def get_cvss_version(cvss_vector: str) -> float: """ Extracts the CVSS score version according to its vector. Args: cvss_vector: The CVSS of the CVE. Returns: The CVSS version as a float. """ if not cvss_vector: return 0 elif cvss_version_regex := re.match("CVSS:(?P<version>.+?)/", cvss_vector): return float(cvss_version_regex.group("version")) else: return 2.0 def cve_command(client: Client, args: dict) -> list[CommandResults] | CommandResults: """ Search for cve with the given ID and returns the cve data if found. Args: client: Integration client args :The demisto args containing the cve_id Returns: CVE details containing ID, CVSS, modified date, published date and description. """ cve_ids = argToList(args.get("cve", "")) command_results: list[CommandResults] = [] skipped_cve_ids = [] for _id in cve_ids: if not valid_cve_id_format(_id): raise DemistoException(f'"{_id}" is not a valid cve ID') if response := client.cve(_id): full_data = process_cve_data(response) if not full_data: skipped_cve_ids.append(_id) continue data = create_cve_summary(full_data) indicator = generate_indicator(full_data) relationships = indicator.relationships if indicator else [] cr = CommandResults( outputs_prefix="CVESearch.CVE", outputs_key_field="CVE", outputs=data, raw_response=response, indicator=indicator, relationships=relationships, ) else: cr = CommandResults(readable_output=f"### No results found for cve {_id}") command_results.append(cr) if skipped_cve_ids: skipped_msg = "The format of the following CVE IDs is not supported and they were skipped:\n" skipped_msg += "\n".join(f"- {cve_id}" for cve_id in skipped_cve_ids) command_results.append(CommandResults(readable_output=skipped_msg)) return command_results def cve_latest_command(client: Client, limit) -> list[CommandResults]: """ Returns the 30 latest updated CVEs. Args: limit int: The amount of CVEs to display Returns: Latest 30 CVE details containing ID, CVSS, modified date, published date and description. """ res = client.cve_latest(limit) command_results: list[CommandResults] = [] for cve_details in res: full_data = process_cve_data(cve_details) if not full_data: continue data = create_cve_summary(full_data) indicator = generate_indicator(full_data) readable_output = tableToMarkdown("Latest CVEs", data) command_results.append( CommandResults( outputs_prefix="CVE", outputs_key_field="ID", outputs=data, readable_output=readable_output, raw_response=res, indicator=indicator, ) ) if not res or not command_results: command_results.append(CommandResults(readable_output="No results found")) return command_results def parse_cpe(cpes: list[str], cve_id: str) -> tuple[list[str], list[EntityRelationship]]: """ Parses a CPE to return the correct tags and relationships needed for the CVE. Args: cpe: A list representing a single CPE, see "https://nvlpubs.nist.gov/nistpubs/legacy/ir/nistir7695.pdf" Returns: A tuple consisting of a list of tags and a list of EntityRelationships. """ cpe_parts = {"a": "Application", "o": "Operating-System", "h": "Hardware"} vendors = set() products = set() parts = set() for cpe in cpes: cpe_split = re.split(r"(?<!\\):", cpe) if cpe.startswith("cpe:2.3:"): with contextlib.suppress(IndexError): if vendor := cpe_split[3].capitalize().replace("\\", "").replace("_", " "): vendors.add(vendor) with contextlib.suppress(IndexError): if product := cpe_split[4].capitalize().replace("\\", "").replace("_", " "): products.add(product) with contextlib.suppress(IndexError): parts.add(cpe_parts[cpe_split[2]]) elif cpe.startswith("cpe:/"): with contextlib.suppress(IndexError): if vendor := cpe_split[2].capitalize().replace("\\", "").replace("_", " "): vendors.add(vendor) with contextlib.suppress(IndexError): if product := cpe_split[3].capitalize().replace("\\", "").replace("_", " "): products.add(product) with contextlib.suppress(IndexError): parts.add(cpe_parts[cpe_split[1].replace("/", "")]) relationships = [ EntityRelationship(name="targets", entity_a=cve_id, entity_a_type="cve", entity_b=vendor, entity_b_type="identity") for vendor in vendors ] relationships.extend( [ EntityRelationship(name="targets", entity_a=cve_id, entity_a_type="cve", entity_b=product, entity_b_type="software") for product in products ] ) return list(vendors | products | parts), relationships def generate_indicator(data: dict) -> Common.CVE: """ Generating a single cve indicator with dbot score from cve data. Args: data: The cve data Returns: A CVE indicator with dbotScore """ cve_id = data.get("id", "") if cpe := data.get("vulnerable_product", ""): tags, relationships = parse_cpe(cpe, cve_id) else: relationships = [] tags = [] cwe = data.get("cwe", "") if cwe and cwe != "NVD-CWE-noinfo": tags.append(cwe) cvss_table = [] for category in ("impact", "access"): for key, value in data.get(category, []).items(): cvss_table.append({"metrics": key, "value": value}) vulnerable_products = [Common.CPE(cpe) for cpe in data.get("vulnerable_product", [])] vulnerable_configurations = [ Common.CPE(cpe.get("id")) if isinstance(cpe, dict) else Common.CPE(cpe) for cpe in data.get("vulnerable_configuration", []) ] cpes = set(vulnerable_products) | set(vulnerable_configurations) cve_object = Common.CVE( id=cve_id, cvss=data.get("cvss"), cvss_vector=data.get("cvss-vector"), cvss_version=get_cvss_version(data.get("cvss-vector", "")), cvss_table=cvss_table, published=data.get("Published"), modified=data.get("Modified"), description=data.get("summary"), vulnerable_products=cpes, publications=[ Common.Publications(title=data.get("id"), link=reference, source="Circl.lu") for reference in data.get("references", []) ], tags=tags, ) if relationships: cve_object.relationships = relationships return cve_object def valid_cve_id_format(cve_id: str) -> bool: """ Validates that the given cve_id is a valid cve ID. For more details see: https://cve.mitre.org/cve/identifiers/syntaxchange.html Args: cve_id: ID to validate Returns: True if cve_id is a valid cve ID else False """ return bool(re.match(cveRegex, cve_id)) def main(): params = demisto.params() proxy = params.get("proxy", False) use_ssl = not params.get("insecure", False) base_url = params.get("url", "https://cve.circl.lu/api/") client = Client(base_url=base_url, verify=use_ssl, proxy=proxy) command = demisto.command() LOG(f"Command being called is {command}") try: if demisto.command() == "test-module": return_results(test_module(client)) elif demisto.command() == "cve": return_results(cve_command(client, demisto.args())) elif demisto.command() == "cve-latest": return_results(cve_latest_command(client, demisto.args().get("limit", 30))) else: raise NotImplementedError(f"{command} is not an existing CVE Search command") except DemistoException as err: if err.res.status_code == 404: return_error(f'Failed to execute {demisto.command()} command.\nError: {"Invalid server URL"}') else: return_error(f"Failed to execute {demisto.command()} command. Error: {str(err)}") if __name__ in ("__main__", "__builtin__", "builtins"): main()