Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 50 additions & 35 deletions application/clicommands/CheckCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use Icinga\Application\Logger;
use Icinga\Module\X509\Command;
use Icinga\Module\X509\DbTool;
use Icinga\Module\X509\Job;
use ipl\Sql\Select;
use Icinga\Module\X509\Model\X509Certificate;
use Icinga\Module\X509\Model\X509CertificateChain;
use Icinga\Module\X509\Model\X509Target;
use ipl\Sql\Expression;
use ipl\Stdlib\Filter;

class CheckCommand extends Command
{
Expand Down Expand Up @@ -66,42 +68,55 @@ public function hostAction()
exit(3);
}

$dbTool = new DbTool($this->getDb());
$targets = (new Select())
->from('x509_target t')
->columns([
't.port',
'cc.valid',
'cc.invalid_reason',
'c.subject',
'self_signed' => 'COALESCE(ci.self_signed, c.self_signed)',
'valid_from' => (new Select())
->from('x509_certificate_chain_link xccl')
->columns('MAX(GREATEST(xc.valid_from, xci.valid_from))')
->join('x509_certificate xc', 'xc.id = xccl.certificate_id')
->join('x509_certificate xci', 'xci.subject_hash = xc.issuer_hash')
->where('xccl.certificate_chain_id = cc.id'),
'valid_to' => (new Select())
->from('x509_certificate_chain_link xccl')
->columns('MIN(LEAST(xc.valid_to, xci.valid_to))')
->join('x509_certificate xc', 'xc.id = xccl.certificate_id')
->join('x509_certificate xci', 'xci.subject_hash = xc.issuer_hash')
->where('xccl.certificate_chain_id = cc.id')
$conn = $this->getDb();
$targets = X509Target::on($conn)->with([
'chain',
'chain.certificate',
'chain.certificate.issuer_certificate'
]);

$targets->getWith()['target.chain.certificate.issuer_certificate']->setJoinType('LEFT');

$targets->columns([
'port',
'chain.valid',
'chain.invalid_reason',
'subject' => 'chain.certificate.subject',
'self_signed' => new Expression('COALESCE(%s, %s)', [
'chain.certificate.issuer_certificate.self_signed',
'chain.certificate.self_signed'
])
]);

// Sub queries for (valid_from, valid_to) columns
$validFrom = $targets->createSubQuery(new X509Certificate(), 'chain.certificate');
$validFrom
->columns([new Expression('MAX(GREATEST(%s, %s))', ['valid_from', 'issuer_certificate.valid_from'])])
->getSelectBase()
->resetWhere()
->where(new Expression('sub_certificate_link.certificate_chain_id = target_chain.id'));

$validTo = clone $validFrom;
$validTo->columns([new Expression('MIN(LEAST(%s, %s))', ['valid_to', 'issuer_certificate.valid_to'])]);

list($validFromSelect, $_) = $validFrom->dump();
list($validToSelect, $_) = $validTo->dump();
$targets
->withColumns([
'valid_from' => new Expression($validFromSelect),
'valid_to' => new Expression($validToSelect)
])
->join('x509_certificate_chain cc', 'cc.id = t.latest_certificate_chain_id')
->join('x509_certificate_chain_link ccl', 'ccl.certificate_chain_id = cc.id')
->join('x509_certificate c', 'c.id = ccl.certificate_id')
->joinLeft('x509_certificate ci', 'ci.subject_hash = c.issuer_hash')
->where(['ccl.order = ?' => 0]);
->getSelectBase()
->where(new Expression('target_chain_link.order = 0'));

if ($ip !== null) {
$targets->where(['t.ip = ?' => $dbTool->marshalBinary(Job::binary($ip))]);
$targets->filter(Filter::equal('ip', $ip));
}
if ($hostname !== null) {
$targets->where(['t.hostname = ?' => $hostname]);
$targets->filter(Filter::equal('hostname', $hostname));
}
if ($this->params->has('port')) {
$targets->where(['t.port = ?' => $this->params->get('port')]);
$targets->filter(Filter::equal('port', $this->params->get('port')));
}

$allowSelfSigned = (bool) $this->params->get('allow-self-signed', false);
Expand All @@ -112,9 +127,9 @@ public function hostAction()
$perfData = [];

$state = 3;
foreach ($this->getDb()->select($targets) as $target) {
if ($target['valid'] === 'n' && ($target['self_signed'] === 'n' || ! $allowSelfSigned)) {
$invalidMessage = $target['subject'] . ': ' . $target['invalid_reason'];
foreach ($targets as $target) {
if (! $target->chain->valid && (! $target['self_signed'] || ! $allowSelfSigned)) {
$invalidMessage = $target['subject'] . ': ' . $target->chain->invalid_reason;
$output[$invalidMessage] = $invalidMessage;
$state = 2;
}
Expand Down
2 changes: 1 addition & 1 deletion application/clicommands/ImportCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function indexAction()
foreach ($bundle as $data) {
$cert = openssl_x509_read($data);

$id = CertificateUtils::findOrInsertCert($db, $cert);
list($id, $_) = CertificateUtils::findOrInsertCert($db, $cert);

$db->update(
'x509_certificate',
Expand Down
20 changes: 10 additions & 10 deletions application/controllers/CertificateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,34 @@
use Icinga\Exception\ConfigurationError;
use Icinga\Module\X509\CertificateDetails;
use Icinga\Module\X509\Controller;
use ipl\Sql;
use Icinga\Module\X509\Model\X509Certificate;
use ipl\Stdlib\Filter;

class CertificateController extends Controller
{
public function indexAction()
{
$this->addTitleTab($this->translate('X.509 Certificate'));
$this->getTabs()->disableLegacyExtensions();

$certId = $this->params->getRequired('cert');

try {
$conn = $this->getDb();
} catch (ConfigurationError $_) {
$this->render('missing-resource', null, true);

return;
}

$cert = $conn->select(
(new Sql\Select())
->from('x509_certificate')
->columns('*')
->where(['id = ?' => $certId])
)->fetch();
$cert = X509Certificate::on($conn)
->filter(Filter::equal('id', $certId))
->first();

if ($cert === false) {
if (! $cert) {
$this->httpNotFound($this->translate('Certificate not found.'));
}

$this->setTitle($this->translate('X.509 Certificate'));

$this->view->certificateDetails = (new CertificateDetails())
->setCert($cert);
}
Expand Down
166 changes: 79 additions & 87 deletions application/controllers/CertificatesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,126 +4,118 @@

namespace Icinga\Module\X509\Controllers;

use Icinga\Data\Filter\FilterExpression;
use Icinga\Exception\ConfigurationError;
use Icinga\Module\X509\CertificatesTable;
use Icinga\Module\X509\Controller;
use Icinga\Module\X509\FilterAdapter;
use Icinga\Module\X509\SortAdapter;
use Icinga\Module\X509\SqlFilter;
use ipl\Web\Control\PaginationControl;
use ipl\Sql;
use ipl\Web\Url;
use Icinga\Module\X509\Model\X509Certificate;
use Icinga\Module\X509\Web\Control\SearchBar\ObjectSuggestions;
use ipl\Orm\Query;
use ipl\Web\Control\LimitControl;
use ipl\Web\Control\SortControl;

class CertificatesController extends Controller
{
public function indexAction()
{
$this
->initTabs()
->setTitle($this->translate('Certificates'));
$this->addTitleTab($this->translate('Certificates'));
$this->getTabs()->enableDataExports();

try {
$conn = $this->getDb();
} catch (ConfigurationError $_) {
$this->render('missing-resource', null, true);

return;
}

$select = (new Sql\Select())
->from('x509_certificate c')
->columns([
'c.id', 'c.subject', 'c.issuer', 'c.version', 'c.self_signed', 'c.ca', 'c.trusted',
'c.pubkey_algo', 'c.pubkey_bits', 'c.signature_algo', 'c.signature_hash_algo',
'c.valid_from', 'c.valid_to',
]);

$this->view->paginator = new PaginationControl(new Sql\Cursor($conn, $select), Url::fromRequest());
$this->view->paginator->apply();

$sortAndFilterColumns = [
'subject' => $this->translate('Certificate'),
'issuer' => $this->translate('Issuer'),
'version' => $this->translate('Version'),
'self_signed' => $this->translate('Is Self-Signed'),
'ca' => $this->translate('Is Certificate Authority'),
'trusted' => $this->translate('Is Trusted'),
'pubkey_algo' => $this->translate('Public Key Algorithm'),
'pubkey_bits' => $this->translate('Public Key Strength'),
'signature_algo' => $this->translate('Signature Algorithm'),
$certificates = X509Certificate::on($conn);

$sortColumns = [
'subject' => $this->translate('Certificate'),
'issuer' => $this->translate('Issuer'),
'version' => $this->translate('Version'),
'self_signed' => $this->translate('Is Self-Signed'),
'ca' => $this->translate('Is Certificate Authority'),
'trusted' => $this->translate('Is Trusted'),
'pubkey_algo' => $this->translate('Public Key Algorithm'),
'pubkey_bits' => $this->translate('Public Key Strength'),
'signature_algo' => $this->translate('Signature Algorithm'),
'signature_hash_algo' => $this->translate('Signature Hash Algorithm'),
'valid_from' => $this->translate('Valid From'),
'valid_to' => $this->translate('Valid To'),
'duration' => $this->translate('Duration'),
'expires' => $this->translate('Expiration')
'valid_from' => $this->translate('Valid From'),
'valid_to' => $this->translate('Valid To'),
'duration' => $this->translate('Duration'),
'expires' => $this->translate('Expiration')
];

$this->setupSortControl(
$sortAndFilterColumns,
new SortAdapter($select, function ($field) {
if ($field === 'duration') {
return '(valid_to - valid_from)';
} elseif ($field === 'expires') {
return 'CASE WHEN UNIX_TIMESTAMP() > valid_to'
. ' THEN 0 ELSE (valid_to - UNIX_TIMESTAMP()) / 86400 END';
}
})
);
$limitControl = $this->createLimitControl();
$paginator = $this->createPaginationControl($certificates);
$sortControl = $this->createSortControl($certificates, $sortColumns);

$this->setupLimitControl();
$searchBar = $this->createSearchBar($certificates, [
$limitControl->getLimitParam(),
$sortControl->getSortParam()
]);

$filterAdapter = new FilterAdapter();
$this->setupFilterControl(
$filterAdapter,
$sortAndFilterColumns,
['subject', 'issuer'],
['format']
);
if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
if ($searchBar->hasBeenSubmitted()) {
$filter = $this->getFilter();
} else {
$this->addControl($searchBar);
$this->sendMultipartUpdate();

(new SqlFilter($conn))->apply($select, $filterAdapter->getFilter(), function (FilterExpression $filter) {
switch ($filter->getColumn()) {
case 'issuer_hash':
$value = $filter->getExpression();

if (is_array($value)) {
$value = array_map('hex2bin', $value);
} else {
$value = hex2bin($value);
}

return $filter->setExpression($value);
case 'duration':
return $filter->setColumn('(valid_to - valid_from)');
case 'expires':
return $filter->setColumn(
'CASE WHEN UNIX_TIMESTAMP() > valid_to THEN 0 ELSE (valid_to - UNIX_TIMESTAMP()) / 86400 END'
);
case 'valid_from':
case 'valid_to':
$expr = $filter->getExpression();
if (! is_numeric($expr)) {
return $filter->setExpression(strtotime($expr));
}

// expression doesn't need changing
default:
return false;
return;
}
});
} else {
$filter = $searchBar->getFilter();
}

$this->handleFormatRequest($conn, $select, function (\PDOStatement $stmt) {
foreach ($stmt as $cert) {
$certificates->peekAhead($this->view->compact);

$certificates->filter($filter);

$this->addControl($paginator);
$this->addControl($sortControl);
$this->addControl($limitControl);
$this->addControl($searchBar);

$this->handleFormatRequest($certificates, function (Query $certificates) {
/** @var X509Certificate $cert */
foreach ($certificates as $cert) {
$cert['valid_from'] = (new \DateTime())
->setTimestamp($cert['valid_from'])
->format('l F jS, Y H:i:s e');
$cert['valid_to'] = (new \DateTime())
->setTimestamp($cert['valid_to'])
->format('l F jS, Y H:i:s e');

yield $cert;
yield array_intersect_key(iterator_to_array($cert), array_flip($cert->getExportableColumns()));
}
});

$this->view->certificatesTable = (new CertificatesTable())->setData($conn->select($select));
$this->addContent((new CertificatesTable())->setData($certificates));

if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
$this->sendMultipartUpdate(); // Updates the browser search bar
}
}

public function completeAction()
{
$this->getDocument()->add(
(new ObjectSuggestions())
->setModel(X509Certificate::class)
->forRequest($this->getServerRequest())
);
}

public function searchEditorAction()
{
$editor = $this->createSearchEditor(X509Certificate::on($this->getDb()), [
LimitControl::DEFAULT_LIMIT_PARAM,
SortControl::DEFAULT_SORT_PARAM
]);

$this->getDocument()->add($editor);
$this->setTitle(t('Adjust Filter'));
}
}
Loading