mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
4dbac421ab
* feat(advisories): add provisional ghsa feed * fix(workflows): include advisory signatures in checksums * fix(workflows): mirror ghsa feed at release root * feat(advisories): consolidate ghsa into agent feed * ci(advisories): consolidate ghsa during nvd poll * fix(advisories): retain unreplaced ghsa feed entries * chore(skills): bump advisory feed consumers * fix(release): resolve ts import closure dry run * fix(release): preserve urls while stripping comments * fix(release): ignore skill test-only changes * fix(advisories): follow ghsa pagination links * test(advisories): add nvd ghsa pipeline dry run
426 lines
12 KiB
JavaScript
426 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
buildConsolidatedAdvisoryFeed,
|
|
buildGhsaWithoutCveFeed,
|
|
fetchGitHubSecurityAdvisories,
|
|
inferPlatforms,
|
|
normalizeGhsaAdvisory,
|
|
} from './ghsa-without-cve-feed.mjs';
|
|
|
|
const fixedNow = '2026-05-24T00:00:00Z';
|
|
|
|
function advisory(overrides = {}) {
|
|
return {
|
|
ghsa_id: 'GHSA-test-1111-2222',
|
|
cve_id: null,
|
|
html_url: 'https://github.com/openclaw/openclaw/security/advisories/GHSA-test-1111-2222',
|
|
summary: 'Workspace bridge allows sandbox escape',
|
|
description: 'OpenClaw before 2026.4.25 allowed a sandbox escape.',
|
|
severity: 'high',
|
|
published_at: '2026-04-24T00:00:00Z',
|
|
updated_at: '2026-04-25T00:00:00Z',
|
|
vulnerabilities: [
|
|
{
|
|
package: { ecosystem: 'npm', name: 'openclaw' },
|
|
vulnerable_version_range: '<2026.4.25',
|
|
patched_versions: '2026.4.25',
|
|
},
|
|
],
|
|
cvss: {
|
|
vector_string: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H',
|
|
score: 7.8,
|
|
},
|
|
cwe_ids: ['CWE-94'],
|
|
credits: [{ login: 'researcher', type: 'reporter' }],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('inferPlatforms maps known repositories to feed platforms', () => {
|
|
assert.deepEqual(inferPlatforms('openclaw/openclaw'), ['openclaw']);
|
|
assert.deepEqual(inferPlatforms('qwibitai/nanoclaw'), ['nanoclaw']);
|
|
assert.deepEqual(inferPlatforms('softwarepub/hermes'), ['hermes']);
|
|
assert.deepEqual(inferPlatforms('sipeed/picoclaw'), ['picoclaw']);
|
|
});
|
|
|
|
test('fetchGitHubSecurityAdvisories follows cursor pagination links', async (t) => {
|
|
const originalFetch = globalThis.fetch;
|
|
const nextUrl =
|
|
'https://api.github.com/repositories/1103012935/security-advisories?per_page=100&after=cursor';
|
|
const calls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
calls.push(String(url));
|
|
if (calls.length === 1) {
|
|
return new globalThis.Response(
|
|
JSON.stringify(
|
|
Array.from({ length: 100 }, (_, index) =>
|
|
advisory({ ghsa_id: `GHSA-page-1111-${String(index).padStart(4, '0')}` }),
|
|
),
|
|
),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
Link: `<${nextUrl}>; rel="next"`,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
if (String(url) !== nextUrl) {
|
|
throw new Error(`unexpected pagination URL: ${url}`);
|
|
}
|
|
return new globalThis.Response(JSON.stringify([advisory({ ghsa_id: 'GHSA-next-1111-2222' })]), {
|
|
status: 200,
|
|
});
|
|
};
|
|
t.after(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
const advisories = await fetchGitHubSecurityAdvisories('openclaw/openclaw', {
|
|
token: 'test-token',
|
|
});
|
|
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(calls[1], nextUrl);
|
|
assert.equal(advisories.length, 101);
|
|
assert.equal(advisories.at(-1).ghsa_id, 'GHSA-next-1111-2222');
|
|
});
|
|
|
|
test('normalizeGhsaAdvisory marks fresh GHSA-only advisories active', () => {
|
|
const normalized = normalizeGhsaAdvisory(advisory(), {
|
|
now: fixedNow,
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
});
|
|
|
|
assert.equal(normalized.id, 'GHSA-test-1111-2222');
|
|
assert.equal(normalized.status, 'active');
|
|
assert.equal(normalized.cve_id, null);
|
|
assert.equal(normalized.stale, false);
|
|
assert.deepEqual(normalized.platforms, ['openclaw']);
|
|
assert.deepEqual(normalized.affected, ['openclaw@<2026.4.25']);
|
|
});
|
|
|
|
test('normalizeGhsaAdvisory marks old GHSA-only advisories stale after threshold', () => {
|
|
const normalized = normalizeGhsaAdvisory(
|
|
advisory({ published_at: '2026-03-01T00:00:00Z' }),
|
|
{
|
|
now: fixedNow,
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
},
|
|
);
|
|
|
|
assert.equal(normalized.status, 'stale');
|
|
assert.equal(normalized.stale, true);
|
|
assert.equal(normalized.cve_id, null);
|
|
});
|
|
|
|
test('normalizeGhsaAdvisory marks existing GHSA entries matured when a CVE appears', () => {
|
|
const normalized = normalizeGhsaAdvisory(
|
|
advisory({ cve_id: 'CVE-2026-9999' }),
|
|
{
|
|
now: fixedNow,
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
},
|
|
);
|
|
|
|
assert.equal(normalized.status, 'matured');
|
|
assert.equal(normalized.stale, false);
|
|
assert.equal(normalized.cve_id, 'CVE-2026-9999');
|
|
assert.equal(normalized.nvd_url, 'https://nvd.nist.gov/vuln/detail/CVE-2026-9999');
|
|
});
|
|
|
|
test('buildGhsaWithoutCveFeed only imports CVE-backed advisories that were already tracked', () => {
|
|
const existing = {
|
|
version: '0.1.0',
|
|
advisories: [
|
|
normalizeGhsaAdvisory(advisory({ ghsa_id: 'GHSA-old-1111-2222' }), {
|
|
now: '2026-04-25T00:00:00Z',
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
}),
|
|
],
|
|
};
|
|
const fetched = [
|
|
{
|
|
repository: 'openclaw/openclaw',
|
|
advisories: [
|
|
advisory({ ghsa_id: 'GHSA-new-1111-2222', cve_id: null }),
|
|
advisory({ ghsa_id: 'GHSA-old-1111-2222', cve_id: 'CVE-2026-1111' }),
|
|
advisory({ ghsa_id: 'GHSA-cve-only-1111-2222', cve_id: 'CVE-2026-2222' }),
|
|
],
|
|
},
|
|
];
|
|
|
|
const feed = buildGhsaWithoutCveFeed({
|
|
fetched,
|
|
existingFeed: existing,
|
|
nvdFeed: { advisories: [] },
|
|
now: fixedNow,
|
|
staleAfterDays: 60,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
feed.advisories.map((entry) => [entry.id, entry.status, entry.cve_id]),
|
|
[
|
|
['GHSA-new-1111-2222', 'active', null],
|
|
['GHSA-old-1111-2222', 'matured', 'CVE-2026-1111'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildGhsaWithoutCveFeed matures tracked GHSAs when the CVE feed references them', () => {
|
|
const existing = {
|
|
version: '0.1.0',
|
|
advisories: [
|
|
normalizeGhsaAdvisory(advisory({ ghsa_id: 'GHSA-oooo-3333-4444' }), {
|
|
now: '2026-04-25T00:00:00Z',
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
}),
|
|
],
|
|
};
|
|
const feed = buildGhsaWithoutCveFeed({
|
|
fetched: [
|
|
{
|
|
repository: 'openclaw/openclaw',
|
|
advisories: [advisory({ ghsa_id: 'GHSA-oooo-3333-4444', cve_id: null })],
|
|
},
|
|
],
|
|
existingFeed: existing,
|
|
nvdFeed: {
|
|
advisories: [
|
|
{
|
|
id: 'CVE-2026-3333',
|
|
references: [
|
|
'https://github.com/openclaw/openclaw/security/advisories/GHSA-oooo-3333-4444',
|
|
],
|
|
},
|
|
],
|
|
},
|
|
now: fixedNow,
|
|
staleAfterDays: 60,
|
|
});
|
|
|
|
assert.equal(feed.advisories[0].status, 'matured');
|
|
assert.equal(feed.advisories[0].cve_id, 'CVE-2026-3333');
|
|
});
|
|
|
|
test('buildConsolidatedAdvisoryFeed appends active GHSA advisories without moving the NVD poll cursor', () => {
|
|
const canonicalFeed = {
|
|
version: '1.0.0',
|
|
updated: '2026-05-23T00:00:00Z',
|
|
description: 'Community-driven security advisory feed for ClawSec',
|
|
advisories: [
|
|
{
|
|
id: 'CVE-2026-1111',
|
|
severity: 'high',
|
|
type: 'os_command_injection',
|
|
title: 'Existing CVE',
|
|
description: 'Existing CVE advisory',
|
|
affected: ['openclaw@*'],
|
|
platforms: ['openclaw'],
|
|
action: 'Review NVD.',
|
|
published: '2026-05-01T00:00:00Z',
|
|
},
|
|
],
|
|
};
|
|
const ghsaFeed = {
|
|
advisories: [
|
|
normalizeGhsaAdvisory(advisory({ ghsa_id: 'GHSA-active-1111-2222', cve_id: null }), {
|
|
now: fixedNow,
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
}),
|
|
],
|
|
};
|
|
|
|
const consolidated = buildConsolidatedAdvisoryFeed({
|
|
canonicalFeed,
|
|
ghsaFeed,
|
|
now: fixedNow,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
consolidated.advisories.map((entry) => entry.id),
|
|
['CVE-2026-1111', 'GHSA-active-1111-2222'],
|
|
);
|
|
assert.equal(consolidated.updated, canonicalFeed.updated);
|
|
assert.equal(consolidated.advisories[1].source_feed, 'ghsa-without-cve');
|
|
});
|
|
|
|
test('buildConsolidatedAdvisoryFeed keeps existing GHSA advisories when replacement feed is empty', () => {
|
|
const canonicalFeed = {
|
|
version: '1.0.0',
|
|
updated: '2026-05-23T00:00:00Z',
|
|
advisories: [
|
|
{
|
|
id: 'CVE-2026-1111',
|
|
published: '2026-05-01T00:00:00Z',
|
|
},
|
|
{
|
|
id: 'GHSA-keep-1111-2222',
|
|
ghsa_id: 'GHSA-keep-1111-2222',
|
|
status: 'active',
|
|
published: '2026-05-02T00:00:00Z',
|
|
source_feed: 'ghsa-without-cve',
|
|
},
|
|
],
|
|
};
|
|
|
|
const consolidated = buildConsolidatedAdvisoryFeed({
|
|
canonicalFeed,
|
|
ghsaFeed: { advisories: [] },
|
|
now: fixedNow,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
consolidated.advisories.map((entry) => entry.id),
|
|
['GHSA-keep-1111-2222', 'CVE-2026-1111'],
|
|
);
|
|
});
|
|
|
|
test('buildConsolidatedAdvisoryFeed replaces only matching GHSA canonical entries', () => {
|
|
const canonicalFeed = {
|
|
version: '1.0.0',
|
|
updated: '2026-05-23T00:00:00Z',
|
|
advisories: [
|
|
{
|
|
id: 'GHSA-repl-1111-2222',
|
|
ghsa_id: 'GHSA-repl-1111-2222',
|
|
status: 'active',
|
|
title: 'Old GHSA payload',
|
|
published: '2026-05-01T00:00:00Z',
|
|
source_feed: 'ghsa-without-cve',
|
|
},
|
|
{
|
|
id: 'GHSA-keep-3333-4444',
|
|
ghsa_id: 'GHSA-keep-3333-4444',
|
|
status: 'active',
|
|
title: 'Retained GHSA payload',
|
|
published: '2026-05-02T00:00:00Z',
|
|
source_feed: 'ghsa-without-cve',
|
|
},
|
|
],
|
|
};
|
|
const ghsaFeed = {
|
|
advisories: [
|
|
{
|
|
id: 'GHSA-repl-1111-2222',
|
|
ghsa_id: 'GHSA-repl-1111-2222',
|
|
status: 'stale',
|
|
title: 'Replacement GHSA payload',
|
|
published: '2026-05-03T00:00:00Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
const consolidated = buildConsolidatedAdvisoryFeed({
|
|
canonicalFeed,
|
|
ghsaFeed,
|
|
now: fixedNow,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
consolidated.advisories.map((entry) => [entry.id, entry.title, entry.status]),
|
|
[
|
|
['GHSA-repl-1111-2222', 'Replacement GHSA payload', 'stale'],
|
|
['GHSA-keep-3333-4444', 'Retained GHSA payload', 'active'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('buildConsolidatedAdvisoryFeed drops GHSA duplicate when matching CVE is present', () => {
|
|
const canonicalFeed = {
|
|
version: '1.0.0',
|
|
updated: '2026-05-23T00:00:00Z',
|
|
advisories: [
|
|
{
|
|
id: 'CVE-2026-2222',
|
|
severity: 'high',
|
|
type: 'code_injection',
|
|
title: 'Canonical CVE',
|
|
description: 'Canonical CVE advisory',
|
|
affected: ['openclaw@*'],
|
|
platforms: ['openclaw'],
|
|
action: 'Review NVD.',
|
|
published: '2026-05-02T00:00:00Z',
|
|
},
|
|
{
|
|
id: 'GHSA-old-duplicate',
|
|
ghsa_id: 'GHSA-old-duplicate',
|
|
cve_id: 'CVE-2026-2222',
|
|
status: 'matured',
|
|
source_feed: 'ghsa-without-cve',
|
|
severity: 'high',
|
|
type: 'github_security_advisory',
|
|
title: 'Old duplicate',
|
|
description: 'Old provisional duplicate',
|
|
affected: ['openclaw@*'],
|
|
platforms: ['openclaw'],
|
|
action: 'Track CVE.',
|
|
published: '2026-05-01T00:00:00Z',
|
|
},
|
|
],
|
|
};
|
|
const ghsaFeed = {
|
|
advisories: [
|
|
normalizeGhsaAdvisory(
|
|
advisory({ ghsa_id: 'GHSA-new-duplicate', cve_id: 'CVE-2026-2222' }),
|
|
{
|
|
now: fixedNow,
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
},
|
|
),
|
|
],
|
|
};
|
|
|
|
const consolidated = buildConsolidatedAdvisoryFeed({
|
|
canonicalFeed,
|
|
ghsaFeed,
|
|
now: fixedNow,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
consolidated.advisories.map((entry) => entry.id),
|
|
['CVE-2026-2222'],
|
|
);
|
|
});
|
|
|
|
test('buildConsolidatedAdvisoryFeed keeps matured GHSA until CVE lands in canonical feed', () => {
|
|
const canonicalFeed = {
|
|
version: '1.0.0',
|
|
updated: '2026-05-23T00:00:00Z',
|
|
advisories: [],
|
|
};
|
|
const ghsaFeed = {
|
|
advisories: [
|
|
normalizeGhsaAdvisory(
|
|
advisory({ ghsa_id: 'GHSA-matured-1111-2222', cve_id: 'CVE-2026-4444' }),
|
|
{
|
|
now: fixedNow,
|
|
repository: 'openclaw/openclaw',
|
|
staleAfterDays: 60,
|
|
},
|
|
),
|
|
],
|
|
};
|
|
|
|
const consolidated = buildConsolidatedAdvisoryFeed({
|
|
canonicalFeed,
|
|
ghsaFeed,
|
|
now: fixedNow,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
consolidated.advisories.map((entry) => [entry.id, entry.status, entry.cve_id]),
|
|
[['GHSA-matured-1111-2222', 'matured', 'CVE-2026-4444']],
|
|
);
|
|
});
|