mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-16 13:53:04 +08:00
Add supply-chain advisory source refresh
This commit is contained in:
committed by
Affaan Mustafa
parent
2d46c00763
commit
a8e3bcb00f
10
.github/workflows/supply-chain-watch.yml
vendored
10
.github/workflows/supply-chain-watch.yml
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
469
scripts/ci/supply-chain-advisory-sources.js
Normal file
469
scripts/ci/supply-chain-advisory-sources.js
Normal 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,
|
||||
};
|
||||
@@ -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.',
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
157
tests/ci/supply-chain-advisory-sources.test.js
Normal file
157
tests/ci/supply-chain-advisory-sources.test.js
Normal 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();
|
||||
@@ -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++;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 })) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user