Skip to content

Commit 0b9a937

Browse files
committed
WIP
1 parent 9c9d26a commit 0b9a937

File tree

6 files changed

+114
-57
lines changed

6 files changed

+114
-57
lines changed

application/clicommands/ImportCommand.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
namespace Icinga\Module\X509\Clicommands;
66

7+
use DateTime;
78
use Icinga\Application\Logger;
89
use Icinga\Module\X509\CertificateUtils;
910
use Icinga\Module\X509\Command;
1011
use ipl\Sql\Connection;
12+
use ipl\Sql\Expression;
1113

1214
class ImportCommand extends Command
1315
{
@@ -45,7 +47,7 @@ public function indexAction()
4547

4648
$db->update(
4749
'x509_certificate',
48-
['trusted' => 'yes'],
50+
['trusted' => 'yes', 'last_seen' => (new DateTime())->getTimestamp()],
4951
['id = ?' => $id]
5052
);
5153

application/clicommands/ScanCommand.php

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
namespace Icinga\Module\X509\Clicommands;
66

7+
use DateTime;
78
use Icinga\Application\Logger;
89
use Icinga\Module\X509\CertificateUtils;
910
use Icinga\Module\X509\Command;
1011
use Icinga\Module\X509\Hook\SniHook;
1112
use Icinga\Module\X509\Job;
13+
use InvalidArgumentException;
1214
use ipl\Sql\Connection;
1315
use ipl\Sql\Expression;
1416

@@ -18,16 +20,20 @@ class ScanCommand extends Command
1820
* Scans IP and port ranges to find X.509 certificates.
1921
*
2022
* This command starts scanning the IP and port ranges which belong to the job that was specified with the
21-
* --job parameter.
23+
* --job parameter and optional --last-scan and --last-seen parameter.
2224
*
2325
* USAGE
2426
*
25-
* icingacli x509 scan --job <name>
27+
* icingacli x509 scan --job <name> --since-last-scan <days> --since-last-seen <days>
2628
*/
2729
public function indexAction()
2830
{
2931
$name = $this->params->shiftRequired('job');
3032

33+
$sinceLastSeen = $this->processThresholdToDateTiime($this->params->get('since-last-seen'));
34+
35+
$sinceLastScan = $this->processThresholdToDateTiime($this->params->get('since-last-scan'));
36+
3137
$parallel = (int) $this->Config()->get('scan', 'parallel', 256);
3238

3339
if ($parallel <= 0) {
@@ -60,19 +66,66 @@ public function indexAction()
6066
$name
6167
);
6268

63-
// $this->cleanupTargets($this->getDb());
69+
if ($sinceLastScan !== null) {
70+
$this->cleanupLastScanTargets($this->getDb(), $sinceLastScan);
71+
}
6472

6573
$verified = CertificateUtils::verifyCertificates($this->getDb());
6674

75+
if ($sinceLastSeen !== null) {
76+
$this->cleanupLastSeenTargets($this->getDb(), $sinceLastSeen);
77+
CertificateUtils::cleanupCertificates($this->getDb(), $sinceLastSeen);
78+
}
79+
80+
CertificateUtils::cleanupCertificateChains($this->getDb());
6781
Logger::info("Checked %d certificate chain%s.", $verified, $verified !== 1 ? 's' : '');
6882
}
6983
}
7084

71-
protected function cleanupTargets(Connection $db)
85+
protected function cleanupLastScanTargets(Connection $db, DateTime $sincLastScan)
86+
{
87+
$db->delete(
88+
'x509_target',
89+
['last_scan < ?' => $sincLastScan->getTimestamp()]
90+
);
91+
}
92+
93+
protected function cleanupLastSeenTargets(Connection $db, DateTime $sincLastSeen)
7294
{
7395
$db->delete(
7496
'x509_target',
75-
['last_seen = ?' => new Expression('NOW() - INTERVAL 1 DAY')]
97+
[new Expression(sprintf(
98+
"last_seen < last_scan - %d",
99+
(new DateTime())->getTimestamp() - $sincLastSeen->getTimestamp()
100+
))]
76101
);
77102
}
103+
104+
/**
105+
* @param string|null $threshold
106+
* @return DateTime|null
107+
*/
108+
protected function processThresholdToDateTiime(?string $threshold)
109+
{
110+
$since = $threshold;
111+
if ($threshold !== null) {
112+
if ($since[0] !== '-') {
113+
// When the user specified "2 days" as a threshold strtotime() will compute the
114+
// timestamp NOW() + 2 days, but it has to be NOW() + (-2 days)
115+
$since = "-$since";
116+
}
117+
118+
$since = strtotime($since);
119+
if ($since === false) {
120+
throw new InvalidArgumentException(sprintf(
121+
"The specified last scan time is in an unknown format: '%s'",
122+
$threshold
123+
));
124+
}
125+
126+
$since = (new DateTime())->setTimestamp($since);
127+
}
128+
129+
return $since;
130+
}
78131
}

