Skip to content

Commit

Permalink
Merge pull request #14 from tonysm/audit-outdated-cmds
Browse files Browse the repository at this point in the history
Adds audit and outdated commands and update the es-module-shims
  • Loading branch information
tonysm authored May 30, 2022
2 parents c27e0c0 + 72a7733 commit 630f5b9
Show file tree
Hide file tree
Showing 16 changed files with 479 additions and 12 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
php: [8.0, 8.1]
laravel: [8.*]
php: [8.1]
laravel: [9.12.*]
stability: [prefer-lowest, prefer-stable]
include:
- laravel: 8.*
testbench: ^6.24
- laravel: 9.12.*
testbench: ^7.0

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,30 @@ Which will add the correct `links` tags to your head tag in the HTML document, l
<link rel="modulepreload" href="https://unpkg.com/[email protected]/dist/module.esm.js">
```

## Dependency Maintenance Commands

Maintaining a healthy dependency list can be tricky. Here's a couple of commands to help you with this task.

### Outdated Dependencies

To keep your dependencies up-to-date, make sure you run the `importmap:outdated` command from time to time:

```bash
php artisan importmap:outdated
```

This command will scan your `config/importmap.php` file, find your current versions, then use the NPM registry API to look for the latest version of the packages you're using. It also handles locally served vendor libs that you added using the `--download` flag from the `importmap:pin` command.

### Auditing Dependencies

If you want to a security audit on your dependecies to see if you're using a version that's been breached, run the `importmap:audit` command from time to time. Better yet, add that command to your CI build:

```bash
php artisan importmap:audit
```

This will also scan your `config/importmap.php` file, find your current versions, then use the NPM registry API to look for vulnerabilities on your packages. It also handles locally serverd vendor libs that you added using the `--download` flag from the `importmap:pin` command.

## Known Problems

### Browser Console Errors
Expand Down
12 changes: 7 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
}
],
"require": {
"php": "^8.0|^8.1",
"spatie/laravel-package-tools": "^1.9.2",
"illuminate/contracts": "^8.73|^9.0"
"php": "^8.1",
"illuminate/contracts": "^9.0",
"illuminate/support": "^9.0",
"spatie/laravel-package-tools": "^1.9.2"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"nunomaduro/collision": "^5.10|^6.0",
"nunomaduro/larastan": "^1.0",
"orchestra/testbench": "^6.24",
"orchestra/testbench": "^7.0",
"pestphp/pest": "^1.21",
"pestphp/pest-plugin-laravel": "^1.2",
"phpstan/extension-installer": "^1.1",
Expand All @@ -45,7 +47,7 @@
"scripts": {
"analyse": "vendor/bin/phpstan analyse",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest coverage"
"test-coverage": "vendor/bin/pest --coverage"
},
"config": {
"sort-packages": true,
Expand Down
2 changes: 1 addition & 1 deletion resources/views/tags.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@if (config('importmap.use_shim'))
@if ($nonce) <script type="esms-options" nonce="{{ $nonce }}">{"nonce":"{{ $nonce }}"}</script> @endif
<script async src="https://ga.jspm.io/npm:es-module-shims@1.3.6/dist/es-module-shims.js" data-turbo-track="reload"@if ($nonce) nonce="{{ $nonce }}"@endif></script>
<script async src="https://ga.jspm.io/npm:es-module-shims@1.5.5/dist/es-module-shims.js" data-turbo-track="reload"@if ($nonce) nonce="{{ $nonce }}"@endif></script>
@endif

<script type="module" data-turbo-track="reload"@if ($nonce) nonce="{{ $nonce }}" @endif>import '{{ $entrypoint }}';</script>
51 changes: 51 additions & 0 deletions src/Commands/AuditCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Tonysm\ImportmapLaravel\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Tonysm\ImportmapLaravel\Npm;
use Tonysm\ImportmapLaravel\VulnerablePackage;

class AuditCommand extends Command
{
public $signature = 'importmap:audit';

public $description = 'Run a security audit.';

public function handle(Npm $npm): int
{
$vulnerablePackages = $npm->vulnerablePackages();

if ($vulnerablePackages->isEmpty()) {
$this->info("No vulnerable packages found.");

return self::SUCCESS;
}

$this->table(
['Package', 'Severity', 'Vulnerable Versions', 'Vulnerability'],
$vulnerablePackages
->map(fn (VulnerablePackage $package) => [$package->name, $package->severity, $package->vulnerableVersions, $package->vulnerability])
->all()
);

$this->newLine();

$summary = $vulnerablePackages
->groupBy('severity')
->map(fn ($vulns) => $vulns->count())
->sortDesc()
->map(fn ($count, $severity) => "$count {$severity}")
->join(", ");

$this->error(sprintf(
"%d %s found: %s",
$vulnerablePackages->count(),
Str::plural('vulnerability', $vulnerablePackages->count()),
$summary,
));

return self::FAILURE;
}
}
43 changes: 43 additions & 0 deletions src/Commands/OutdatedCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Tonysm\ImportmapLaravel\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Tonysm\ImportmapLaravel\Npm;
use Tonysm\ImportmapLaravel\OutdatedPackage;

class OutdatedCommand extends Command
{
public $signature = 'importmap:outdated';

public $description = 'Checks for outdated packages.';

public function handle(Npm $npm): int
{
$outdatedPackages = $npm->outdatedPackages();

if ($outdatedPackages->isEmpty()) {
$this->info("No outdated packages found.");

return self::SUCCESS;
}

$this->table(
['Package', 'Current', 'Latest'],
$outdatedPackages
->map(fn (OutdatedPackage $package) => [$package->name, $package->currentVersion, $package->latestVersion ?: $package->error])
->all(),
);

$this->newLine();

$this->error(sprintf(
'%d outdated %s found.',
$outdatedPackages->count(),
Str::plural('package', $outdatedPackages->count()),
));

return self::FAILURE;
}
}
2 changes: 2 additions & 0 deletions src/ImportmapLaravelServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public function configurePackage(Package $package): void
->hasCommand(Commands\JsonCommand::class)
->hasCommand(Commands\PinCommand::class)
->hasCommand(Commands\UnpinCommand::class)
->hasCommand(Commands\OutdatedCommand::class)
->hasCommand(Commands\AuditCommand::class)
;
}

Expand Down
156 changes: 156 additions & 0 deletions src/Npm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

namespace Tonysm\ImportmapLaravel;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;

class Npm
{
private string $baseUrl = "https://registry.npmjs.org";

public function __construct(private ?string $configPath = null)
{
$this->configPath ??= base_path("routes/importmap.php");
}

public function outdatedPackages(): Collection
{
return $this->packagesWithVersion()
->reduce(function (Collection $outdatedPackages, PackageVersion $package) {
$latestVersion = null;
$error = null;

if (! ($response = $this->getPackage($package))) {
$error = "Response error";
} elseif ($response["error"] ?? false) {
$error = $response["error"];
} else {
$latestVersion = $this->findLatestVersion($response);

if (! $this->outdated($package->version, $latestVersion)) {
return $outdatedPackages;
}
}

return $outdatedPackages->add(new OutdatedPackage(
name: $package->name,
currentVersion: $package->version,
latestVersion: $latestVersion,
error: $error,
));
}, collect());
}

public function vulnerablePackages(): Collection
{
$data = $this->packagesWithVersion()
->mapWithKeys(fn (PackageVersion $package) => [
$package->name => [$package->version],
])
->all();

return $this->getAudit($data)
->collect()
->flatMap(function (array $vulnerabilities, string $package) {
return collect($vulnerabilities)
->map(fn (array $vulnerability) => new VulnerablePackage(
name: $package,
severity: $vulnerability['severity'],
vulnerableVersions: $vulnerability['vulnerable_versions'],
vulnerability: $vulnerability['title'],
));
})
->sortBy([
['name', 'asc'],
['severity', 'asc'],
])
->values();
}

private function packagesWithVersion(): Collection
{
$content = File::get($this->configPath);

return $this->findPackagesFromCdnMatches($content)
->merge($this->findPackagesFromLocalMatches($content))
->unique('name')
->values();
}

private function findPackagesFromCdnMatches(string $content)
{
preg_match_all('/^Importmap\:\:pin\(.*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*\)\;\r?$/m', $content, $matches);

if (count($matches) !== 3) {
return collect();
}

return collect($matches[1])
->zip($matches[2])
->map(fn ($items) => new PackageVersion(name: $items[0], version: $items[1]))
->values();
}

private function findPackagesFromLocalMatches(string $content)
{
preg_match_all('/^Importmap::pin\("([^"]*)".*\)\; \/\/.*@(\d+\.\d+\.\d+(?:[^\s]*)).*\r?$/m', $content, $matches);

if (count($matches) !== 3) {
return collect();
}

return collect($matches[1])
->zip($matches[2])
->map(fn ($items) => new PackageVersion(name: $items[0], version: $items[1]))
->values();
}

private function getPackage(PackageVersion $package)
{
$response = Http::get($this->baseUrl . "/" . $package->name);

if (! $response->ok()) {
return null;
}

return $response->json();
}

private function findLatestVersion(array $json)
{
$latestVersion = data_get($json, "dist-tags.latest");

if ($latestVersion) {
return $latestVersion;
}

if (! isset($json["versions"])) {
return;
}

return collect($json["versions"])
->keys()
->sort(fn ($versionA, $versionB) => version_compare($versionB, $versionA))
->values()
->first();
}

private function outdated(string $currentVersion, string $latestVersion)
{
return version_compare($currentVersion, $latestVersion) === -1;
}

private function getAudit(array $packages)
{
$response = Http::asJson()
->post($this->baseUrl . "/-/npm/v1/security/advisories/bulk", $packages);

if (! $response->ok()) {
return collect();
}

return $response->collect();
}
}
15 changes: 15 additions & 0 deletions src/OutdatedPackage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Tonysm\ImportmapLaravel;

class OutdatedPackage
{
public function __construct(
public string $name,
public string $currentVersion,
public ?string $latestVersion = null,
public ?string $error = null,
) {
//
}
}
12 changes: 12 additions & 0 deletions src/PackageVersion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Tonysm\ImportmapLaravel;

class PackageVersion
{
public function __construct(
public string $name,
public string $version,
) {
}
}
14 changes: 14 additions & 0 deletions src/VulnerablePackage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Tonysm\ImportmapLaravel;

class VulnerablePackage
{
public function __construct(
public string $name,
public string $severity,
public string $vulnerableVersions,
public string $vulnerability,
) {
}
}
Loading

0 comments on commit 630f5b9

Please sign in to comment.