Add supply-chain advisory source refresh

This commit is contained in:
Affaan Mustafa
2026-05-15 22:39:35 -04:00
committed by Affaan Mustafa
parent 2d46c00763
commit a8e3bcb00f
11 changed files with 675 additions and 13 deletions

View File

@@ -40,11 +40,17 @@ jobs:
- name: Validate IOC scanner fixtures
run: node tests/ci/scan-supply-chain-iocs.test.js
- name: Validate advisory source fixtures
run: node tests/ci/supply-chain-advisory-sources.test.js
- name: Generate IOC report
run: |
mkdir -p artifacts
node scripts/ci/scan-supply-chain-iocs.js --json > artifacts/supply-chain-ioc-report.json
- name: Generate advisory source report
run: node scripts/ci/supply-chain-advisory-sources.js --refresh --json > artifacts/supply-chain-advisory-sources.json
- name: Validate workflow hardening rules
run: node scripts/ci/validate-workflow-security.js
@@ -53,5 +59,7 @@ jobs:
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: supply-chain-ioc-report
path: artifacts/supply-chain-ioc-report.json
path: |
artifacts/supply-chain-ioc-report.json
artifacts/supply-chain-advisory-sources.json
retention-days: 14

View File

@@ -73,6 +73,7 @@ node scripts/ci/scan-supply-chain-iocs.js --home
npm ci --ignore-scripts
npm audit signatures
npm audit --audit-level=high
node scripts/ci/supply-chain-advisory-sources.js --json
node scripts/ci/validate-workflow-security.js
node tests/scripts/npm-publish-surface.test.js
node tests/run-all.js
@@ -86,8 +87,10 @@ evidence but do not rotate credentials for a docs-only reference.
ECC also runs `.github/workflows/supply-chain-watch.yml` every six hours and on
manual dispatch. The workflow is read-only, disables checkout credential
persistence, installs with `npm ci --ignore-scripts`, verifies npm registry
signatures, runs the IOC scanner fixtures, emits
`supply-chain-ioc-report.json`, and re-validates GitHub Actions hardening rules.
signatures, runs the IOC scanner fixtures, runs
`scripts/ci/supply-chain-advisory-sources.js --refresh --json`, emits
`supply-chain-ioc-report.json` and `supply-chain-advisory-sources.json`, and
re-validates GitHub Actions hardening rules.
Treat a failed scheduled watch as a release blocker until an operator confirms
whether the failure is a newly reported advisory, a stale scanner fixture, a
@@ -96,6 +99,12 @@ needs new indicators, update `scripts/ci/scan-supply-chain-iocs.js`, add fixture
coverage in `tests/ci/scan-supply-chain-iocs.test.js`, refresh this runbook, and
attach the latest JSON artifact to the release evidence.
The advisory-source artifact is the ITO-57 status payload. It records the
trusted source registry, live URL refresh warnings, and a Linear-ready summary.
Refresh source coverage through `npm run security:advisory-sources -- --json`
before changing IOC coverage, and attach the artifact to the next Linear project
status update after each significant merge batch.
## Immediate Response
If ECC or a maintainer machine installed a known-bad package version:

View File

@@ -64,6 +64,7 @@
"schemas/",
"scripts/catalog.js",
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/auto-update.js",
"scripts/claw.js",
@@ -301,6 +302,7 @@
"platform:audit": "node scripts/platform-audit.js",
"discussion:audit": "node scripts/discussion-audit.js",
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",
"security:advisory-sources": "node scripts/ci/supply-chain-advisory-sources.js",
"claw": "node scripts/claw.js",
"orchestrate:status": "node scripts/orchestration-status.js",
"orchestrate:worker": "bash scripts/orchestrate-codex-worker.sh",

View File

