mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2024-11-27 03:50:42 +03:00
428 lines
14 KiB
PHP
428 lines
14 KiB
PHP
|
<?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>';
|
||
|
}
|
||
|
}
|