<?php class OpenCVEBridge extends BridgeAbstract { const MAINTAINER = 'sqrtminusone'; const NAME = 'OpenCVE Bridge'; const URI = 'https://opencve.io'; const CACHE_TIMEOUT = 3600; // 1 hour const DESCRIPTION = 'A feed for search results from OpenCVE'; const PARAMETERS = [ '' => [ 'instance' => [ 'name' => 'OpenCVE Instance', 'required' => true, 'defaultValue' => 'https://www.opencve.io', 'exampleValue' => 'https://www.opencve.io' ], 'login' => [ 'name' => 'Login', 'type' => 'text', 'required' => true ], 'password' => [ 'name' => 'Password', 'type' => 'text', 'required' => true ], 'pages' => [ 'name' => 'Number of pages', 'type' => 'number', 'required' => false, 'exampleValue' => 1, 'defaultValue' => 1 ], 'filter' => [ 'name' => 'Filter', 'type' => 'text', 'required' => false, 'exampleValue' => 'search:jenkins;product:gitlab,cvss:critical', 'title' => 'Syntax: param1:value1,param2:value2;param1query2:param2query2. See https://docs.opencve.io/api/cve/ for parameters' ], 'upd_timestamp' => [ 'name' => 'Use updated_at instead of created_at as timestamp', 'type' => 'checkbox' ], 'trunc_summary' => [ 'name' => 'Truncate summary for header', 'type' => 'number', 'defaultValue' => 100 ], 'fetch_contents' => [ 'name' => 'Fetch detailed contents for CVEs', 'defaultValue' => 'checked', 'type' => 'checkbox' ] ] ]; const CSS = ' <style> .cvss-na-color { background-color: #d2d6de; color: #000; } .cvss-low-color { background-color: #0073b7; color: #fff; } .cvss-medium-color { background-color: #f39c12; color: #fff; } .cvss-high-color { background-color: #dd4b39; color: #fff; } .cvss-crit-color { background-color: #972b1e; color: #fff; } .label { padding: .2em .6em .3em; font-size: 75%; font-weight: 700; line-height: 1; text-align: center; white-space: nowrap; border-radius: .25em; } .labels-row { display: flex; flex-direction: row; align-items: center; white-space: nowrap; overflow: hidden; margin-bottom: 6px; } .labels-row div { margin-right: 6px; } .cvss-table table { border-collapse: collapse; width: 100%; margin-bottom: 12px; } .cvss-table td, th { border: 1px solid #dddddd; text-align: left; padding: 8px; } </style>'; public function collectData() { $creds = $this->getInput('login') . ':' . $this->getInput('password'); $authHeader = 'Authorization: Basic ' . base64_encode($creds); $instance = $this->getInput('instance'); $queries = []; $filter = $this->getInput('filter'); $filterValues = []; if ($filter && mb_strlen($filter) > 0) { $filterValues = explode(';', $filter); } else { $queries[''] = []; } foreach ($filterValues as $filterValue) { $params = explode(',', $filterValue); $queryName = $filterValue; $query = []; foreach ($params as $param) { [$key, $value] = explode(':', $param); if ($key == 'title') { $queryName = $value; } else { $query[$key] = $value; } } $queries[$queryName] = $query; } $fetchedIds = []; foreach ($queries as $queryName => $query) { for ($i = 1; $i <= $this->getInput('pages'); $i++) { $queryPaginated = array_merge($query, ['page' => $i]); $url = $instance . '/api/cve?' . http_build_query($queryPaginated); $response = getContents($url, [$authHeader]); $titlePrefix = ''; if (count($queries) > 1) { $titlePrefix = '[' . $queryName . '] '; } foreach (json_decode($response) as $cveItem) { if (array_key_exists($cveItem->id, $fetchedIds)) { continue; } $fetchedIds[$cveItem->id] = true; $item = [ 'uri' => $instance . '/cve/' . $cveItem->id, 'uid' => $cveItem->id, ]; if ($this->getInput('upd_timestamp') == 1) { $item['timestamp'] = strtotime($cveItem->updated_at); } else { $item['timestamp'] = strtotime($cveItem->created_at); } if ($this->getInput('fetch_contents')) { [$content, $title] = $this->fetchContents( $cveItem, $titlePrefix, $instance, $authHeader ); $item['content'] = $content; $item['title'] = $title; } else { $item['content'] = $cveItem->summary . $this->getLinks($cveItem->id); $item['title'] = $this->getTitle($titlePrefix, $cveItem); } $this->items[] = $item; } } } usort($this->items, function ($a, $b) { return $b['timestamp'] - $a['timestamp']; }); } private function getTitle($titlePrefix, $cveItem) { $summary = $cveItem->summary; $limit = $this->getInput('limit'); if ($limit && mb_strlen($summary) > 100) { $summary = mb_substr($summary, 0, $limit) + '...'; } return $titlePrefix . $cveItem->id . '. ' . $summary; } private function fetchContents($cveItem, $titlePrefix, $instance, $authHeader) { $url = $instance . '/api/cve/' . $cveItem->id; $response = getContents($url, [$authHeader]); $datum = json_decode($response); $title = $this->getTitleFromDatum($datum, $titlePrefix); $result = self::CSS; $result .= '<h1>' . $cveItem->id . '</h1>'; $result .= $this->getCVSSLabels($datum); $result .= '<p>' . $datum->summary . '</p>'; $result .= <<<EOD <h3>Information:</h3> <p> <ul> <li><b>Publication date</b>: {$datum->raw_nvd_data->published} <li><b>Last modified</b>: {$datum->raw_nvd_data->lastModified} <li><b>Last modified</b>: {$datum->raw_nvd_data->lastModified} </ul> </p> EOD; $result .= $this->getV3Table($datum); $result .= $this->getV2Table($datum); $result .= $this->getLinks($datum->id); $result .= $this->getReferences($datum); $result .= $this->getVendors($datum); return [$result, $title]; } private function getTitleFromDatum($datum, $titlePrefix) { $title = $titlePrefix; if ($datum->cvss->v3) { $title .= "[v3: {$datum->cvss->v3}] "; } if ($datum->cvss->v2) { $title .= "[v2: {$datum->cvss->v2}] "; } $title .= $datum->id . '. '; $titlePostfix = $datum->summary; $limit = $this->getInput('limit'); if ($limit && mb_strlen($titlePostfix) > 100) { $titlePostfix = mb_substr($titlePostfix, 0, $limit) + '...'; } $title .= $titlePostfix; return $title; } private function getCVSSLabels($datum) { $CVSSv2Text = 'n/a'; $CVSSv2Class = 'cvss-na-color'; if ($datum->cvss->v2) { $importance = ''; if ($datum->cvss->v2 >= 7) { $importance = 'HIGH'; $CVSSv2Class = 'cvss-high-color'; } else if ($datum->cvss->v2 >= 4) { $importance = 'MEDIUM'; $CVSSv2Class = 'cvss-medium-color'; } else { $importance = 'LOW'; $CVSSv2Class = 'cvss-low-color'; } $CVSSv2Text = sprintf('[%s] %.1f', $importance, $datum->cvss->v2); } $CVSSv2Item = "<div>CVSS v2: </div><div class=\"label {$CVSSv2Class}\">{$CVSSv2Text}</div>"; $CVSSv3Text = 'n/a'; $CVSSv3Class = 'cvss-na-color'; if ($datum->cvss->v3) { $importance = ''; if ($datum->cvss->v3 >= 9) { $importance = 'CRITICAL'; $CVSSv3Class = 'cvss-crit-color'; } else if ($datum->cvss->v3 >= 7) { $importance = 'HIGH'; $CVSSv3Class = 'cvss-high-color'; } else if ($datum->cvss->v3 >= 4) { $importance = 'MEDIUM'; $CVSSv3Class = 'cvss-medium-color'; } else { $importance = 'LOW'; $CVSSv3Class = 'cvss-low-color'; } $CVSSv3Text = sprintf('[%s] %.1f', $importance, $datum->cvss->v3); } $CVSSv3Item = "<div>CVSS v3: </div><div class=\"label {$CVSSv3Class}\">{$CVSSv3Text}</div>"; return '<div class="labels-row">' . $CVSSv3Item . $CVSSv2Item . '</div>'; } private function getReferences($datum) { if (count($datum->raw_nvd_data->references) == 0) { return ''; } $res = '<h3>References:</h3> <p><ul>'; foreach ($datum->raw_nvd_data->references as $ref) { $item = '<li>'; if (isset($ref->tags) && count($ref->tags) > 0) { $item .= '[' . implode(', ', $ref->tags) . '] '; } $item .= "<a href=\"{$ref->url}\">{$ref->url}</a>"; $item .= '<li>'; $res .= $item; } $res .= '</p></ul>'; return $res; } private function getLinks($id) { return <<<EOD <h3>Links</h3> <p> <ul> <li>NVD Link: <a href="https://nvd.nist.gov/vuln/detail/{$id}">{$id}</a> <li>MITRE Link: <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name={$id}">{$id}</a> <li>CVE.ORG Link: <a href="https://www.cve.org/CVERecord?id={$id}">{$id}</a> </ul> </p> EOD; } private function getV3Table($datum) { $metrics = $datum->raw_nvd_data->metrics; if (!isset($metrics->cvssMetricV31) || count($metrics->cvssMetricV31) == 0) { return ''; } $v3 = $metrics->cvssMetricV31[0]; $data = $v3->cvssData; return <<<EOD <div class="cvss-table"> <h3>CVSS v3 details</h3> <table> <tr> <td>Impact score</td><td>{$v3->impactScore}</td> <td>Exploitability score</td><td>{$v3->exploitabilityScore}</td> </tr> <tr> <td>Attack vector</td><td>{$data->attackVector}</td> <td>Confidentiality Impact</td><td>{$data->confidentialityImpact}</td> </tr> <tr> <td>Attack complexity</td><td>{$data->attackComplexity}</td> <td>Integrity Impact</td><td>{$data->integrityImpact}</td> </tr> <tr> <td>Privileges Required</td><td>{$data->privilegesRequired}</td> <td>Availability Impact</td><td>{$data->availabilityImpact}</td> </tr> <tr> <td>User Interaction</td><td>{$data->userInteraction}</td> <td>Scope</td><td>{$data->scope}</td> </tr> </table> </div> EOD; } private function getV2Table($datum) { $metrics = $datum->raw_nvd_data->metrics; if (!isset($metrics->cvssMetricV2) || count($metrics->cvssMetricV2) == 0) { return ''; } $v2 = $metrics->cvssMetricV2[0]; $data = $v2->cvssData; return <<<EOD <div class="cvss-table"> <h3>CVSS v2 details</h3> <table> <tr> <td>Impact score</td><td>{$v2->impactScore}</td> <td>Exploitability score</td><td>{$v2->exploitabilityScore}</td> </tr> <tr> <td>Access Vector</td><td>{$data->accessVector}</td> <td>Confidentiality Impact</td><td>{$data->confidentialityImpact}</td> </tr> <tr> <td>Access Complexity</td><td>{$data->accessComplexity}</td> <td>Integrity Impact</td><td>{$data->integrityImpact}</td> </tr> <tr> <td>Authentication</td><td>{$data->authentication}</td> <td>Availability Impact</td><td>{$data->availabilityImpact}</td> </tr> <tr> </table> </div> EOD; } private function getVendors($datum) { if (count((array)$datum->vendors) == 0) { return ''; } $res = '<h3>Affected products</h3><p><ul>'; foreach ($datum->vendors as $vendor => $products) { $res .= "<li>{$vendor}"; if (count($products) > 0) { $res .= '<ul>'; foreach ($products as $product) { $res .= '<li>' . $product . '</li>'; } $res .= '</ul>'; } $res .= '</li>'; } $res .= '</ul></p>'; } }