etc/schema/mysql.schema.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ CREATE TABLE x509_certificate (
1818
fingerprint binary(32) NOT NULL COMMENT 'sha256 hash',
1919
`serial` blob NOT NULL,
2020
certificate blob NOT NULL COMMENT 'DER encoded certificate',
21+
last_seen bigint(20) NOT NULL,
2122
ctime timestamp NULL DEFAULT NULL,
2223
mtime timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
2324
PRIMARY KEY (id),
@@ -86,6 +87,8 @@ CREATE TABLE x509_target (
8687
hostname varchar(255) NULL DEFAULT NULL,
8788
latest_certificate_chain_id int(10) unsigned NULL DEFAULT NULL,
8889
ctime timestamp NULL DEFAULT NULL,
90+
last_scan bigint(20) NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())),
91+
last_seen bigint(20) NOT NULL,
8992
mtime timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
9093
PRIMARY KEY (id),
9194
INDEX x509_idx_target_ip_port (ip, port)

etc/schema/postgresql.schema.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ CREATE TABLE x509_certificate (
3535
valid_to bigint NOT NULL,
3636
fingerprint bytea NOT NULL,
3737
serial bytea NOT NULL,
38+
last_seen bigint NOT NULL,
3839
certificate bytea NOT NULL,
3940
ctime timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
4041
mtime timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -98,6 +99,8 @@ CREATE TABLE x509_target (
9899
port uint2 NOT NULL,
99100
hostname varchar(255) NULL DEFAULT NULL,
100101
latest_certificate_chain_id int NULL DEFAULT NULL,
102+
last_seen bigint NOT NULL,
103+
last_scan bigint NOT NULL,
101104
ctime timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
102105
mtime timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
103106
);

library/X509/CertificateUtils.php

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Icinga\Module\X509;
66

7+
use DateTime;
78
use Exception;
89
use Icinga\Application\Logger;
910
use Icinga\File\Storage\TemporaryLocalFileStorage;
@@ -246,7 +247,7 @@ public static function findOrInsertCert(Connection $db, $cert)
246247
'fingerprint' => $dbTool->marshalBinary($fingerprint),
247248
'serial' => $dbTool->marshalBinary(gmp_export($certInfo['serialNumber'])),
248249
'certificate' => $dbTool->marshalBinary($der),
249-
'last_seen' => new Expression('NOW()')
250+
'last_seen' => (new DateTime())->getTimestamp()
250251
]
251252
);
252253

@@ -355,24 +356,25 @@ private static function findOrInsertDn($db, $certInfo, $type)
355356
return $hash;
356357
}
357358

358-
public static function cleanupCertificates(Connection $db)
359+
public static function cleanupCertificates(Connection $db, DateTime $lastSeen)
359360
{
360361
$db->delete(
361362
'x509_certificate',
362-
['last_seen = ?' => new Expression('NOW() - INTERVAL 7 DAY')]
363+
[
364+
'last_seen < ?' => $lastSeen->getTimestamp(),
365+
'trusted = ?' => 'no'
366+
]
363367
);
368+
}
364369

370+
public static function cleanupCertificateChains(Connection $db)
371+
{
365372
$db->delete(
366-
'x509_dn',
367-
['last_seen' => new Expression('NOW() - INTERVAL 7 DAY')]
373+
'x509_certificate_chain',
374+
['target_id NOT IN (?)' => (new Select())
375+
->from('x509_target')
376+
->columns(['id'])]
368377
);
369-
370-
// DELETE
371-
//FROM x509_dn
372-
//WHERE type='subject' AND HASH <> ALL (
373-
// SELECT DISTINCT subject_hash
374-
//FROM x509_certificate
375-
//);
376378
}
377379

