Abdulkader Safi

An audit said 92 out of 100, and the summary was wrong about why

A package audit tool scored Atelier at 92. The web page listed three failures and a warning about files in the release archive. The JSON API told a different story: one check failed, worth three points, and the archive was clean. Fixing it took one file, and adding CI afterwards was the part that could have cost points.

4 min read

Share
A package audit report showing category scores for security, maintenance and ecosystem, beside a CI workflow with SHA-pinned actions.

v0.1.3 has no package code in it. It is a security policy, CI, and a dependency updater, and it exists because an audit tool gave me a number and I went looking for the reasoning behind it.

The tool was Plumb, which scores PHP packages on security, maintenance and ecosystem signals. Atelier came out at 92 out of 100.

The rendered summary was misleading

Read as a web page, the report said three security checks failed and flagged "4 items" as development files shipped inside the release archive.

That would have been a morning of work. It was also wrong.

The same tool has a JSON API, and it disagreed with its own rendering:

Check Weight Status
Open security advisories 10 pass
Dependabot PR responsiveness 7 pass
Security policy present 3 fail
Actions pinned to SHA 8 not applicable
Dependabot or Renovate configured 5 not applicable
Dependency update cooldown 4 not applicable

One failure. Seventeen of twenty applicable weight, which is exactly the 85 the security category showed.

The "4 items" in the archive was not four files. It was the four category names the check looks in, and the evidence field read {"ai": [], "ci": [], "tests": [], "tooling": []}. All empty. The check passed. I confirmed it independently by downloading the actual release zip and listing it, which is the sort of thing worth doing before believing either the tool or myself.

So the entire gap was one missing file.

The file was worth writing properly

SECURITY.md could be four lines. It is more useful as a scope document, because the interesting question for this package is what counts as a vulnerability at all.

In scope: template injection through block content, draft content reaching an unauthenticated visitor, a preview link that resolves without a valid signature, slug handling that serves a page the request did not ask for.

Out of scope, stated explicitly: the raw HTML block executing the HTML somebody deliberately typed into it. That is the feature. Without saying so, every security researcher who finds it reports it, and every report costs a reply.

I also turned on GitHub's private vulnerability reporting so the link in the policy resolves to something rather than being a gesture.

Adding CI was the part that could have gone backwards

Three of those checks were "not applicable" for one reason: there was no .github/workflows directory. Between them they carry 17 weight, dormant.

The moment you add a single workflow, all three activate. Every third-party action then needs a full 40-character commit SHA, a dependency updater has to cover the github-actions ecosystem, and that updater needs a cooldown configured. Done carelessly, adding CI would have dropped the score from 100 to around 75.

That is an argument for adding CI deliberately, not for avoiding it. The suite had 29 tests that only ran when I remembered to run them.

Actions are pinned like this, with the version in a comment because a SHA tells a reader nothing:

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

The reason is not theoretical. In March 2025 a widely used action had its tag re-pointed at malicious code and CI secrets were exfiltrated from thousands of repositories. A tag is a mutable pointer somebody else controls.

Dependabot covers the workflows on a seven day cooldown, because merging an update the moment it is published is precisely the window a malicious release depends on.

The floor I was claiming and not testing

The suite runs on PHP 8.4. It cannot run on 8.3, because Pest 5 requires 8.4.

composer.json claims ^8.3. Dropping 8.3 from the matrix silently would have left a public promise that nothing verified.

So there is a separate job that checks the two halves that promise is actually made of: that the package's own code parses under 8.3, and that a consumer on 8.3 can resolve and install it. Both pass. The claim is honest, and now it is checked rather than asserted.

The bug the dry run caught

I ran the workflow's commands in a clean clone before pushing. php artisan test from the repository root failed with Could not open input file: vendor/pestphp/pest/bin/pest, because artisan test spawns Pest at a path relative to the current directory.

Ten seconds to fix, and it would otherwise have been a red first build that looks like the feature is broken rather than the pipeline.

Where it stands

v0.1.3. A security policy, CI on every push, a dependency updater, and a changelog covering every release back to the first.

The composite score goes to 100 once the tool rescans, which it does daily. Which matters less than the two things that came out of it: the tests now run whether or not I remember, and there is a documented place to report something that should not be a public issue.

Last updated 18 Aug 2026 · filed under Laravel, filamentphp, plugin, tools, web application, website audit

Every entry on this project

28 build notes, in order

Including the ones where nothing worked. You are on part 7.

Read the series →

FAQ

Frequently asked questions

Why pin GitHub Actions to a commit SHA instead of a version tag?

Because a tag is a mutable pointer controlled by whoever owns the action's repository, and it can be moved to different code at any time without your workflow changing. In March 2025 a widely used action had its tag re-pointed at malicious code that exfiltrated CI secrets from thousands of repositories, which is the concrete version of this risk rather than a hypothetical one. A full 40-character commit SHA names an immutable object, so the code that ran yesterday is the code that runs today. Keep the version in a trailing comment, because a SHA alone tells a reader nothing about how current the pin is.

Should you drop a PHP version from CI when a dev dependency will not install on it?

You can drop it from the test matrix, but not silently, because the composer constraint is a public promise that somebody will install against. The gap is that a test toolchain has different requirements from the package itself: a test runner needing a newer PHP says nothing about whether the library runs on an older one. The honest approach is to check the two things that promise is actually made of, that the package's own source parses on the floor version, and that a consumer on the floor version can resolve and install it. Both are cheap, and together they verify the constraint without needing the suite to run there.

What belongs in a security policy for a CMS or page builder package?

Scope, more than contact details. The useful part is stating what counts as a vulnerability in a system where authenticated users are supposed to publish arbitrary content. Worth listing in scope: template injection through user content, unpublished or draft content reaching anonymous visitors, signed preview links resolving without a valid signature, and routing that serves different content than the URL requested. Worth excluding explicitly: a raw HTML block executing HTML that an authorised editor deliberately typed, since that is the documented feature. Without that line every researcher who finds it files a report, and each one costs a reply.

Why does adding CI to a repository sometimes lower an audit score?

Because several supply chain checks are conditional on having workflows at all. With no workflow directory, checks for pinning actions to commit SHAs, for having a dependency updater that covers the actions ecosystem, and for that updater enforcing a cooldown are all reported as not applicable and carry no weight. Adding one workflow activates every one of them at once. Done properly the score is unchanged, done carelessly it drops noticeably. It is a reason to add CI deliberately rather than a reason to avoid it, since the alternative is a test suite that only runs when somebody remembers.

Written by

Abdulkader Safi

Software Engineer

Lead engineer at DSRPT, from Lebanon and based in Kuwait. I write about the tools and bugs from real client work, with the numbers I measured.

About me → GitHub LinkedIn

Need this kind of work done on your project?

Start a project →

Keep reading

More from Filament Atelier

All 28 entries →
  • A version number changing from 0.5.0 to 1.0.0 beside a list of deferred items.
    Filament Atelier

    · part 28 of 28

    Tagging 1.0.0 with four features missing, on purpose

    The gate list had thirteen items. Six went in, seven did not, and the tag went out anyway. What the number promises is that the API stops moving, not that the feature list is finished, and conflating those two is how packages sit at 0.x for three years.

  • The same block of JavaScript appearing in three different Blade layout files.
    Filament Atelier

    · part 26 of 28

    A script in three layouts is a contract nobody signed

    The editor's preview needed a few lines of JavaScript in the page it renders. I put them in the shipped layout, then copied them into two more. Anyone writing their own layout had to copy them too, and missing them broke half the editor with no error at all.