{"vulnerability": "cve-2021-1234", "sightings": [{"uuid": "a4b895b9-4cfe-4789-86a6-da0aad7f2069", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-1234", "type": "seen", "source": "https://infosec.exchange/users/cve/statuses/113504757865674846", "content": "", "creation_timestamp": "2024-11-18T15:52:40.453652Z"}, {"uuid": "88300209-c350-4e4f-94e8-b4fe5f791d67", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-12345", "type": "seen", "source": "https://bsky.app/profile/cve.skyfleet.blue/post/3ltb3ul2nau2i", "content": "", "creation_timestamp": "2025-07-06T01:15:06.479688Z"}, {"uuid": "009d40f5-5e7e-4331-b9c8-487156442164", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-1234", "type": "seen", "source": "https://bsky.app/profile/cve.skyfleet.blue/post/3lmgkzglhv22h", "content": "", "creation_timestamp": "2025-04-10T03:33:13.173848Z"}, {"uuid": "97df6b0a-5acc-43af-82e9-c16d62a25500", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-12341", "type": "seen", "source": "https://gist.github.com/chrispatrick/ef9adba1d09172543266af24c83130a8", "content": "", "creation_timestamp": "2025-05-06T14:49:58.000000Z"}, {"uuid": "22ee4268-0eda-4ac3-baee-afef8d0e9e44", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-1234", "type": "seen", "source": "https://gist.github.com/Darkcrai86/7dfb0d3968baac2618392890b93e72e6", "content": "", "creation_timestamp": "2025-09-01T17:58:46.000000Z"}, {"uuid": "a7e2c2cd-3966-4038-ba68-096eeef8728a", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-1234", "type": "seen", "source": "MISP/1c5c38d6-3401-41ac-be0e-4cf361fa6f51", "content": "", "creation_timestamp": "2025-09-25T00:36:28.000000Z"}, {"uuid": "5fd1d761-b3cc-4d6e-9280-c7f550959d7d", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-1234", "type": "seen", "source": "https://gist.github.com/m-mizutani/16e2fb7a562ed95e8ba42a191ba1c38a", "content": "", "creation_timestamp": "2026-01-04T00:18:20.000000Z"}, {"uuid": "13e5d294-7268-495c-b136-9c44473daec4", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-1234", "type": "seen", "source": "https://bsky.app/profile/cve.skyfleet.blue/post/3mclhbf3snk2v", "content": "", "creation_timestamp": "2026-01-17T01:23:39.165335Z"}, {"uuid": "19da8865-022c-4efc-9359-43a3527fed2b", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-1234", "type": "seen", "source": "https://bsky.app/profile/cve.skyfleet.blue/post/3mjswnnbdig2k", "content": "", "creation_timestamp": "2026-04-19T02:43:55.962649Z"}, {"uuid": "29cd4ad6-adb9-4e3d-8bcb-4d654f1cba7f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-12345", "type": "seen", "source": "https://gist.github.com/mkrause/9ab4178c905b7e3ab46c5a3e3598adfc", "content": "import json\nimport logging\nimport re\n\nfrom django.conf import settings\n\nfrom dojo.models import Finding\nfrom dojo.tools.locations import LocationData\nfrom dojo.tools.utils import get_npm_cwe\n\nlogger = logging.getLogger(__name__)\n\n# pnpm audit --json output has changed significantly across versions:\n#\n# pnpm \u226410 (legacy endpoint /-/npm/v1/security/audits):\n#   Root keys: advisories, actions, metadata, muted\n#   Each advisory has: id, title, module_name, url, overview, recommendation,\n#     vulnerable_versions, patched_versions, severity, cwe, cves, access,\n#     findings[{version, paths[]}]\n#\n# pnpm \u226511 (new endpoint /-/npm/v1/security/advisories/bulk):\n#   Root keys: advisories, metadata\n#   Each advisory has: id, title, module_name, url, vulnerable_versions,\n#     patched_versions (optional, inferred), severity, cwe, github_advisory_id,\n#     findings[{version, paths[]}]\n#   Dropped fields: cves, overview, recommendation, access, references, etc.\n#   Use github_advisory_id (GHSA-xxxx-xxxx-xxxx) instead of cves for vuln IDs.\n\n\nclass PnpmAuditParser:\n\n    def get_scan_types(self):\n        return [\"PNPM Audit Scan\"]\n\n    def get_label_for_scan_types(self, scan_type):\n        return scan_type\n\n    def get_description_for_scan_types(self, scan_type):\n        return (\n            \"PNPM Audit Scan JSON output (pnpm audit --json). \"\n            \"Supports pnpm \u226410 (legacy audit endpoint) and pnpm \u226511 \"\n            \"(advisories/bulk endpoint).\"\n        )\n\n    def get_findings(self, json_output, test):\n        tree = self.parse_json(json_output)\n        if not tree:\n            return []\n        return self.get_items(tree, test)\n\n    def parse_json(self, json_output):\n        if json_output is None:\n            return None\n\n        try:\n            data = json_output.read()\n            try:\n                tree = json.loads(str(data, \"utf-8\"))\n            except Exception:\n                tree = json.loads(data)\n        except Exception:\n            msg = \"Invalid format, unable to parse json.\"\n            raise ValueError(msg)\n\n        if tree.get(\"error\"):\n            error = tree.get(\"error\")\n            code = error.get(\"code\", \"unknown\")\n            summary = error.get(\"summary\", \"unknown\")\n            msg = \"pnpm audit report contains errors: %s, %s\"\n            raise ValueError(msg, code, summary)\n\n        advisories = tree.get(\"advisories\")\n        if advisories is None:\n            msg = (\n                \"Unexpected format: 'advisories' key not found. \"\n                \"Ensure the report was produced with 'pnpm audit --json'.\"\n            )\n            raise ValueError(msg)\n\n        return advisories\n\n    def get_items(self, tree, test):\n        items = {}\n        for node in tree.values():\n            item = get_item(node, test)\n            unique_key = str(node[\"id\"]) + str(node[\"module_name\"])\n            items[unique_key] = item\n        return list(items.values())\n\n\ndef censor_path_hashes(path):\n    \"\"\"Replace git-installed dependency hashes with a stable placeholder.\n\n    pnpm (like npm) replaces the name of git-sourced packages with a\n    random-ish hash that can change between runs, which would cause\n    DefectDojo to treat the same vulnerability as a new finding every time.\n    We replace any 64-char hex string with 'censored_by_pnpm_audit'.\n    \"\"\"\n    if not path:\n        return None\n    return re.sub(r\"[a-f0-9]{64}\", \"censored_by_pnpm_audit\", path)\n\n\ndef _map_severity(raw_severity):\n    \"\"\"Map pnpm severity strings to DefectDojo severity levels.\n\n    pnpm uses: info, low, moderate, high, critical.\n    \"\"\"\n    return {\n        \"critical\": \"Critical\",\n        \"high\": \"High\",\n        \"moderate\": \"Medium\",\n        \"low\": \"Low\",\n        \"info\": \"Info\",\n    }.get(raw_severity, \"Info\")\n\n\ndef _get_vulnerability_ids(item_node):\n    \"\"\"Extract vulnerability IDs, handling both pnpm \u226410 and \u226511.\n\n    pnpm \u226410: 'cves' list (e.g. [\"CVE-2021-12345\"])\n    pnpm \u226511: 'github_advisory_id' string (e.g. \"GHSA-xxxx-xxxx-xxxx\");\n              'cves' field was dropped from the bulk endpoint response.\n    \"\"\"\n    ids = []\n\n    # pnpm \u226410: explicit CVE list\n    cves = item_node.get(\"cves\") or []\n    ids.extend(cves)\n\n    # pnpm \u226511: GHSA identifier (always present when using bulk endpoint)\n    ghsa = item_node.get(\"github_advisory_id\")\n    if ghsa and ghsa not in ids:\n        ids.append(ghsa)\n\n    # Fallback: parse GHSA from the advisory URL if neither field is present\n    # e.g. https://github.com/advisories/GHSA-xxxx-xxxx-xxxx\n    if not ids:\n        url = item_node.get(\"url\", \"\")\n        match = re.search(r\"(GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4})\", url, re.IGNORECASE)\n        if match:\n            ids.append(match.group(1).upper())\n\n    return ids\n\n\ndef get_item(item_node, test):\n    severity = _map_severity(item_node.get(\"severity\", \"\"))\n\n    # --- Build the vulnerable paths / versions summary ---\n    paths = \"\"\n    component_version = None\n    for pnpm_finding in item_node.get(\"findings\", []):\n        component_version = component_version or pnpm_finding.get(\"version\")\n        version = pnpm_finding.get(\"version\", \"unknown\")\n        finding_paths = pnpm_finding.get(\"paths\", [])\n        paths += (\n            \"\\n - \"\n            + str(version)\n            + \":\"\n            + str(\",\".join(finding_paths[:25]))\n        )\n        if len(finding_paths) &gt; 25:\n            paths += \"\\n - ..... (list of paths truncated after 25 paths)\"\n\n    # --- CWE ---\n    cwe = get_npm_cwe(item_node)\n\n    # --- File path (first finding's first path, hash-censored) ---\n    try:\n        filepath = censor_path_hashes(\n            item_node[\"findings\"][0][\"paths\"][0]\n        )\n    except (IndexError, KeyError):\n        filepath = \"\"\n\n    # --- Description: gracefully degrade for pnpm \u226511 missing fields ---\n    module_name = item_node.get(\"module_name\", \"unknown\")\n    vulnerable_versions = item_node.get(\"vulnerable_versions\", \"unknown\")\n    patched_versions = item_node.get(\"patched_versions\", \"unknown\")\n    url = item_node.get(\"url\", \"\")\n\n    # overview and recommendation were dropped in pnpm \u226511\n    overview = item_node.get(\"overview\", \"No overview available (pnpm \u226511 audit format).\")\n    recommendation = item_node.get(\"recommendation\", \"\")\n    access = item_node.get(\"access\", \"\")\n\n    description_parts = [url, overview]\n    description_parts.append(f\" Vulnerable Module: {module_name}\")\n    description_parts.append(f\" Vulnerable Versions: {vulnerable_versions}\")\n    description_parts.append(f\" Patched Version: {patched_versions}\")\n    description_parts.append(f\" Vulnerable Paths: {paths}\")\n    description_parts.append(f\" CWE: {item_node.get('cwe', 'unknown')}\")\n    if access:\n        description_parts.append(f\" Access: {access}\")\n\n    description = \"\\n\".join(description_parts)\n\n    dojo_finding = Finding(\n        title=(\n            item_node.get(\"title\", \"Unknown vulnerability\")\n            + \" - (\"\n            + module_name\n            + \", \"\n            + vulnerable_versions\n            + \")\"\n        ),\n        test=test,\n        severity=severity,\n        file_path=filepath,\n        description=description,\n        cwe=cwe,\n        mitigation=recommendation,\n        references=url,\n        component_name=module_name,\n        component_version=component_version,\n        false_p=False,\n        duplicate=False,\n        out_of_scope=False,\n        mitigated=None,\n        impact=\"No impact provided\",\n        static_finding=True,\n        dynamic_finding=False,\n    )\n\n    # Attach CVE / GHSA IDs as vulnerability identifiers\n    vulnerability_ids = _get_vulnerability_ids(item_node)\n    if vulnerability_ids:\n        dojo_finding.unsaved_vulnerability_ids = vulnerability_ids\n\n    # Optional: record PURL-based location data (requires V3_FEATURE_LOCATIONS)\n    if settings.V3_FEATURE_LOCATIONS and module_name and component_version:\n        dojo_finding.unsaved_locations.append(\n            LocationData.dependency(\n                purl_type=\"npm\",\n                name=module_name,\n                version=component_version,\n                file_path=filepath,\n            ),\n        )\n\n    return dojo_finding", "creation_timestamp": "2026-05-26T15:21:17.000000Z"}]}