378380
/**
@@ -438,9 +440,10 @@ public static function verifyCertificates(Connection $db)
438440
foreach ($certs as $cert) {
439441
$db->update(
440442
'x509_certificate',
441-
['last_seen' => new Expression('NOW()')],
442-
['id = ?' => $chain->id]
443+
['last_seen' => (new DateTime())->getTimestamp()],
444+
['id = ?' => $cert->id]
443445
);
446+
444447
$collection[] = CertificateUtils::der2pem(DbTool::unmarshalBinary($cert->certificate));
445448
}
446449

library/X509/Job.php

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Icinga\Module\X509;
66

7+
use DateTime;
78
use Icinga\Application\Config;
89
use Icinga\Application\Logger;
910
use Icinga\Data\ConfigObject;
@@ -41,6 +42,7 @@ class Job
4142
private $snimap;
4243
private $parallel;
4344
private $name;
45+
private $currentDateTime;
4446

4547
public function __construct($name, ConfigObject $jobDescription, array $snimap, $parallel)
4648
{
@@ -50,6 +52,7 @@ public function __construct($name, ConfigObject $jobDescription, array $snimap,
5052
$this->snimap = $snimap;
5153
$this->parallel = $parallel;
5254
$this->name = $name;
55+
$this->currentDateTime = (new DateTime())->getTimestamp();
5356
}
5457

5558
private function getConnector($peerName)
@@ -85,7 +88,7 @@ private static function numberToAddr($num, $ipv6 = true)
8588
}
8689
}
8790

88-
private function generateTargets(ConfigObject $jobDescription, array $hostnamesConfig)
91+
private static function generateTargets(ConfigObject $jobDescription, array $hostnamesConfig)
8992
{
9093
foreach (StringHelper::trimSplit($jobDescription->get('cidrs')) as $cidr) {
9194
$pieces = explode('/', $cidr);
@@ -124,17 +127,6 @@ private function generateTargets(ConfigObject $jobDescription, array $hostnamesC
124127
foreach (range($start_port, $end_port) as $port) {
125128
$hostnames = isset($hostnamesConfig[$ip]) ? $hostnamesConfig[$ip] : [];
126129

127-
if (! empty($hostnames)) {
128-
$this->db->delete(
129-
'x509_target',
130-
[
131-
'ip = ?' => $this->dbTool->marshalBinary(static::binary($ip)),
132-
'port = ?' => $port,
133-
'hostname NOT IN (?)' => array_values($hostnames)
134-
]
135-
);
136-
}
137-
138130
if (empty($hostnames)) {
139131
$hostnames[] = null;
140132
}
@@ -144,8 +136,6 @@ private function generateTargets(ConfigObject $jobDescription, array $hostnamesC
144136
$target->ip = $ip;
145137
$target->port = $port;
146138
$target->hostname = $hostname;
147-
148-
$this->updateLastScan($target);
149139
yield $target;
150140
}
151141
}
@@ -216,6 +206,16 @@ function (ConnectionInterface $conn) use ($target, $streamCapture) {
216206
// Close connection in order to capture stream context options
217207
$conn->close();
218208

209+
$this->db->update(
210+
'x509_target',
211+
['last_seen' => $this->currentDateTime],
212+
[
213+
'ip = ?' => $this->dbTool->marshalBinary(static::binary($target->ip)),
214+
'port = ?' => $target->port,
215+
'hostname = ?' => $target->hostname
216+
]
217+
);
218+
219219
$capturedStreamOptions = $streamCapture->getCapturedStreamOptions();
220220

221221
$this->processChain($target, $capturedStreamOptions['ssl']['peer_certificate_chain']);
@@ -253,25 +253,19 @@ function (\Exception $exception) use ($target, $streamCapture) {
253253
)->otherwise(function (\Exception $e) {
254254
echo $e->getMessage() . PHP_EOL;
255255
echo $e->getTraceAsString() . PHP_EOL;
256+
})->always(function () use ($target) {
257+
$this->db->update(
258+
'x509_target',
259+
['last_scan' => $this->currentDateTime],
260+
[
261+
'ip = ?' => $this->dbTool->marshalBinary(static::binary($target->ip)),
262+
'port = ?' => $target->port,
263+
'hostname = ?' => $target->hostname
264+
]
265+
);
256266
});
257267
}
258268

259-
protected function updateLastScan($target)
260-
{
261-
if (isset($target->id)) {
262-
$filter = ['id = ?' => $target->id];
263-
} else {
264-
$filter = [
265-
'ip = ?' => $this->dbTool->marshalBinary(static::binary($target->ip)),
266-
'port = ?' => $target->port,
267-
'hostname = ?' => $target->hostname
268-
];
269-
}
270-
271-
$now = new Expression('NOW()');
272-
$this->db->update('x509_target', ['last_scan' => $now], $filter);
273-
}
274-
275269
public function getJobId()
276270
{
277271
return $this->jobId;
@@ -281,13 +275,13 @@ public function run()
281275
{
282276
$this->loop = Factory::create();
283277

284-
$this->totalTargets = iterator_count($this->generateTargets($this->jobDescription, $this->snimap));
278+
$this->totalTargets = iterator_count(static::generateTargets($this->jobDescription, $this->snimap));
285279

286280
if ($this->totalTargets == 0) {
287281
return null;
288282
}
289283

290-
$this->targets = $this->generateTargets($this->jobDescription, $this->snimap);
284+
$this->targets = static::generateTargets($this->jobDescription, $this->snimap);
291285

292286
$this->db->insert(
293287
'x509_job_run',
@@ -339,7 +333,8 @@ protected function processChain($target, $chain)
339333
'ip' => $this->dbTool->marshalBinary(static::binary($target->ip)),
340334
'port' => $target->port,
341335
'hostname' => $target->hostname,
342-
'last_scan' => new Expression('NOW()')
336+
'last_scan' => $this->currentDateTime,
337+
'last_seen' => $this->currentDateTime
343338
]
344339
);
345340
$targetId = $this->db->lastInsertId();
@@ -401,7 +396,7 @@ protected function processChain($target, $chain)
401396

402397
$this->db->update(
403398
'x509_certificate',
404-
['last_seen' => new Expression('NOW()')],
399+
['last_seen' => $this->currentDateTime],
405400
['id = ?' => $certId]
406401
);
407402

@@ -419,9 +414,7 @@ protected function processChain($target, $chain)
419414
$this->db->update(
420415
'x509_target',
421416
[
422-
'latest_certificate_chain_id' => $chainId,
423-
'last_seen' => new Expression('NOW()')
424-
],
417+
'latest_certificate_chain_id' => $chainId ],
425418
['id = ?' => $targetId]
426419
);
427420
});

0 commit comments

Comments
 (0)