diff --git a/app/CertificateScan.php b/app/CertificateScan.php new file mode 100644 index 0000000..a918468 --- /dev/null +++ b/app/CertificateScan.php @@ -0,0 +1,27 @@ + 'array', + 'valid_from' => 'datetime', + 'valid_to' => 'datetime', + 'was_valid' => 'boolean', + 'did_expire' => 'boolean', + ]; + + protected $appends = [ + 'expires_in', + ]; + + public function getExpiresInAttribute() + { + return now()->diffAsCarbonInterval($this->valid_to)->forHumans(['join' => true]); + } +} diff --git a/app/Checkers/Certificate.php b/app/Checkers/Certificate.php new file mode 100644 index 0000000..005755b --- /dev/null +++ b/app/Checkers/Certificate.php @@ -0,0 +1,66 @@ +website = $website; + } + + public function run() + { + $this->fetch(); + $this->notify(); + } + + private function fetch() + { + $certificate = SslCertificate::createForHostName($this->website->certificate_hostname); + + $scan = new CertificateScan([ + 'issuer' => $certificate->getIssuer(), + 'domain' => $certificate->getDomain(), + 'additional_domains' => $certificate->getAdditionalDomains(), + 'valid_from' => $certificate->validFromDate(), + 'valid_to' => $certificate->expirationDate(), + 'was_valid' => $certificate->isValid(), + 'did_expire' => $certificate->isExpired(), + ]); + + $labs = new \VisualAppeal\SslLabs(); + + $result = $labs->analyze( + $this->website->certificate_hostname, + $publish = false, + $startNew = false, + $fromCache = true, + $maxAge = 12, + $all = null, + $ignoreMismatch = false + ); + + foreach ($result->endpoints ?? [] as $endpoint) { + if ($endpoint->statusMessage === 'Ready') { + $scan->grade = $endpoint->grade; + $this->website->certificates()->save($scan); + break; + } + } + + dump($scan->exists ? 'Cert Updated' : 'Pending...'); + } + + private function notify() + { + + } +} diff --git a/app/Checkers/Dns.php b/app/Checkers/Dns.php new file mode 100644 index 0000000..b5c0ead --- /dev/null +++ b/app/Checkers/Dns.php @@ -0,0 +1,78 @@ +website = $website; + } + + public function run() + { + $this->fetch(); + $this->compare(); + $this->notify(); + } + + private function fetch() + { + $response = (new DNSParser('array'))->lookup($this->website->dns_hostname); + + $flat = collect($response['records'])->transform(function ($item) { + return sprintf( + '%s %s %s ttl:%d', + $item['host'], + $item['type'], + $item['ip'] ?? $item['target'] ?? $item['mname'] ?? $item['txt'] ?? $item['ipv6'] ?? '', + $item['ttl'] + ); + })->sort()->values()->implode("\n"); + + $scan = new DnsScan([ + 'records' => $response['records'], + 'flat' => $flat, + ]); + + $this->website->dns()->save($scan); + } + + private function compare() + { + $scans = $this->website->last_dns_scans; + + if ($scans->isEmpty() || $scans->count() === 1) { + return; + } + + $diff = (new Differ)->diff( + $scans->last()->flat, + $scans->first()->flat + ); + + $placeholder = '--- Original ++++ New +'; + + if ($diff === $placeholder) { + $diff = null; + } + + $scans->first()->diff = $diff; + $scans->first()->save(); + } + + private function notify() + { + + } +} diff --git a/app/Checkers/Robots.php b/app/Checkers/Robots.php index 686fbd2..bb2fc6b 100644 --- a/app/Checkers/Robots.php +++ b/app/Checkers/Robots.php @@ -46,6 +46,14 @@ private function compare() $diff = (new Differ)->diff($scans->last()->txt, $scans->first()->txt); + $placeholder = '--- Original ++++ New +'; + + if ($diff === $placeholder) { + $diff = null; + } + $scans->first()->diff = $diff; $scans->first()->save(); } diff --git a/app/Checkers/Uptime.php b/app/Checkers/Uptime.php index f1f8958..c9001a6 100644 --- a/app/Checkers/Uptime.php +++ b/app/Checkers/Uptime.php @@ -2,11 +2,9 @@ namespace App\Checkers; -use App\RobotScan; +use App\Website; use App\UptimeScan; use GuzzleHttp\Client; -use App\Website; -use GuzzleHttp\TransferStats; use Illuminate\Support\Str; use SebastianBergmann\Diff\Differ; @@ -23,7 +21,7 @@ public function __construct(Website $website) public function run() { $this->fetch(); -// $this->compare(); + $this->notify(); } private function fetch() @@ -43,26 +41,25 @@ private function fetch() ], ]); + $keywordFound = Str::contains($response->getBody(), $this->website->uptime_keyword); + + if (!$keywordFound && $response->getStatusCode() == '200') { + $reason = sprintf('Keyword: %s not found (%d)', $this->website->uptime_keyword, 200); + } else { + $reason = sprintf('%s (%d)', $response->getReasonPhrase(), $response->getStatusCode()); + } + $scan = new UptimeScan([ - 'response_status' => sprintf('%s (%d)', $response->getReasonPhrase(), $response->getStatusCode()), + 'response_status' => $reason, 'response_time' => $response_time, - 'was_online' => Str::contains($response->getBody(), $this->website->uptime_keyword) + 'was_online' => $keywordFound, ]); $this->website->uptimes()->save($scan); } - private function compare() + private function notify() { - $scans = $this->website->last_robot_scans; - - if ($scans->isEmpty() || $scans->count() === 1) { - return; - } - - $diff = (new Differ)->diff($scans->last()->txt, $scans->first()->txt); - $scans->first()->diff = $diff; - $scans->first()->save(); } } diff --git a/app/Console/Commands/AllCheckersCommand.php b/app/Console/Commands/AllCheckersCommand.php new file mode 100644 index 0000000..5928d97 --- /dev/null +++ b/app/Console/Commands/AllCheckersCommand.php @@ -0,0 +1,53 @@ +argument('website'); + $website = Website::findOrFail($websiteId); + + DnsCheck::dispatchNow($website); + RobotsCheck::dispatchNow($website); + UptimeCheck::dispatchNow($website); + CertificateCheck::dispatchNow($website); + } +} diff --git a/app/Console/Commands/CertificateCheckCommand.php b/app/Console/Commands/CertificateCheckCommand.php new file mode 100644 index 0000000..8a663d3 --- /dev/null +++ b/app/Console/Commands/CertificateCheckCommand.php @@ -0,0 +1,48 @@ +argument('website'); + + CertificateCheck::dispatchNow( + Website::findOrFail($websiteId) + ); + } +} diff --git a/app/Console/Commands/DnsCheckCommand.php b/app/Console/Commands/DnsCheckCommand.php new file mode 100644 index 0000000..b5441ee --- /dev/null +++ b/app/Console/Commands/DnsCheckCommand.php @@ -0,0 +1,48 @@ +argument('website'); + + DnsCheck::dispatchNow( + Website::findOrFail($websiteId) + ); + } +} diff --git a/app/DnsScan.php b/app/DnsScan.php new file mode 100644 index 0000000..5e40063 --- /dev/null +++ b/app/DnsScan.php @@ -0,0 +1,19 @@ + 'array', + ]; + + protected $hidden = [ + 'website_id', 'records', + 'updated_at', 'diff', + ]; +} diff --git a/app/HasCertificates.php b/app/HasCertificates.php new file mode 100644 index 0000000..ef3b423 --- /dev/null +++ b/app/HasCertificates.php @@ -0,0 +1,16 @@ +hasMany(CertificateScan::class); + } + + public function getCertificateHostnameAttribute() + { + return parse_url($this->url, PHP_URL_HOST); + } +} diff --git a/app/HasDns.php b/app/HasDns.php new file mode 100644 index 0000000..9ccd6d9 --- /dev/null +++ b/app/HasDns.php @@ -0,0 +1,20 @@ +hasMany(DnsScan::class); + } + + public function getLastDnsScansAttribute() + { + return $this->dns()->orderBy('created_at', 'desc')->take(2)->get(); + } + public function getDnsHostnameAttribute() + { + return str_replace('www.', '', parse_url($this->url, PHP_URL_HOST)); + } +} diff --git a/app/HasUptime.php b/app/HasUptime.php index 48d8446..be3f150 100644 --- a/app/HasUptime.php +++ b/app/HasUptime.php @@ -1,9 +1,7 @@ hasMany(UptimeScan::class)->orderBy('created_at', 'desc'); } - public function getRecentEventsAttribute() - { - return $this->uptimes->sortByDesc('created_at')->take(10); - } - public function getLastIncidentAttribute() { $event = $this->uptimes @@ -103,6 +96,15 @@ public function getUptimeSummaryAttribute() /* @var Collection $events */ $events = $this->uptimes; + if ($events->isEmpty()) { + return [ + 'total' => 0, + 'day' => 0, + 'week' => 0, + 'month' => 0, + ]; + } + $upCount = $events->where('was_online', 1); $totalPercentage = ($upCount->count() * 100) / $events->count(); @@ -138,4 +140,53 @@ public function getUptimeSummaryAttribute() 'month' => floor($monthlyPercentage), ]; } + + public function getRecentEventsAttribute() + { + //'id' => $scan->getKey(), + //'date' => $scan->created_at, + //'type' => $scan->was_online ? 'up' : 'down', + //'reason' => $scan->response_status, + //'duration' => 10, + + $events = $this->uptimes->sortByDesc('created_at')->values(); + + $grouped = []; + $lastType = null; + + foreach ($events as $pos => $event) { + // If its null then we've only just started! + if (is_null($lastType)) { + $grouped[] = collect(); + $lastType = $event->online; + } + + // If the event is the same as the previous + // we just bundle them together + if ($lastType === $event->online) { + $last = end($grouped); + $last->push($event); + } + // if its a different event, we start a new group! + else { + $lastType = $event->online; + $grouped[] = collect(); + $last = end($grouped); + $last->push($event); + } + } + + $grouped = collect($grouped)->take(10); + + return $grouped->transform(function ($group) { + $events = $group->sortBy('created_at'); + + return [ + 'state' => $events->first()->online ? 'up' : 'down', + 'reason' => $events->first()->response_status, + 'date' => $events->first()->created_at, + 'duration' => $events->first()->created_at->diffAsCarbonInterval($events->last()->created_at)->forHumans(['join' => true]), + ]; + }); + } } diff --git a/app/Http/Controllers/CertificateReportController.php b/app/Http/Controllers/CertificateReportController.php new file mode 100644 index 0000000..87a16af --- /dev/null +++ b/app/Http/Controllers/CertificateReportController.php @@ -0,0 +1,28 @@ +has('refresh')) { + CertificateCheck::dispatchNow($website); + } + + $scan = $website->certificates()->latest()->first(); + + return $scan; + } +} diff --git a/app/Http/Controllers/DnsCompareController.php b/app/Http/Controllers/DnsCompareController.php new file mode 100644 index 0000000..828f361 --- /dev/null +++ b/app/Http/Controllers/DnsCompareController.php @@ -0,0 +1,38 @@ +has('refresh')) { + DnsCheck::dispatchNow($website); + } + + $scans = $website->last_dns_scans; + + if ($scans->isEmpty()) { + return [ + 'now' => null, + 'previous' => null, + ]; + } + + return [ + 'now' => $scans[0], + 'previous' => $scans[1] ?? null, + ]; + } +} diff --git a/app/Http/Controllers/RobotCompareController.php b/app/Http/Controllers/RobotCompareController.php index ec0adef..8b600ec 100644 --- a/app/Http/Controllers/RobotCompareController.php +++ b/app/Http/Controllers/RobotCompareController.php @@ -11,6 +11,7 @@ class RobotCompareController extends Controller /** * Handle the incoming request. * + * @param Request $request * @param Website $website * @return array */ diff --git a/app/Http/Controllers/UptimeReportController.php b/app/Http/Controllers/UptimeReportController.php index b4bda37..301f382 100644 --- a/app/Http/Controllers/UptimeReportController.php +++ b/app/Http/Controllers/UptimeReportController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; -use App\UptimeScan; use App\Website; +use App\Jobs\UptimeCheck; use Illuminate\Http\Request; class UptimeReportController extends Controller @@ -11,12 +11,16 @@ class UptimeReportController extends Controller /** * Handle the incoming request. * - * @param \Illuminate\Http\Request $request + * @param Request $request * @param Website $website * @return array */ public function __invoke(Request $request, Website $website) { + if ($request->has('refresh')) { + UptimeCheck::dispatchNow($website); + } + $website->load(['uptimes']); $response = [ @@ -26,15 +30,7 @@ public function __invoke(Request $request, Website $website) 'online' => $website->current_state, 'online_time' => $website->uptime, 'last_incident' => $website->last_incident, - 'events' => $website->recent_events->transform(function (UptimeScan $scan) { - return [ - 'id' => $scan->getKey(), - 'date' => $scan->created_at, - 'type' => $scan->was_online ? 'up' : 'down', - 'reason' => $scan->response_status, - 'duration' => 10, - ]; - })->values(), + 'events' => $website->recent_events, ]; // return view('debug', ['data' => $response]); diff --git a/app/Jobs/CertificateCheck.php b/app/Jobs/CertificateCheck.php new file mode 100644 index 0000000..601c2ad --- /dev/null +++ b/app/Jobs/CertificateCheck.php @@ -0,0 +1,42 @@ +website = $website; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $checker = new Certificate($this->website); + $checker->run(); + } +} diff --git a/app/Jobs/DnsCheck.php b/app/Jobs/DnsCheck.php new file mode 100644 index 0000000..597c2c8 --- /dev/null +++ b/app/Jobs/DnsCheck.php @@ -0,0 +1,42 @@ +website = $website; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $checker = new Dns($this->website); + $checker->run(); + } +} diff --git a/app/Website.php b/app/Website.php index 7140ce8..de90ec7 100644 --- a/app/Website.php +++ b/app/Website.php @@ -8,6 +8,8 @@ class Website extends Model { use HasUptime; use HasRobots; + use HasCertificates; + use HasDns; protected $fillable = [ 'url', diff --git a/composer.json b/composer.json index 6e6097b..a34b272 100644 --- a/composer.json +++ b/composer.json @@ -9,12 +9,20 @@ "license": "MIT", "require": { "php": "^7.1.3", + "amhr/laravel-dnsparser": "^1.3@dev", + "andyftw/ssllabs-php": "^1.2", + "doctrine/annotations": "^1.7", "fideloper/proxy": "^4.0", "guzzlehttp/guzzle": "^6.3", "laravel/framework": "5.8.*", "laravel/tinker": "^1.0", "maelstrom-cms/toolkit": "^1.0", - "sebastian/diff": "^3.0" + "sebastian/diff": "^3.0", + "spatie/dns": "^1.4", + "spatie/ssl-certificate": "^1.15", + "visualappeal/php-ssllabs-api": "^1.0", + "whoisdoma/dnsparser": "dev-master", + "whoisdoma/domainparser": "dev-master" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.2", diff --git a/composer.lock b/composer.lock index 316890c..b2dcb40 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,87 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ece6160ed320638076330a17011cc2bd", + "content-hash": "ee4227d4054badebf0147d1038e752de", "packages": [ + { + "name": "amhr/laravel-dnsparser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Amhr/laravel-dnsparser.git", + "reference": "8038b0ad01d7fa1ada2f212bab4c5d70e3b0a262" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Amhr/laravel-dnsparser/zipball/8038b0ad01d7fa1ada2f212bab4c5d70e3b0a262", + "reference": "8038b0ad01d7fa1ada2f212bab4c5d70e3b0a262", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Mhr\\DNSParser\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "authors": [ + { + "name": "Mohammad", + "email": "pp2007ws@yahoo.com" + } + ], + "time": "2017-12-16T12:12:54+00:00" + }, + { + "name": "andyftw/ssllabs-php", + "version": "v1.2", + "source": { + "type": "git", + "url": "https://github.com/andyftw/ssllabs-php.git", + "reference": "c14869253419916c26086de9e199030763876de4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/andyftw/ssllabs-php/zipball/c14869253419916c26086de9e199030763876de4", + "reference": "c14869253419916c26086de9e199030763876de4", + "shasum": "" + }, + "require": { + "jms/serializer": "^0.16.0", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Andyftw\\SSLLabs\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andy", + "email": "info@andyfront.de", + "homepage": "https://andyfront.de" + } + ], + "description": "SSL Labs API PHP-Port", + "homepage": "https://github.com/andyftw/ssllabs-php", + "keywords": [ + "ssllabs" + ], + "time": "2016-05-24T23:25:27+00:00" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "0.1", @@ -39,6 +118,74 @@ "description": "implementation of xdg base directory specification for php", "time": "2014-10-24T07:27:01+00:00" }, + { + "name": "doctrine/annotations", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "fa4c4e861e809d6a1103bd620cce63ed91aedfeb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/fa4c4e861e809d6a1103bd620cce63ed91aedfeb", + "reference": "fa4c4e861e809d6a1103bd620cce63ed91aedfeb", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^7.1" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^7.5@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2019-08-08T18:11:40+00:00" + }, { "name": "doctrine/inflector", "version": "v1.3.0", @@ -725,6 +872,166 @@ "description": "Highlight PHP code in terminal", "time": "2018-09-29T18:48:56+00:00" }, + { + "name": "jms/metadata", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "time": "2018-10-26T12:40:10+00:00" + }, + { + "name": "jms/parser-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/parser-lib.git", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "shasum": "" + }, + "require": { + "phpoption/phpoption": ">=0.9,<2.0-dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "description": "A library for easily creating recursive-descent parsers.", + "time": "2012-11-18T18:08:43+00:00" + }, + { + "name": "jms/serializer", + "version": "0.16.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "c8a171357ca92b6706e395c757f334902d430ea9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/c8a171357ca92b6706e395c757f334902d430ea9", + "reference": "c8a171357ca92b6706e395c757f334902d430ea9", + "shasum": "" + }, + "require": { + "doctrine/annotations": "1.*", + "jms/metadata": "~1.1", + "jms/parser-lib": "1.*", + "php": ">=5.3.2", + "phpcollection/phpcollection": "~0.1" + }, + "require-dev": { + "doctrine/orm": "~2.1", + "doctrine/phpcr-odm": "~1.0.1", + "jackalope/jackalope-doctrine-dbal": "1.0.*", + "propel/propel1": "~1.7", + "symfony/filesystem": "2.*", + "symfony/form": "~2.1", + "symfony/translation": "~2.0", + "symfony/validator": "~2.0", + "symfony/yaml": "2.*", + "twig/twig": ">=1.8,<2.0-dev" + }, + "suggest": { + "symfony/yaml": "Required if you'd like to serialize data to YAML format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.15-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\Serializer": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes Schmitt", + "role": "Developer of wrapped JMSSerializerBundle", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "time": "2014-03-18T08:39:00+00:00" + }, { "name": "laravel/framework", "version": "v5.8.32", @@ -1372,6 +1679,54 @@ ], "time": "2018-07-02T15:55:56+00:00" }, + { + "name": "phpcollection/phpcollection", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-collection.git", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "shasum": "" + }, + "require": { + "phpoption/phpoption": "1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4-dev" + } + }, + "autoload": { + "psr-0": { + "PhpCollection": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "General-Purpose Collection Library for PHP", + "keywords": [ + "collection", + "list", + "map", + "sequence", + "set" + ], + "time": "2015-05-17T12:39:23+00:00" + }, { "name": "phpoption/phpoption", "version": "1.5.0", @@ -1868,6 +2223,159 @@ ], "time": "2019-02-04T06:01:07+00:00" }, + { + "name": "spatie/dns", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/dns.git", + "reference": "403865f19c01e0b9cfa9356fba5748dd5307b35c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/dns/zipball/403865f19c01e0b9cfa9356fba5748dd5307b35c", + "reference": "403865f19c01e0b9cfa9356fba5748dd5307b35c", + "shasum": "" + }, + "require": { + "php": "^7.1", + "symfony/process": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "role": "Developer", + "email": "freek@spatie.be", + "homepage": "https://spatie.be" + }, + { + "name": "Harish Toshniwal", + "role": "Developer", + "email": "harish@spatie.be", + "homepage": "https://spatie.be" + } + ], + "description": "Retrieve DNS records", + "homepage": "https://github.com/spatie/dns", + "keywords": [ + "dns", + "spatie" + ], + "time": "2019-05-17T07:19:43+00:00" + }, + { + "name": "spatie/macroable", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/macroable.git", + "reference": "74b0d189ce75142f1706aad834d5a428dfc7c3c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/macroable/zipball/74b0d189ce75142f1706aad834d5a428dfc7c3c3", + "reference": "74b0d189ce75142f1706aad834d5a428dfc7c3c3", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Macroable\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A trait to dynamically add methods to a class", + "homepage": "https://github.com/spatie/macroable", + "keywords": [ + "macroable", + "spatie" + ], + "time": "2017-09-18T09:51:20+00:00" + }, + { + "name": "spatie/ssl-certificate", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/ssl-certificate.git", + "reference": "3415761f8baba48050faf7bb79bfea8f16a9b771" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ssl-certificate/zipball/3415761f8baba48050faf7bb79bfea8f16a9b771", + "reference": "3415761f8baba48050faf7bb79bfea8f16a9b771", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "ext-json": "*", + "nesbot/carbon": "^1.15|^2.0", + "php": "^7.2", + "spatie/macroable": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0", + "spatie/phpunit-snapshot-assertions": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\SslCertificate\\": "src" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "role": "Developer", + "email": "freek@spatie.be", + "homepage": "https://spatie.be" + } + ], + "description": "A class to easily query the properties of an ssl certificate", + "homepage": "https://github.com/spatie/ssl-certificate", + "keywords": [ + "spatie", + "ssl-certificate" + ], + "time": "2019-07-22T20:45:19+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v6.2.1", @@ -3287,6 +3795,49 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "time": "2017-11-27T11:13:29+00:00" }, + { + "name": "visualappeal/php-ssllabs-api", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/VisualAppeal/php-ssllabs-api.git", + "reference": "51d5d6fab1613620ca7f5c3f88ce4d9936716435" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VisualAppeal/php-ssllabs-api/zipball/51d5d6fab1613620ca7f5c3f88ce4d9936716435", + "reference": "51d5d6fab1613620ca7f5c3f88ce4d9936716435", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "VisualAppeal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Tim Helfensdörfer", + "email": "tim@visualappeal.de" + }, + { + "name": "Björn Roland", + "email": "bjoern.roland@gmail.com" + } + ], + "description": "API for ssllabs.com", + "time": "2018-11-10T17:14:22+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v3.4.0", @@ -3338,6 +3889,77 @@ "environment" ], "time": "2019-06-15T22:40:20+00:00" + }, + { + "name": "whoisdoma/dnsparser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Whoisdoma/DNSParser.git", + "reference": "5eafb137569040988979bd16abc4288152d02c13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Whoisdoma/DNSParser/zipball/5eafb137569040988979bd16abc4288152d02c13", + "reference": "5eafb137569040988979bd16abc4288152d02c13", + "shasum": "" + }, + "require": { + "whoisdoma/domainparser": "dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "Whoisdoma\\DNSParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Preferm DNS lookups for domains", + "homepage": "https://github.com/Whoisdoma/DNSParser/", + "keywords": [ + "dns", + "dnsparser", + "domain dns", + "php", + "whois" + ], + "time": "2017-06-05T18:11:53+00:00" + }, + { + "name": "whoisdoma/domainparser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Whoisdoma/DomainParser.git", + "reference": "11179359e0f018ada15e38a2f2fd4b7226713a71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Whoisdoma/DomainParser/zipball/11179359e0f018ada15e38a2f2fd4b7226713a71", + "reference": "11179359e0f018ada15e38a2f2fd4b7226713a71", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Whoisdoma\\DomainParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A domain name parser to parse and to validate a domain name.", + "homepage": "https://github.com/Whoisdoma/DomainParser/", + "keywords": [ + "domain", + "domainparser", + "php" + ], + "time": "2016-12-28T16:51:52+00:00" } ], "packages-dev": [ @@ -5180,7 +5802,11 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": { + "amhr/laravel-dnsparser": 20, + "whoisdoma/dnsparser": 20, + "whoisdoma/domainparser": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/database/migrations/2019_08_18_175820_create_certificate_scans_table.php b/database/migrations/2019_08_18_175820_create_certificate_scans_table.php new file mode 100644 index 0000000..5aee87a --- /dev/null +++ b/database/migrations/2019_08_18_175820_create_certificate_scans_table.php @@ -0,0 +1,40 @@ +bigIncrements('id'); + $table->bigInteger('website_id'); + $table->string('issuer'); + $table->string('domain'); + $table->text('additional_domains'); + $table->dateTime('valid_from'); + $table->dateTime('valid_to'); + $table->boolean('was_valid'); + $table->boolean('did_expire'); + $table->string('grade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('certificate_scans'); + } +} diff --git a/database/migrations/2019_08_18_212716_create_dns_scans_table.php b/database/migrations/2019_08_18_212716_create_dns_scans_table.php new file mode 100644 index 0000000..c48f109 --- /dev/null +++ b/database/migrations/2019_08_18_212716_create_dns_scans_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->bigInteger('website_id'); + $table->text('records')->nullable(); + $table->text('flat')->nullable(); + $table->text('diff')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('dns_scans'); + } +} diff --git a/public/js/maelstrom.js b/public/js/maelstrom.js index 8330332..6918a02 100644 --- a/public/js/maelstrom.js +++ b/public/js/maelstrom.js @@ -322032,10 +322032,16 @@ function (_React$Component) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return CertificateReport; }); -/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js"); -/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/regenerator */ "./node_modules/@babel/runtime/regenerator/index.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react */ "./node_modules/react/index.js"); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var antd__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! antd */ "./node_modules/antd/es/index.js"); +/* harmony import */ var _helpers__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../helpers */ "./resources/js/helpers.js"); function _typeof2(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof2 = function _typeof2(obj) { return typeof obj; }; } else { _typeof2 = function _typeof2(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof2(obj); } + + function _typeof(obj) { if (typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol") { _typeof = function _typeof(obj) { @@ -322050,6 +322056,76 @@ function _typeof(obj) { return _typeof(obj); } +function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + if (enumerableOnly) symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + keys.push.apply(keys, symbols); + } + + return keys; +} + +function _objectSpread(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + + if (i % 2) { + ownKeys(source, true).forEach(function (key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(source).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); + } + } + + return target; +} + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + Promise.resolve(value).then(_next, _throw); + } +} + +function _asyncToGenerator(fn) { + return function () { + var self = this, + args = arguments; + return new Promise(function (resolve, reject) { + var gen = fn.apply(self, args); + + function _next(value) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); + } + + function _throw(err) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); + } + + _next(undefined); + }); + }; +} + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -322136,6 +322212,9 @@ function _defineProperty(obj, key, value) { + +var TreeNode = antd__WEBPACK_IMPORTED_MODULE_2__["Tree"].TreeNode; + var CertificateReport = /*#__PURE__*/ function (_React$Component) { @@ -322155,21 +322234,218 @@ function (_React$Component) { _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(CertificateReport)).call.apply(_getPrototypeOf2, [this].concat(args))); _defineProperty(_assertThisInitialized(_this), "state", { - website: JSON.parse(_this.props.website) + website: JSON.parse(_this.props.website), + loading: true, + issuer: null, + domain: null, + additional_domains: [], + valid_from: null, + valid_to: null, + was_valid: false, + did_expire: true, + grade: 'N/A', + expires_in: 'N/A' + }); + + _defineProperty(_assertThisInitialized(_this), "update", + /*#__PURE__*/ + _asyncToGenerator( + /*#__PURE__*/ + _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default.a.mark(function _callee() { + var refresh, + endpoint, + response, + _args = arguments; + return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default.a.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + refresh = _args.length > 0 && _args[0] !== undefined ? _args[0] : false; + _context.next = 3; + return _this.setState({ + loading: true + }); + + case 3: + endpoint = window.location.href + '/ssl'; + + if (refresh) { + endpoint += '?refresh=1'; + } + + _context.next = 7; + return window.axios.get(endpoint); + + case 7: + response = _context.sent.data; + + _this.setState(_objectSpread({}, response, { + loading: false + })); + + case 9: + case "end": + return _context.stop(); + } + } + }, _callee); + }))); + + _defineProperty(_assertThisInitialized(_this), "forceUpdate", function () { + return _this.update(true); + }); + + _defineProperty(_assertThisInitialized(_this), "renderLeft", function () { + var color = function color(grade) { + switch (grade) { + case 'A+': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["GREEN"]; + + case 'A': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["GREEN"]; + + case 'A-': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["GREEN"]; + + case 'B': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["YELLOW"]; + + case 'C': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["YELLOW"]; + + case 'D': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["RED"]; + + case 'E': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["RED"]; + + case 'F': + return _helpers__WEBPACK_IMPORTED_MODULE_3__["RED"]; + } + }; + + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_1___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Progress"], { + strokeColor: color(_this.state.grade), + type: "circle", + percent: 100, + format: function format() { + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", { + className: "text-6xl font-bold", + style: { + color: color(_this.state.grade) + } + }, _this.state.grade); + } + }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "mt-8 pr-12" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "font-bold mb-2" + }, "Issued By:"), _this.state.issuer), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "mt-2" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "font-bold mb-2" + }, "Issued On:"), Object(_helpers__WEBPACK_IMPORTED_MODULE_3__["formatDateTime"])(_this.state.valid_from)))); + }); + + _defineProperty(_assertThisInitialized(_this), "renderRight", function () { + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_1___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("h4", null, "Primary Domain"), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Tag"], { + color: "green" + }, _this.state.domain), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("h4", { + className: "mt-5" + }, "Additional Domains"), _this.state.additional_domains.map(function (i) { + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Tag"], { + className: "mb-2", + key: i + }, i); + }), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "flex flex-wrap mt-5" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-1/2 pr-5" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "font-bold mb-2" + }, "Expires At:"), Object(_helpers__WEBPACK_IMPORTED_MODULE_3__["formatDateTime"])(_this.state.valid_to)), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-1/2" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "font-bold mb-2" + }, "Valid Certificate:"), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "flex items-center", + style: { + color: _this.state.was_valid ? _helpers__WEBPACK_IMPORTED_MODULE_3__["GREEN"] : _helpers__WEBPACK_IMPORTED_MODULE_3__["RED"] + } + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Icon"], { + style: { + fontSize: 20, + marginRight: 10 + }, + type: _this.state.was_valid ? 'check-circle' : 'close-circle' + }), " ", _this.state.was_valid ? 'Yes' : 'No')), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-1/2 mt-5 pr-32" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "font-bold mb-2" + }, "Expires In:"), _this.state.expires_in), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-1/2 mt-5" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "font-bold mb-2" + }, "Has Expired:"), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "flex items-center", + style: { + color: _this.state.did_expire ? _helpers__WEBPACK_IMPORTED_MODULE_3__["RED"] : _helpers__WEBPACK_IMPORTED_MODULE_3__["GREEN"] + } + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Icon"], { + style: { + fontSize: 20, + marginRight: 10 + }, + type: _this.state.did_expire ? 'check-circle' : 'close-circle' + }), " ", _this.state.did_expire ? 'Yes' : 'No')))); + }); + + _defineProperty(_assertThisInitialized(_this), "renderReport", function () { + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "flex" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-1/4 pr-7" + }, _this.renderLeft()), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-3/4" + }, _this.renderRight())); + }); + + _defineProperty(_assertThisInitialized(_this), "renderBusy", function () { + var _this$state = _this.state, + now = _this$state.now, + previous = _this$state.previous; + + if (now && previous) { + return _this.renderReport(); + } + + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Spin"], null); }); return _this; } _createClass(CertificateReport, [{ + key: "componentDidMount", + value: function componentDidMount() { + this.update(); + } + }, { key: "render", value: function render() { - return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "xxxxxxxxxxxxxxxxx"); + var loading = this.state.loading; + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Card"], { + title: "SSL Report", + extra: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Button"], { + loading: loading, + onClick: this.forceUpdate + }, "Refresh") + }, !loading ? this.renderReport() : this.renderBusy())); } }]); return CertificateReport; -}(react__WEBPACK_IMPORTED_MODULE_0___default.a.Component); +}(react__WEBPACK_IMPORTED_MODULE_1___default.a.Component); @@ -322185,10 +322461,18 @@ function (_React$Component) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return DnsReport; }); -/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js"); -/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/regenerator */ "./node_modules/@babel/runtime/regenerator/index.js"); +/* harmony import */ var _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react */ "./node_modules/react/index.js"); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var antd__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! antd */ "./node_modules/antd/es/index.js"); +/* harmony import */ var react_diff_viewer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-diff-viewer */ "./node_modules/react-diff-viewer/lib/index.js"); +/* harmony import */ var react_diff_viewer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(react_diff_viewer__WEBPACK_IMPORTED_MODULE_3__); +/* harmony import */ var _helpers__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../helpers */ "./resources/js/helpers.js"); function _typeof2(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof2 = function _typeof2(obj) { return typeof obj; }; } else { _typeof2 = function _typeof2(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof2(obj); } + + function _typeof(obj) { if (typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol") { _typeof = function _typeof(obj) { @@ -322203,6 +322487,76 @@ function _typeof(obj) { return _typeof(obj); } +function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + if (enumerableOnly) symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + keys.push.apply(keys, symbols); + } + + return keys; +} + +function _objectSpread(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + + if (i % 2) { + ownKeys(source, true).forEach(function (key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(source).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); + } + } + + return target; +} + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + Promise.resolve(value).then(_next, _throw); + } +} + +function _asyncToGenerator(fn) { + return function () { + var self = this, + args = arguments; + return new Promise(function (resolve, reject) { + var gen = fn.apply(self, args); + + function _next(value) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); + } + + function _throw(err) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); + } + + _next(undefined); + }); + }; +} + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -322289,6 +322643,9 @@ function _defineProperty(obj, key, value) { + + + var DnsReport = /*#__PURE__*/ function (_React$Component) { @@ -322308,21 +322665,114 @@ function (_React$Component) { _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(DnsReport)).call.apply(_getPrototypeOf2, [this].concat(args))); _defineProperty(_assertThisInitialized(_this), "state", { - website: JSON.parse(_this.props.website) + website: JSON.parse(_this.props.website), + now: null, + previous: null, + loading: true + }); + + _defineProperty(_assertThisInitialized(_this), "update", + /*#__PURE__*/ + _asyncToGenerator( + /*#__PURE__*/ + _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default.a.mark(function _callee() { + var refresh, + endpoint, + response, + _args = arguments; + return _babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default.a.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + refresh = _args.length > 0 && _args[0] !== undefined ? _args[0] : false; + _context.next = 3; + return _this.setState({ + loading: true + }); + + case 3: + endpoint = window.location.href + '/dns'; + + if (refresh) { + endpoint += '?refresh=1'; + } + + _context.next = 7; + return window.axios.get(endpoint); + + case 7: + response = _context.sent.data; + + _this.setState(_objectSpread({}, response, { + loading: false + })); + + case 9: + case "end": + return _context.stop(); + } + } + }, _callee); + }))); + + _defineProperty(_assertThisInitialized(_this), "forceUpdate", function () { + return _this.update(true); + }); + + _defineProperty(_assertThisInitialized(_this), "renderReport", function () { + var _this$state = _this.state, + now = _this$state.now, + previous = _this$state.previous; + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_1___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "flex" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-1/2" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("h3", null, "Before (", previous && Object(_helpers__WEBPACK_IMPORTED_MODULE_4__["formatDateTime"])(previous.created_at), ")")), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { + className: "w-1/2" + }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("h3", null, "Now (", now && Object(_helpers__WEBPACK_IMPORTED_MODULE_4__["formatDateTime"])(now.created_at), ")"))), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(react_diff_viewer__WEBPACK_IMPORTED_MODULE_3___default.a, { + newValue: now ? now.flat : '', + oldValue: previous ? previous.flat : ' ', + splitView: true, + showDiffOnly: false + })); + }); + + _defineProperty(_assertThisInitialized(_this), "renderBusy", function () { + var _this$state2 = _this.state, + now = _this$state2.now, + previous = _this$state2.previous; + + if (now && previous) { + return _this.renderReport(); + } + + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Spin"], null); }); return _this; } _createClass(DnsReport, [{ + key: "componentDidMount", + value: function componentDidMount() { + this.update(); + } + }, { key: "render", value: function render() { - return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "xxxxxxxxxxxxxxxxx"); + var loading = this.state.loading; + return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", null, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Card"], { + title: "DNS Records", + extra: react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Button"], { + loading: loading, + onClick: this.forceUpdate + }, "Refresh") + }, !loading ? this.renderReport() : this.renderBusy())); } }]); return DnsReport; -}(react__WEBPACK_IMPORTED_MODULE_0___default.a.Component); +}(react__WEBPACK_IMPORTED_MODULE_1___default.a.Component); @@ -322870,9 +323320,6 @@ function _defineProperty(obj, key, value) { -var GREEN = '#72c040'; -var YELLOW = '#efaf41'; -var RED = '#e23c39'; var UptimeReport = /*#__PURE__*/ @@ -323100,14 +323547,14 @@ function (_React$Component) { var color = function color(uptime) { if (uptime >= 90) { - return GREEN; + return _helpers__WEBPACK_IMPORTED_MODULE_4__["GREEN"]; } if (uptime <= 50) { - return RED; + return _helpers__WEBPACK_IMPORTED_MODULE_4__["RED"]; } - return YELLOW; + return _helpers__WEBPACK_IMPORTED_MODULE_4__["YELLOW"]; }; var icon = function icon(uptime) { @@ -323178,7 +323625,7 @@ function (_React$Component) { }), " Last Downtime")), react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("div", { className: "mt-2", style: { - color: RED + color: _helpers__WEBPACK_IMPORTED_MODULE_4__["RED"] } }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("p", null, this.state.last_incident))); } @@ -323186,8 +323633,8 @@ function (_React$Component) { key: "renderEvents", value: function renderEvents() { var tag = function tag(row) { - var color = row.type === 'up' ? 'green' : 'red'; - var icon = row.type === 'up' ? 'arrow-up' : 'arrow-down'; + var color = row.state === 'up' ? 'green' : 'red'; + var icon = row.state === 'up' ? 'arrow-up' : 'arrow-down'; return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Tag"], { color: color }, react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(antd__WEBPACK_IMPORTED_MODULE_2__["Icon"], { @@ -323195,11 +323642,11 @@ function (_React$Component) { style: { marginRight: 5 } - }), row.type); + }), row.state); }; var reason = function reason(row) { - var color = row.type === 'up' ? GREEN : RED; + var color = row.state === 'up' ? _helpers__WEBPACK_IMPORTED_MODULE_4__["GREEN"] : _helpers__WEBPACK_IMPORTED_MODULE_4__["RED"]; return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("span", { style: { color: color @@ -323213,11 +323660,11 @@ function (_React$Component) { pagination: false, width: "100%", dataSource: this.state.events, - rowKey: "id", + rowKey: "date", columns: [{ key: 'event', title: 'Recent Events', - dataIndex: 'type', + dataIndex: 'state', render: function render(text, row) { return tag(row); } @@ -323267,18 +323714,24 @@ function (_React$Component) { /*!*********************************!*\ !*** ./resources/js/helpers.js ***! \*********************************/ -/*! exports provided: formatDateTime */ +/*! exports provided: formatDateTime, GREEN, YELLOW, RED */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "formatDateTime", function() { return formatDateTime; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "GREEN", function() { return GREEN; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "YELLOW", function() { return YELLOW; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RED", function() { return RED; }); /* harmony import */ var moment__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! moment */ "./node_modules/moment/moment.js"); /* harmony import */ var moment__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(moment__WEBPACK_IMPORTED_MODULE_0__); var formatDateTime = function formatDateTime(string) { return moment__WEBPACK_IMPORTED_MODULE_0___default()(string).utc().format('ddd Do MMM "YY @ H:mma'); }; +var GREEN = '#72c040'; +var YELLOW = '#efaf41'; +var RED = '#e23c39'; /***/ }), diff --git a/resources/js/components/CertificateReport.js b/resources/js/components/CertificateReport.js index a7bd5d0..8888fb2 100644 --- a/resources/js/components/CertificateReport.js +++ b/resources/js/components/CertificateReport.js @@ -1,15 +1,162 @@ import React from 'react'; +import { Card, Button, Spin, Progress, Icon, Tree, Tag } from 'antd'; +import {formatDateTime, GREEN, RED, YELLOW} from "../helpers"; + +const TreeNode = Tree.TreeNode; export default class CertificateReport extends React.Component { state = { - website: JSON.parse(this.props.website) + website: JSON.parse(this.props.website), + loading: true, + issuer: null, + domain: null, + additional_domains: [], + valid_from: null, + valid_to: null, + was_valid: false, + did_expire: true, + grade: 'N/A', + expires_in: 'N/A', + }; + + componentDidMount() { + this.update(); + }; + + update = async(refresh = false) => { + await this.setState({ + loading: true + }); + + let endpoint = window.location.href + '/ssl'; + + if (refresh) { + endpoint += '?refresh=1'; + } + + const response = (await window.axios.get(endpoint)).data; + + this.setState({ + ...response, + loading: false, + }); + }; + + forceUpdate = () => { + return this.update(true); + }; + + renderLeft = () => { + const color = grade => { + switch (grade) { + case 'A+': + return GREEN; + case 'A': + return GREEN; + case 'A-': + return GREEN; + case 'B': + return YELLOW; + case 'C': + return YELLOW; + case 'D': + return RED; + case 'E': + return RED; + case 'F': + return RED; + } + }; + + return ( + <> + { this.state.grade } } + /> + +
+
+
Issued By:
+ { this.state.issuer} +
+
+
Issued On:
+ { formatDateTime(this.state.valid_from) } +
+
+ + ) + }; + + renderRight = () => { + return ( + <> +

Primary Domain

+ { this.state.domain } + +

Additional Domains

+ { this.state.additional_domains.map(i => { i }) } + +
+
+
Expires At:
+ { formatDateTime(this.state.valid_to) } +
+
+
Valid Certificate:
+
{ this.state.was_valid ? 'Yes' : 'No' }
+
+
+
Expires In:
+ { this.state.expires_in } +
+
+
Has Expired:
+
{ this.state.did_expire ? 'Yes' : 'No' }
+
+
+ + ) + }; + + renderReport = () => { + return ( +
+
+ { this.renderLeft() } +
+
+ { this.renderRight() } +
+
+ ) + }; + + renderBusy = () => { + const { now, previous } = this.state; + + if (now && previous) { + return this.renderReport() + } + + return }; render() { + const { loading } = this.state; + return (
- xxxxxxxxxxxxxxxxx + Refresh } + > + { !loading ? this.renderReport() : this.renderBusy() } +
) } diff --git a/resources/js/components/DnsReport.js b/resources/js/components/DnsReport.js index 1b70daf..ffc1d4b 100644 --- a/resources/js/components/DnsReport.js +++ b/resources/js/components/DnsReport.js @@ -1,15 +1,85 @@ import React from 'react'; +import { Card, Button, Spin } from 'antd'; +import ReactDiffViewer from 'react-diff-viewer' +import { formatDateTime } from "../helpers"; export default class DnsReport extends React.Component { state = { - website: JSON.parse(this.props.website) + website: JSON.parse(this.props.website), + now: null, + previous: null, + loading: true, + }; + + componentDidMount() { + this.update(); + } + + update = async(refresh = false) => { + await this.setState({ + loading: true + }); + + let endpoint = window.location.href + '/dns'; + + if (refresh) { + endpoint += '?refresh=1'; + } + + const response = (await window.axios.get(endpoint)).data; + + this.setState({ + ...response, + loading: false, + }); + }; + + forceUpdate = () => { + return this.update(true); + }; + + renderReport = () => { + const { now, previous } = this.state; + + return ( + <> +
+

Before ({ previous && formatDateTime(previous.created_at) })

+

Now ({ now && formatDateTime(now.created_at) })

+
+ + + + ) + }; + + renderBusy = () => { + const { now, previous } = this.state; + + if (now && previous) { + return this.renderReport() + } + + return }; render() { + const { loading } = this.state; + return (
- xxxxxxxxxxxxxxxxx + Refresh } + > + { !loading ? this.renderReport() : this.renderBusy() } +
) } diff --git a/resources/js/components/RobotsReport.js b/resources/js/components/RobotsReport.js index 9e76eda..00eb0d6 100644 --- a/resources/js/components/RobotsReport.js +++ b/resources/js/components/RobotsReport.js @@ -14,7 +14,7 @@ export default class RobotsReport extends React.Component { componentDidMount() { this.update(); - } + }; update = async(refresh = false) => { await this.setState({ @@ -57,7 +57,7 @@ export default class RobotsReport extends React.Component { /> ) - } + }; renderBusy = () => { const { now, previous } = this.state; diff --git a/resources/js/components/UptimeReport.js b/resources/js/components/UptimeReport.js index 54b41ec..ab1cc7d 100644 --- a/resources/js/components/UptimeReport.js +++ b/resources/js/components/UptimeReport.js @@ -1,11 +1,7 @@ import React from 'react'; import { Card, Button, Spin, Progress, Icon, Table, Tag } from 'antd'; import { Line } from 'react-chartjs-2' -import { formatDateTime } from '../helpers'; - -const GREEN = '#72c040'; -const YELLOW = '#efaf41'; -const RED = '#e23c39'; +import { formatDateTime, GREEN, YELLOW, RED } from '../helpers'; export default class UptimeReport extends React.Component { @@ -201,17 +197,17 @@ export default class UptimeReport extends React.Component { renderEvents() { const tag = function (row) { - const color = row.type === 'up' ? 'green' : 'red'; - const icon = row.type === 'up' ? 'arrow-up' : 'arrow-down'; + const color = row.state === 'up' ? 'green' : 'red'; + const icon = row.state === 'up' ? 'arrow-up' : 'arrow-down'; return - { row.type } + { row.state } }; const reason = function (row) { - const color = row.type === 'up' ? GREEN : RED; + const color = row.state === 'up' ? GREEN : RED; return { row.reason } }; @@ -222,9 +218,9 @@ export default class UptimeReport extends React.Component { pagination={ false } width="100%" dataSource={ this.state.events } - rowKey="id" + rowKey="date" columns={[ - { key: 'event', title: 'Recent Events', dataIndex: 'type', render: (text, row) => tag(row)}, + { key: 'event', title: 'Recent Events', dataIndex: 'state', render: (text, row) => tag(row)}, { key: 'date', title: 'Time', dataIndex: 'date', render: text => formatDateTime(text) }, { key: 'reason', title: 'Reason', dataIndex: 'reason', render: (text, row) => reason(row)}, { key: 'duration', title: 'Duration', dataIndex: 'duration' }, diff --git a/resources/js/helpers.js b/resources/js/helpers.js index ec16aea..962a0e7 100644 --- a/resources/js/helpers.js +++ b/resources/js/helpers.js @@ -3,3 +3,7 @@ import moment from 'moment'; export const formatDateTime = string => { return moment(string).utc().format('ddd Do MMM "YY @ H:mma') }; + +export const GREEN = '#72c040'; +export const YELLOW = '#efaf41'; +export const RED = '#e23c39'; diff --git a/resources/views/websites-form.blade.php b/resources/views/websites-form.blade.php index abdc67a..11883c1 100644 --- a/resources/views/websites-form.blade.php +++ b/resources/views/websites-form.blade.php @@ -16,17 +16,17 @@
- @include('maelstrom::inputs.switch', [ - 'name' => 'ssl_enabled', - 'label' => 'Enable SSL Monitoring?' - ]) - @include('maelstrom::inputs.switch', [ 'name' => 'uptime_enabled', 'label' => 'Enable Up-Time Monitoring?', 'hide_off' => ['uptime_keyword'], ]) + @include('maelstrom::inputs.switch', [ + 'name' => 'ssl_enabled', + 'label' => 'Enable SSL Monitoring?' + ]) + @include('maelstrom::inputs.switch', [ 'name' => 'robots_enabled', 'label' => 'Enable Robots.txt Monitoring?' @@ -43,7 +43,7 @@ 'label' => 'Uptime Keyword', 'help' => 'This word *must* exist on the web page to confirm the site is online.', 'prefix' => '🔑', - //'required' => true, + 'required' => true, ]) @endcomponent diff --git a/resources/views/websites-show.blade.php b/resources/views/websites-show.blade.php index 6726664..3066db8 100644 --- a/resources/views/websites-show.blade.php +++ b/resources/views/websites-show.blade.php @@ -16,17 +16,21 @@ @section('content') - @if ($website->robots_enabled) -
- @endif - @if ($website->uptime_enabled)
@endif - + @if ($website->ssl_enabled)
+ @endif + + @if ($website->robots_enabled) +
+ @endif + + @if ($website->dns_enabled)
+ @endif @endsection diff --git a/routes/web.php b/routes/web.php index bce93e5..b848685 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,4 +21,6 @@ Route::resource('websites', 'WebsiteController'); Route::get('websites/{website}/robots', 'RobotCompareController'); Route::get('websites/{website}/uptime', 'UptimeReportController'); + Route::get('websites/{website}/ssl', 'CertificateReportController'); + Route::get('websites/{website}/dns', 'DnsCompareController'); });