ci: Drop exports-subpath stubs from image SBOMs (no-changelog) (#37488)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Declan Carroll 2026-09-01 08:59:15 +00:00 committed by GitHub
parent f86e11f1dc
commit f7b507ca65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 79 additions and 4 deletions

View File

@ -706,8 +706,11 @@ jobs:
notify-on-failure:
name: Notify Cats on nightly build failure
runs-on: ubuntu-latest
needs: [build-and-push-docker]
if: always() && needs.build-and-push-docker.result == 'failure' && github.event_name == 'schedule'
needs: [build-and-push-docker, sbom-attestation]
if: |
always() && github.event_name == 'schedule' &&
(needs.build-and-push-docker.result == 'failure' ||
needs.sbom-attestation.result == 'failure')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@ -719,4 +722,4 @@ jobs:
run: |
node .github/scripts/slack/notify.mjs \
--channel '#team-catalysts' \
--text 'Nightly Docker build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
--text 'Nightly Docker build failed (build=${{ needs.build-and-push-docker.result }}, sbom=${{ needs.sbom-attestation.result }}) - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'

View File

@ -75,6 +75,16 @@ function srcFileOf(component) {
return null;
}
/**
* An npm name is `name` or `@scope/name` — neither part may contain a slash. A
* further slash means the scanner named a directory inside a package, i.e. an
* `exports` subpath (@google/genai/node, @linear/sdk/webhooks).
*/
function isExportsSubpath(component) {
const qualified = qualifiedName(component) ?? '';
return qualified.replace(/^@[^/]+\//, '').includes('/');
}
/**
* A scan of a container image filesystem (unlike the pnpm-lockfile scan of the
* release closure) deep-walks node_modules and mis-catalogs nested package.json
@ -91,6 +101,11 @@ export function isPhantomNpm(component) {
const purl = component.purl ?? '';
if (!purl.startsWith('pkg:npm/')) return false;
// Checked before the path rules: a subpath stub lives at
// node_modules/@google/genai/node/package.json, which *does* end with its own
// qualified name, so the canonical-path check below would call it real.
if (isExportsSubpath(component)) return true;
const src = srcFileOf(component);
// syft writes "UNKNOWN" where cdxgen omits the field. Neither is a real
// version. cdxgen emits `exports`-subpath phantoms with no path either, so

View File

@ -204,6 +204,42 @@ describe('isPhantomNpm (cdxgen image-scan noise)', () => {
const syftSrc = (p) => ({ properties: [{ name: 'syft:location:0:path', value: p }] });
it('flags an exports subpath whose syft path matches its own qualified name', () => {
assert.equal(
isPhantomNpm({
name: 'genai/node',
group: '@google',
version: 'UNKNOWN',
purl: 'pkg:npm/%40google/genai%2Fnode',
...syftSrc('/usr/local/lib/node_modules/n8n/node_modules/@google/genai/node/package.json'),
}),
true,
);
assert.equal(
isPhantomNpm({
name: 'sdk/webhooks',
group: '@linear',
version: 'UNKNOWN',
purl: 'pkg:npm/%40linear/sdk%2Fwebhooks',
...syftSrc('/usr/local/lib/node_modules/n8n/node_modules/@linear/sdk/webhooks/package.json'),
}),
true,
);
});
it('keeps an unscoped package whose name is a prefix of a subpath', () => {
assert.equal(
isPhantomNpm({
name: 'genai',
group: '@google',
version: '1.19.0',
purl: 'pkg:npm/%40google/genai@1.19.0',
...syftSrc('/usr/local/lib/node_modules/n8n/node_modules/@google/genai/package.json'),
}),
false,
);
});
it('keeps an application root outside node_modules (runners ship the task runner there)', () => {
assert.equal(
isPhantomNpm({

View File

@ -238,7 +238,7 @@ describe('isPhantomNpm — properties', () => {
}
});
it('CANONICAL-PATH-WITH-VERSION-NEVER-PHANTOM', () => {
it('CANONICAL-PATH-WITH-VERSION-NEVER-PHANTOM (for a well-formed npm name)', () => {
const node = (group, name) => ({
group,
name,
@ -255,6 +255,27 @@ describe('isPhantomNpm — properties', () => {
assert.equal(isPhantomNpm(node('@n8n', 'db')), false);
});
it('SUBPATH-NAME-ALWAYS-PHANTOM: a slash past the scope is never a real npm name', () => {
const atCanonicalPath = (group, name) => ({
group,
name,
version: '1.0.0',
purl: `pkg:npm/${group ? group + '/' : ''}${name}@1.0.0`,
properties: [
{
name: 'syft:location:0:path',
value: `/x/node_modules/${group ? group + '/' : ''}${name}/package.json`,
},
],
});
// Canonical path AND a resolved version — every other signal says "real".
assert.equal(isPhantomNpm(atCanonicalPath('@google', 'genai/node')), true);
assert.equal(isPhantomNpm(atCanonicalPath('@linear', 'sdk/webhooks')), true);
assert.equal(isPhantomNpm(atCanonicalPath('', 'foo/bar')), true);
// The scope slash alone must not trip it.
assert.equal(isPhantomNpm(atCanonicalPath('@google', 'genai')), false);
});
it('NON-NPM-NEVER-PHANTOM: OS and other ecosystems are out of scope', () => {
for (const purl of ['pkg:apk/alpine/busybox@1', 'pkg:deb/debian/libc@2', 'pkg:cargo/x@1']) {
assert.equal(isPhantomNpm({ purl, name: 'x' }), false, purl);