@@ -0,0 +1,469 @@
#!/usr/bin/env node
/**
* Build a refreshable source report for active supply-chain advisories.
*/
const fs = require('fs');
const http = require('http');
const https = require('https');
const path = require('path');
const DEFAULT_GENERATED_AT = () => new Date().toISOString();
const DEFAULT_TIMEOUT_MS = 5000;
const MAX_REDIRECTS = 5;
const DEFAULT_ADVISORY_SOURCES = [
{
id: 'tanstack-postmortem',
title: 'TanStack npm supply-chain compromise postmortem',
publisher: 'TanStack',
url: 'https://tanstack.com/blog/npm-supply-chain-compromise-postmortem',
sourceType: 'primary-incident-postmortem',
ecosystems: ['npm', 'GitHub Actions'],
signals: ['tanstack', 'trusted-publishing-limits', 'github-actions-cache-poisoning'],
},
{
id: 'github-ghsa-g7cv-rxg3-hmpx',
title: 'GitHub Advisory GHSA-g7cv-rxg3-hmpx / CVE-2026-45321',
publisher: 'GitHub Advisory Database',
url: 'https://github.com/advisories/GHSA-g7cv-rxg3-hmpx',
sourceType: 'security-advisory',
ecosystems: ['npm', 'AI developer tooling'],
signals: ['credential-theft', 'malicious-lifecycle-script', 'tanstack'],
},
{
id: 'tanstack-followup',
title: 'TanStack incident follow-up',
publisher: 'TanStack',
url: 'https://tanstack.com/blog/incident-followup',
sourceType: 'primary-incident-followup',
ecosystems: ['npm', 'GitHub Actions'],
signals: ['remediation', 'trusted-publishing-limits'],
},
{
id: 'stepsecurity-mini-shai-hulud',
title: 'Mini Shai-Hulud campaign analysis',
publisher: 'StepSecurity',
url: 'https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem',
sourceType: 'incident-analysis',
ecosystems: ['npm', 'PyPI', 'AI developer tooling'],
signals: ['mini-shai-hulud', 'claude-code-persistence', 'vscode-persistence', 'os-persistence'],
},
{
id: 'openai-tanstack-response',
title: 'OpenAI response to the TanStack npm supply-chain attack',
publisher: 'OpenAI',
url: 'https://openai.com/index/our-response-to-the-tanstack-npm-supply-chain-attack/',
sourceType: 'vendor-response',
ecosystems: ['npm', 'AI developer tooling'],
signals: ['codex-update', 'developer-tooling-exposure', 'remediation'],
},
{
id: 'wiz-mini-shai-hulud',
title: 'Mini Shai-Hulud broader npm campaign coverage',
publisher: 'Wiz',
url: 'https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised',
sourceType: 'incident-analysis',
ecosystems: ['npm', 'PyPI', 'AI developer tooling'],
signals: ['mini-shai-hulud', 'opensearch', 'mistral-ai', 'uipath', 'squawk'],
},
{
id: 'socket-node-ipc',
title: 'node-ipc package compromise',
publisher: 'Socket',
url: 'https://socket.dev/blog/node-ipc-package-compromised',
sourceType: 'incident-analysis',
ecosystems: ['npm'],
signals: ['node-ipc', 'payload-hash', 'destructive-package-behavior'],
},
{
id: 'npm-trusted-publishers',
title: 'npm trusted publishing documentation',
publisher: 'npm',
url: 'https://docs.npmjs.com/trusted-publishers/',
sourceType: 'registry-control-reference',
ecosystems: ['npm', 'GitHub Actions'],
signals: ['trusted-publishing-limits', 'provenance'],
},
{
id: 'cisa-npm-compromise',
title: 'CISA widespread supply-chain compromise impacting npm ecosystem',
publisher: 'CISA',
url: 'https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem',
sourceType: 'government-alert',
ecosystems: ['npm'],
signals: ['incident-response', 'credential-rotation', 'npm-compromise'],
},
];
function normalizeArray(values) {
return Array.isArray(values) ? values.filter(Boolean) : [];
}
function createCheck(id, status, summary, fix) {
return { id, status, summary, fix };
}
function uniqueValues(sources, field) {
return new Set(sources.flatMap(source => normalizeArray(source[field])));
}
function validateSources(sources) {
const checks = [];
const ids = new Set();
const duplicateIds = [];
const invalidSources = [];
for (const source of sources) {
if (ids.has(source.id)) duplicateIds.push(source.id);
ids.add(source.id);
if (!source.id || !source.title || !source.publisher || !source.url) {
invalidSources.push(source.id || '(missing id)');
}
}
checks.push(createCheck(
'advisory-source-count',
sources.length >= 8 ? 'pass' : 'fail',
`${sources.length} advisory sources registered`,
'Track at least eight sources spanning primary advisories, vendor responses, and registry controls.',
));
checks.push(createCheck(
'advisory-source-shape',
invalidSources.length === 0 && duplicateIds.length === 0 ? 'pass' : 'fail',
invalidSources.length === 0 && duplicateIds.length === 0
? 'all sources include id, title, publisher, and URL'
: `invalid sources: ${[...invalidSources, ...duplicateIds].join(', ')}`,
'Fix duplicate or incomplete advisory source records before relying on the watch artifact.',
));
const ecosystems = uniqueValues(sources, 'ecosystems');
const requiredEcosystems = ['npm', 'PyPI', 'AI developer tooling'];
const missingEcosystems = requiredEcosystems.filter(ecosystem => !ecosystems.has(ecosystem));
checks.push(createCheck(
'advisory-ecosystem-coverage',
missingEcosystems.length === 0 ? 'pass' : 'fail',
missingEcosystems.length === 0
? 'sources cover npm, PyPI, and AI developer tooling'
: `missing ecosystem coverage: ${missingEcosystems.join(', ')}`,
'Add sources for every active ecosystem touched by the campaign.',
));
const signals = uniqueValues(sources, 'signals');
const requiredSignals = [
'tanstack',
'mini-shai-hulud',
'claude-code-persistence',
'vscode-persistence',
'os-persistence',
'node-ipc',
'trusted-publishing-limits',
'remediation',
];
const missingSignals = requiredSignals.filter(signal => !signals.has(signal));
checks.push(createCheck(
'advisory-signal-coverage',
missingSignals.length === 0 ? 'pass' : 'fail',
missingSignals.length === 0
? 'sources cover package versions, persistence hooks, provenance limits, and remediation'
: `missing signal coverage: ${missingSignals.join(', ')}`,
'Update the source registry before adding or removing scanner indicators.',
));
return checks;
}
function refreshStatusFromResult(result) {
if (result && result.ok) {
return {
status: 'ok',
statusCode: result.statusCode || null,
finalUrl: result.finalUrl || null,
checkedAt: result.checkedAt || null,
};
}
return {
status: 'warning',
statusCode: result && result.statusCode ? result.statusCode : null,
finalUrl: result && result.finalUrl ? result.finalUrl : null,
checkedAt: result && result.checkedAt ? result.checkedAt : null,
error: result && result.error ? String(result.error) : 'source refresh failed',
};
}
async function defaultFetchSource(source, options = {}) {
const checkedAt = options.checkedAt || DEFAULT_GENERATED_AT();
try {
const result = await requestUrl(source.url, {
timeoutMs: options.timeoutMs || DEFAULT_TIMEOUT_MS,
redirectsRemaining: MAX_REDIRECTS,
method: 'HEAD',
});
if (result.statusCode === 405 || result.statusCode === 403) {
return requestUrl(source.url, {
timeoutMs: options.timeoutMs || DEFAULT_TIMEOUT_MS,
redirectsRemaining: MAX_REDIRECTS,
method: 'GET',
checkedAt,
});
}
return { ...result, checkedAt };
} catch (error) {
return {
ok: false,
statusCode: null,
finalUrl: source.url,
checkedAt,
error: error.message,
};
}
}
function requestUrl(url, options) {
return new Promise(resolve => {
const parsed = new URL(url);
const client = parsed.protocol === 'http:' ? http : https;
const request = client.request(parsed, {
method: options.method || 'HEAD',
timeout: options.timeoutMs || DEFAULT_TIMEOUT_MS,
headers: {
'User-Agent': 'ecc-supply-chain-watch/2.0',
Accept: 'text/html,application/json;q=0.9,*/*;q=0.8',
},
}, response => {
const statusCode = response.statusCode || 0;
const location = response.headers.location;
if (
statusCode >= 300
&& statusCode < 400
&& location
&& options.redirectsRemaining > 0
) {
response.resume();
const nextUrl = new URL(location, parsed).toString();
resolve(requestUrl(nextUrl, {
...options,
redirectsRemaining: options.redirectsRemaining - 1,
}));
return;
}
response.resume();
response.on('end', () => {
resolve({
ok: statusCode >= 200 && statusCode < 400,
statusCode,
finalUrl: url,
});
});
});
request.on('timeout', () => {
request.destroy(new Error(`timed out after ${options.timeoutMs || DEFAULT_TIMEOUT_MS}ms`));
});
request.on('error', error => {
resolve({
ok: false,
statusCode: null,
finalUrl: url,
error: error.message,
});
});
request.end();
});
}
function buildLinearStatus(report, sources) {
const primaryEvidence = sources
.filter(source => [
'primary-incident-postmortem',
'security-advisory',
'vendor-response',
'incident-analysis',
].includes(source.sourceType))
.slice(0, 5)
.map(source => `${source.publisher}: ${source.title}`);
return {
issueId: 'ITO-57',
status: 'in_progress',
summary: report.ready
? 'Advisory sources current; scheduled supply-chain watch now emits source refresh evidence.'
: 'Advisory source coverage needs repair before release readiness.',
evidence: primaryEvidence,
remaining: 'Linear status synchronization still needs a live connector/status-update pass after each significant merge batch.',
};
}
async function buildAdvisorySourceReport(options = {}) {
const generatedAt = options.generatedAt || DEFAULT_GENERATED_AT();
const sources = (options.sources || DEFAULT_ADVISORY_SOURCES).map(source => ({
...source,
ecosystems: normalizeArray(source.ecosystems),
signals: normalizeArray(source.signals),
}));
const checks = validateSources(sources);
const refreshEnabled = Boolean(options.refresh);
const fetchSource = options.fetchSource || defaultFetchSource;
let refreshWarnings = 0;
const reportSources = [];
for (const source of sources) {
let refreshStatus = { status: 'not_requested' };
if (refreshEnabled && source.refresh !== false) {
const result = await fetchSource(source, {
timeoutMs: options.timeoutMs || DEFAULT_TIMEOUT_MS,
checkedAt: generatedAt,
});
refreshStatus = refreshStatusFromResult(result);
if (refreshStatus.status !== 'ok') refreshWarnings += 1;
}
reportSources.push({ ...source, refreshStatus });
}
if (refreshEnabled) {
checks.push(createCheck(
'advisory-refresh',
refreshWarnings === 0 ? 'pass' : 'warn',
refreshWarnings === 0
? 'all advisory source URLs responded during refresh'
: `${refreshWarnings} advisory source URL(s) returned warnings during refresh`,
'Review warning sources manually before changing IOC coverage or release evidence.',
));
} else {
checks.push(createCheck(
'advisory-refresh',
'pass',
'live advisory refresh not requested for this offline source contract report',
'Run with --refresh in the scheduled watch to capture live URL status evidence.',
));
}
const ready = checks.every(check => check.status !== 'fail');
const report = {
schema_version: 'ecc.supply-chain-advisory-sources.v1',
generatedAt,
ready,
refresh: {
enabled: refreshEnabled,
ok: refreshEnabled ? refreshWarnings === 0 : null,
warningCount: refreshWarnings,
},
sources: reportSources,
checks,
};
report.linear = {
status: buildLinearStatus(report, reportSources),
};
return report;
}
function parseArgs(argv) {
const options = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--help' || arg === '-h') {
options.help = true;
} else if (arg === '--json') {
options.json = true;
} else if (arg === '--refresh') {
options.refresh = true;
} else if (arg === '--strict-refresh') {
options.strictRefresh = true;
options.refresh = true;
} else if (arg === '--generated-at') {
options.generatedAt = argv[++i];
} else if (arg === '--timeout-ms') {
options.timeoutMs = Number(argv[++i]);
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
throw new Error('--timeout-ms must be a positive number');
}
} else if (arg === '--write') {
options.writePath = argv[++i];
if (!options.writePath) throw new Error('--write requires a path');
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
return options;
}
function printHelp() {
console.log(`Usage: node scripts/ci/supply-chain-advisory-sources.js [options]
Build the active supply-chain advisory source report used by the scheduled
watch workflow and Linear ITO-57 status updates.
Options:
--json Emit JSON instead of text
--refresh Check source URLs and record warning status
--strict-refresh Fail when a refreshed source URL returns a warning
--generated-at <ts> Override the report timestamp
--timeout-ms <n> Per-source refresh timeout (default: ${DEFAULT_TIMEOUT_MS})
--write <path> Write the report to a file
--help, -h Show this help
`);
}
function renderText(report) {
const lines = [
`Supply-chain advisory sources: ${report.ready ? 'ready' : 'blocked'}`,
`Sources: ${report.sources.length}`,
`Refresh: ${report.refresh.enabled ? (report.refresh.ok ? 'ok' : `warnings=${report.refresh.warningCount}`) : 'not requested'}`,
`Linear ${report.linear.status.issueId}: ${report.linear.status.summary}`,
];
for (const check of report.checks) {
lines.push(`- ${check.status.toUpperCase()} ${check.id}: ${check.summary}`);
}
return `${lines.join('\n')}\n`;
}
function writeReport(report, writePath) {
const absolutePath = path.resolve(writePath);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, `${JSON.stringify(report, null, 2)}\n`);
}
if (require.main === module) {
(async () => {
try {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
process.exit(0);
}
const report = await buildAdvisorySourceReport(options);
if (options.writePath) writeReport(report, options.writePath);
if (options.json) {
console.log(JSON.stringify(report, null, 2));
} else {
process.stdout.write(renderText(report));
}
const failed = !report.ready || (options.strictRefresh && report.refresh.enabled && !report.refresh.ok);
process.exit(failed ? 1 : 0);
} catch (error) {
console.error(error.message);
process.exit(2);
}
})();
}
module.exports = {
DEFAULT_ADVISORY_SOURCES,
buildAdvisorySourceReport,
parseArgs,
renderText,
};

