Monitor and alert about composer and npm package security vulnerabilities

This might be one of those itches that affects one person and one person alone, but I scratched it and now I’m sharing the result just in case someone else finds it helpful.

There are some great tools out there already to tell you if you are running outdated and vulnerable composer dependencies in your Laravel app. GitHub’s Dependabot is a common one. Freek Van der Herten’s “How to monitor your Laravel app for critical vulnerabilities” and the related “Monitor your Laravel app for vulnerabilities with Oh Dear” is another great example of a ready-to-go solution for this.

I wanted a system that would work independent of where my code repos are hosted, that didn’t assume a fully realized Laravel app, and that integrated in to my use of Nagios for infrastructure monitoring. I wanted something that would be able to take a list of root directories of any kind of application using composer and/or npm packages on a given server, and alert me as if there are known security vulnerabilities to handle.

(For some reason, thinking about this as a server-level issue makes sense to me. If one package on one app on a server is vulnerable, the whole server is vulnerable to compromise.)

Thus was born package-vulnerability-audit, which combines a PHP scanning tool that runs the “audit” functions of both composer and npm on each application and outputs a JSON file with the results, and a Nagios NRPE script that checks the JSON file and returns an OK/WARNING/CRITICAL result to Nagios.

Once it’s set up, I can control the way I get alerted about these issues, and in theory give them a bit more urgency than an email notification.

The early reality is that I have some package updates to do, and I probably should have just spent time on that instead of building (with help from Claude) another tool. ๐Ÿ˜…

Emoji form field creation and validation in Filament and Laravel

If you need a form field for picking an emoji to save as a resource/model attribute in Filament and Laravel, here’s how I did it:

First, install a few packages:

$ composer require tangodev-it/filament-emoji-picker
$ composer require steppinghat/emoji-detector

Set up your form field:

TextInput::make('emoji')
	->label('Emoji')
	->placeholder('๐ŸŽ‰')
	->suffixAction(EmojiPickerAction::make('emoji-title'))
	->rules([
		'nullable',
		fn (): Closure => static function (string $attribute, mixed $value, Closure $fail): void {
			$value = is_string($value) ? trim($value) : $value;

			if ($value === null || $value === '') {
				return;
			}

			$detector = new EmojiDetector;

			if (! $detector->isSingleEmoji((string) $value)) {
				$fail('Please enter exactly one emoji.');
			}
		},
	]),

And then profit! That’s pretty much it.

Things I tried that didn’t work:

  • Setting the max length of the field to 1; as Unicode entities that might span multiple typical characters, this won’t work.
  • Validating the emoji using regular expressions; the Unicode and emoji standards apparently evolve often enough that this will drive you insane.

Thank you to Javan Eskander for php-emoji-detector and Emanuele Gandolfi for filament-emoji-picker.

WooCommerce Subscriptions API start_date bug workaround

The WooCommerce Subscriptions plugin has a bug where if you use its REST API endpoints to make updates to a subscription, it will almost always reset the start date of the subscription you are updating to the current date and time.

I reported the bug to the WooCommerce folks in November 2022 and I believe it has a GitHub issue filed. When I checked in about it recently I was told it’s a low priority to fix. I consider it somewhat serious for my purposes โ€” unexpectedly overwriting/losing key data about a record that’s used in calculating user-facing financial transactions โ€”  so I’m documenting it here for others who might encounter it, along with a possible workaround.

The bug is in the plugin file includes/api/class-wc-rest-subscriptions-controller.php that processes the incoming API request. In particular, in the function prepare_object_for_database() there’s this code:

// If the start date is not set in the request, set its default to now.
if ( ! isset( $request['start_date'] ) ) {
	$request['start_date'] = gmdate( 'Y-m-d H:i:s' );
}

All of the valid dates contained in the $request array are subsequently copied into an array called $dates. Later in the same function, there’s this code:

if ( ! empty( $dates ) ) {
	...
	try {
		$subscription->update_dates( $dates );
	} catch ( Exception $e ) {
		...
	}
}

The implication is that for completely unrelated API requests to change something like, say, a meta field value or the subscription’s status, the date validation and update logic will be run. And because the start date value is overridden to be the current date, it means that any API request to update a completely unrelated field is going to unintentionally reset the start date of the subscription.

Nothing about the documentation at https://woocommerce.github.io/subscriptions-rest-api-docs/?php#update-a-subscription indicates that a start_date value is required in the API requests. In fact, the example given in those docs where the status of a subscription is updated would trigger this bug.

I’ve even noticed this bug surfacing even in regular wp-admin operations involving WooCommerce Subscriptions, as I think some of the logic used to do something like put a subscription on hold or cancel it from within the admin interface is calling the same internal functions.

My workaround for this bug, at least on the API client side, introduces an extra API request, and so is less than ideal for any kind of production or long-term use.

For every API request I make to the Subscriptions API update endpoint, if I’m not explicitly setting/changing the start_date field in my request, I first fetch the existing subscription record and then set the start_date field in my request to the current value.

if ('subscription' === $recordType && empty($updateValues['start_date'])) {
      $current_values = $wooApiFacade::find($recordId);
      $updateValues['start_date'] = Carbon::parse($current_values['start_date_gmt'])->format('Y-m-d\ H:i:s');
}

Hopefully they’ll fix this issue sooner rather than later so that users of the plugin don’t unexpectedly see subscription start dates overwritten.

CrowdTangle API SDK in PHP

After not finding anyone else who has done so, I created a minimal PHP implementation of the CrowdTangle API, which I needed anyway for a project I’m working on:

Example usage syntax:

$client = new ChrisHardie\CrowdtangleApi\Client($accessToken);

// get lists
$client->getLists();

// get accounts in a list
$client->getAccountsForList($listId);

// get posts
$client->getPosts([
    'accounts' => '12345678',
    'startDate' => '2022-03-01',
]);

// get a single post
$client->getPost($postId);

I’m sure there’s plenty to improve but I hope it’s helpful to anyone working with CrowdTangle in PHP.

Creating a personalized, private RSS feed for users in Laravel

In building WP Lookout I had the need to create user-specific, private RSS feeds as a way for users to get updates and information specific to their account. Here’s how I did it.

I started with the laravel-feed package from Spatie, which allows you to easily generate RSS feeds, either from a model’s records or from some other method that you define. I found that someone has proposed a feature and submitted a PR to add support for signed routes, which is Laravel’s way of adding a hash key to a given URL so that it can’t be accessed if it’s been modified, perfect for something like a user-specific RSS feed. But the Spatie folks decided not to include the feature, so I had to figure out how to do it on my own.

First, I added the package to my project and published the related config file:

$ composer require spatie/laravel-feed
$ php artisan vendor:publish \
    --provider="Spatie\Feed\FeedServiceProvider" \
    --tag="config"

Then, I added a user-specific feed identifier to the User model:

$ php artisan make:migration add_feed_id_to_users_table --table=users

The migration looked like this in part:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        //
        $table->string('feed_id');
    });

    DB::statement('UPDATE users SET feed_id = ...');
}

The UPDATE statement there is used to set the initial value of the feed identifier for existing users. An alternative would have been to make the field nullable and then set it elsewhere. For new users, I set the feed identifier as users are created, via the boot() method in the User model:

Continue reading Creating a personalized, private RSS feed for users in Laravel