View File

@@ -287,6 +287,7 @@ function buildRequirements(rootDir, platformReport) {
const observabilityReadiness = readText(rootDir, 'docs/architecture/observability-readiness.md');
const stalePrSalvage = readText(rootDir, 'docs/stale-pr-salvage-ledger.md');
const supplyChainRunbook = readText(rootDir, 'docs/security/supply-chain-incident-response.md');
const supplyChainWorkflow = readText(rootDir, '.github/workflows/supply-chain-watch.yml');
const packageJson = readPackage(rootDir);
const scripts = packageJson.scripts || {};
@@ -444,12 +445,16 @@ function buildRequirements(rootDir, platformReport) {
'supply-chain-local-protection',
'Keep Mini Shai-Hulud/TanStack protection loop current',
'supply-chain watch plus runbook',
includesAll(supplyChainRunbook, ['TanStack', 'Mini Shai-Hulud', 'scan-supply-chain-iocs.js'])
includesAll(supplyChainRunbook, ['TanStack', 'Mini Shai-Hulud', 'scan-supply-chain-iocs.js', 'supply-chain-advisory-sources.js'])
&& includesAll(supplyChainWorkflow, ['supply-chain-advisory-sources.js', 'supply-chain-advisory-sources.json'])
&& scripts['security:advisory-sources'] === 'node scripts/ci/supply-chain-advisory-sources.js'
&& fileExists(rootDir, '.github/workflows/supply-chain-watch.yml')
? 'current'
: 'in_progress',
'scheduled supply-chain watch and runbook are present',
'advisory-source refresh automation and Linear status synchronization remain ITO-57 follow-up'
scripts['security:advisory-sources'] === 'node scripts/ci/supply-chain-advisory-sources.js'
? 'scheduled supply-chain watch now emits IOC and advisory-source refresh artifacts'
: 'scheduled supply-chain watch or advisory-source command is missing',
'Linear status synchronization remains ITO-57 follow-up after each significant merge batch'
),
];
}
@@ -502,7 +507,7 @@ function buildReport(options) {
top_actions: topActions,
next_work_order: [
'Regenerate this dashboard from the final release commit before publication evidence is recorded.',
'Continue ITO-57 with advisory-source refresh automation and Linear status synchronization for the scheduled supply-chain watch.',
'Continue ITO-57 with Linear status synchronization for the scheduled supply-chain watch advisory-source report.',
'Advance ECC Tools live Marketplace test-account readback before publishing native-payments announcement copy.',
'Resume ITO-45, ITO-46, and ITO-56 only after the generated dashboard and final release gates are refreshed.',
],

View File

@@ -464,8 +464,11 @@ function buildLocalEvidenceChecks(rootDir) {
),
buildCheck(
'supply-chain-runbook',
includesAll(supplyChain, ['TanStack', 'Mini Shai-Hulud', 'node-ipc', 'scan-supply-chain-iocs.js']) ? 'pass' : 'fail',
'supply-chain runbook covers the current TanStack/Mini Shai-Hulud/node-ipc scanner lane',
includesAll(supplyChain, ['TanStack', 'Mini Shai-Hulud', 'node-ipc', 'scan-supply-chain-iocs.js', 'supply-chain-advisory-sources.js'])
&& packageScripts['security:advisory-sources'] === 'node scripts/ci/supply-chain-advisory-sources.js'
? 'pass'
: 'fail',
'supply-chain runbook covers the current TanStack/Mini Shai-Hulud/node-ipc scanner and advisory-source lanes',
{ path: 'docs/security/supply-chain-incident-response.md' }
),
buildCheck(

View File

@@ -0,0 +1,157 @@
#!/usr/bin/env node
/**
* Validate the supply-chain advisory source refresh report.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const SCRIPT_PATH = path.join(
__dirname,
'..',
'..',
'scripts',
'ci',
'supply-chain-advisory-sources.js',
);
const {
DEFAULT_ADVISORY_SOURCES,
buildAdvisorySourceReport,
} = require(SCRIPT_PATH);
async function test(name, fn) {
try {
await fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function run() {
console.log('\n=== Testing supply-chain advisory source refresh ===\n');
let passed = 0;
let failed = 0;
if (await test('default sources cover the active npm and PyPI campaign', async () => {
const ids = DEFAULT_ADVISORY_SOURCES.map(source => source.id);
for (const requiredId of [
'tanstack-postmortem',
'github-ghsa-g7cv-rxg3-hmpx',
'stepsecurity-mini-shai-hulud',
'openai-tanstack-response',
'socket-node-ipc',
'cisa-npm-compromise',
]) {
assert.ok(ids.includes(requiredId), `Missing advisory source ${requiredId}`);
}
const ecosystemCoverage = new Set(DEFAULT_ADVISORY_SOURCES.flatMap(source => source.ecosystems));
assert.ok(ecosystemCoverage.has('npm'));
assert.ok(ecosystemCoverage.has('PyPI'));
assert.ok(ecosystemCoverage.has('AI developer tooling'));
})) passed++; else failed++;
if (await test('offline report emits passing coverage checks and Linear-ready ITO-57 payload', async () => {
const report = await buildAdvisorySourceReport({
generatedAt: '2026-05-16T00:00:00.000Z',
refresh: false,
});
assert.strictEqual(report.schema_version, 'ecc.supply-chain-advisory-sources.v1');
assert.strictEqual(report.ready, true);
assert.strictEqual(report.refresh.enabled, false);
assert.ok(report.sources.length >= 8);
assert.ok(report.checks.every(check => check.status === 'pass'));
assert.strictEqual(report.linear.status.issueId, 'ITO-57');
assert.match(report.linear.status.summary, /advisory sources current/i);
assert.match(report.linear.status.remaining, /Linear status/i);
})) passed++; else failed++;
if (await test('refresh mode records per-source live check results', async () => {
const calls = [];
const report = await buildAdvisorySourceReport({
generatedAt: '2026-05-16T00:00:00.000Z',
refresh: true,
fetchSource: async source => {
calls.push(source.id);
return {
ok: true,
statusCode: 200,
finalUrl: source.url,
checkedAt: '2026-05-16T00:00:00.000Z',
};
},
});
assert.deepStrictEqual(
calls.sort(),
DEFAULT_ADVISORY_SOURCES.filter(source => source.refresh !== false).map(source => source.id).sort(),
);
assert.strictEqual(report.refresh.enabled, true);
assert.strictEqual(report.refresh.ok, true);
assert.ok(report.sources.every(source => source.refreshStatus.status === 'ok'));
})) passed++; else failed++;
if (await test('refresh errors are captured as evidence without breaking offline source coverage', async () => {
const report = await buildAdvisorySourceReport({
generatedAt: '2026-05-16T00:00:00.000Z',
refresh: true,
fetchSource: async source => ({
ok: source.id !== 'socket-node-ipc',
statusCode: source.id === 'socket-node-ipc' ? 403 : 200,
error: source.id === 'socket-node-ipc' ? 'forbidden' : null,
finalUrl: source.url,
checkedAt: '2026-05-16T00:00:00.000Z',
}),
});
const socketSource = report.sources.find(source => source.id === 'socket-node-ipc');
assert.strictEqual(report.ready, true);
assert.strictEqual(report.refresh.ok, false);
assert.strictEqual(socketSource.refreshStatus.status, 'warning');
assert.match(socketSource.refreshStatus.error, /forbidden/);
assert.ok(report.checks.some(check => check.id === 'advisory-refresh' && check.status === 'warn'));
})) passed++; else failed++;
if (await test('CLI JSON can be written as a scheduled workflow artifact', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-advisory-sources-'));
const outputPath = path.join(tempDir, 'advisory-sources.json');
try {
const result = spawnSync('node', [
SCRIPT_PATH,
'--json',
'--generated-at',
'2026-05-16T00:00:00.000Z',
'--write',
outputPath,
], {
encoding: 'utf8',
shell: process.platform === 'win32',
});
assert.strictEqual(result.status, 0, result.stderr);
const parsed = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
assert.strictEqual(parsed.schema_version, 'ecc.supply-chain-advisory-sources.v1');
assert.strictEqual(parsed.ready, true);
assert.ok(parsed.linear.status.evidence.length >= 3);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
run();

View File

@@ -58,9 +58,13 @@ function run() {
if (test('runs IOC fixtures, emits JSON report, and uploads the artifact', () => {
assert.match(source, /node tests\/ci\/scan-supply-chain-iocs\.test\.js/);
assert.match(source, /node scripts\/ci\/scan-supply-chain-iocs\.js --json > artifacts\/supply-chain-ioc-report\.json/);
assert.match(source, /node tests\/ci\/supply-chain-advisory-sources\.test\.js/);
assert.match(source, /node scripts\/ci\/supply-chain-advisory-sources\.js --refresh --json > artifacts\/supply-chain-advisory-sources\.json/);
assert.match(source, /node scripts\/ci\/validate-workflow-security\.js/);
assert.match(source, /uses: actions\/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a/);
assert.match(source, /name: supply-chain-ioc-report/);
assert.match(source, /artifacts\/supply-chain-ioc-report\.json/);
assert.match(source, /artifacts\/supply-chain-advisory-sources\.json/);
assert.match(source, /retention-days: 14/);
})) passed++; else failed++;

View File

@@ -44,6 +44,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/ecc.js",
"scripts/catalog.js",
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/claw.js",
"scripts/discussion-audit.js",
@@ -124,6 +125,7 @@ function main() {
for (const requiredPath of [
"scripts/catalog.js",
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
"scripts/discussion-audit.js",
"scripts/operator-readiness-dashboard.js",

View File

@@ -39,7 +39,8 @@ function seedRepo(rootDir, overrides = {}) {
'observability:ready': 'node scripts/observability-readiness.js',
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
'platform:audit': 'node scripts/platform-audit.js',
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js'
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js'
}
}, null, 2),
'scripts/operator-readiness-dashboard.js': 'operator dashboard generator',
@@ -74,9 +75,9 @@ function seedRepo(rootDir, overrides = {}) {
'docs/stale-pr-salvage-ledger.md': 'Manual review tail',
'docs/architecture/progress-sync-contract.md': 'GitHub PRs/issues/discussions Linear project local handoff repo roadmap scripts/work-items.js',
'docs/architecture/observability-readiness.md': 'observability-readiness.js',
'docs/security/supply-chain-incident-response.md': 'TanStack Mini Shai-Hulud node-ipc scan-supply-chain-iocs.js',
'docs/security/supply-chain-incident-response.md': 'TanStack Mini Shai-Hulud node-ipc scan-supply-chain-iocs.js supply-chain-advisory-sources.js',
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md': 'TanStack Mini Shai-Hulud Node IPC follow-up node-ipc IOC scan',
'.github/workflows/supply-chain-watch.yml': 'name: Supply-Chain Watch'
'.github/workflows/supply-chain-watch.yml': 'name: Supply-Chain Watch supply-chain-advisory-sources.js supply-chain-advisory-sources.json'
};
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {

View File

@@ -35,6 +35,7 @@ function seedRepo(rootDir, overrides = {}) {
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
'observability:ready': 'node scripts/observability-readiness.js',
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js',
'harness:audit': 'node scripts/harness-audit.js'
}
}, null, 2),
@@ -55,7 +56,8 @@ function seedRepo(rootDir, overrides = {}) {
'TanStack',
'Mini Shai-Hulud',
'node-ipc',
'scan-supply-chain-iocs.js'
'scan-supply-chain-iocs.js',
'supply-chain-advisory-sources.js'
].join('\n'),
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md': [
'TanStack',