security: add supply-chain IOC scanner (#1904)

This commit is contained in:
Affaan Mustafa
2026-05-14 21:15:35 -04:00
committed by GitHub
parent 0e66c838c7
commit 7d15a2282b
7 changed files with 562 additions and 11 deletions

View File

@@ -242,11 +242,16 @@ jobs:
with:
node-version: '20.x'
- name: Install audit dependencies
run: npm ci --ignore-scripts
- name: Run npm audit
run: |
npm audit signatures
npm audit --audit-level=high
continue-on-error: true # Allows PR to proceed, but marks job as failed if vulnerabilities found
- name: Run supply-chain IOC scan
run: npm run security:ioc-scan
lint:
name: Lint

View File

@@ -7,16 +7,24 @@ they do not prove that the workflow executed the intended code path.
## Current External Trigger
As of 2026-05-13, the active incident class is the May 2026 TanStack npm
supply-chain compromise. ECC also keeps Mini Shai-Hulud-style npm worm IOCs in
the same release-safety sweep because both incident classes target package
install/publish paths and developer credentials:
As of 2026-05-15, the active incident class is the May 2026 TanStack npm
supply-chain compromise and broader Mini Shai-Hulud campaign. ECC keeps the
same IOC sweep for the related npm/PyPI waves because these incidents target
package install/publish paths, AI developer-tool configs, and developer
credentials:
- TanStack reported 84 malicious versions across 42 `@tanstack/*` packages,
published on 2026-05-11 between 19:20 and 19:26 UTC.
- GitHub advisory `GHSA-g7cv-rxg3-hmpx` / `CVE-2026-45321` describes
install-time malware that harvests cloud credentials, GitHub tokens, npm
credentials, Vault tokens, Kubernetes tokens, and SSH private keys.
- Follow-on reporting from StepSecurity, Socket, Aikido, and Wiz describes the
same campaign expanding into packages associated with Mistral AI, UiPath,
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
- The live IOC set includes persistence through Claude Code
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
`gh-token-monitor` LaunchAgent/systemd services. Remove those persistence
hooks before rotating a stolen GitHub token.
- The attack chain combined `pull_request_target`, GitHub Actions cache
poisoning across a fork/base trust boundary, and OIDC token extraction from a
GitHub Actions runner.
@@ -38,8 +46,8 @@ Run this before a release candidate, after a broad dependency bump, and after
any package-registry incident.
```bash
rg -n '(@tanstack|mistralai|uipath|opensearch|guardrails|axios)' \
package.json package-lock.json .opencode/package.json .opencode/package-lock.json
npm run security:ioc-scan
node scripts/ci/scan-supply-chain-iocs.js --home
npm ci --ignore-scripts
npm audit signatures
npm audit --audit-level=high
@@ -63,16 +71,23 @@ If ECC or a maintainer machine installed a known-bad package version:
- npm package versions and tarball integrity hashes;
- outbound network logs where available.
3. Treat the install host as compromised if lifecycle scripts may have run.
4. Rotate every credential reachable by the process:
4. Remove persistence hooks before token revocation:
- `~/.claude/settings.json` `SessionStart` hooks and adjacent
`router_runtime.js` / `setup.mjs` payload files;
- `.vscode/tasks.json` folder-open tasks and adjacent payload files;
- `~/Library/LaunchAgents/com.user.gh-token-monitor.plist`;
- `~/.config/systemd/user/gh-token-monitor.service`;
- `~/.local/bin/gh-token-monitor.sh`.
5. Rotate every credential reachable by the process:
- npm automation tokens and maintainer tokens;
- GitHub PATs, fine-grained tokens, deploy keys, and Actions secrets;
- cloud credentials, Vault tokens, Kubernetes service-account tokens, SSH
keys, and local `.npmrc` tokens;
- any MCP, plugin, or harness credentials available in environment variables
or user-scope config.
5. Purge GitHub Actions caches for affected repositories.
6. Reinstall from a clean environment with `npm ci --ignore-scripts` first.
7. Re-enable lifecycle scripts only after the dependency tree and package
6. Purge GitHub Actions caches for affected repositories.
7. Reinstall from a clean environment with `npm ci --ignore-scripts` first.
8. Re-enable lifecycle scripts only after the dependency tree and package
versions are pinned to known-clean releases.
## GitHub Actions Rules
@@ -108,6 +123,8 @@ Before tagging or publishing ECC:
Escalate to a maintainer security review before any release or merge if:
- a dependency lockfile references a package named in an active advisory;
- `node scripts/ci/scan-supply-chain-iocs.js --home` finds Claude Code,
VS Code, or OS-level persistence indicators;
- a workflow combines `pull_request_target` with dependency installation,
cache restore/save, PR-head checkout, or write permissions;
- a release workflow combines `id-token: write` with shared cache usage;

View File

@@ -289,6 +289,7 @@
"harness:adapters": "node scripts/harness-adapter-compliance.js",
"harness:audit": "node scripts/harness-audit.js",
"observability:ready": "node scripts/observability-readiness.js",
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.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,371 @@
#!/usr/bin/env node
/**
* Scan dependency manifests, lockfiles, AI-tool configs, and installed package
* payload paths for active supply-chain incident indicators.
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const DEFAULT_ROOT = path.resolve(__dirname, '../..');
const MALICIOUS_PACKAGE_VERSIONS = {
'@mistralai/mistralai': ['2.2.3', '2.2.4'],
'@mistralai/mistralai-azure': ['1.7.2', '1.7.3'],
'@mistralai/mistralai-gcp': ['1.7.2', '1.7.3'],
'@opensearch-project/opensearch': ['3.6.2', '3.8.0'],
'@tanstack/arktype-adapter': ['1.166.12', '1.166.15'],
'@tanstack/eslint-plugin-router': ['1.161.9', '1.161.12'],
'@tanstack/eslint-plugin-start': ['0.0.4', '0.0.7'],
'@tanstack/history': ['1.161.9', '1.161.12'],
'@tanstack/nitro-v2-vite-plugin': ['1.154.12', '1.154.15'],
'@tanstack/react-router': ['1.169.5', '1.169.8'],
'@tanstack/react-router-devtools': ['1.166.16', '1.166.19'],
'@tanstack/react-router-ssr-query': ['1.166.15', '1.166.18'],
'@tanstack/react-start': ['1.167.68', '1.167.71'],
'@tanstack/react-start-client': ['1.166.51', '1.166.54'],
'@tanstack/react-start-rsc': ['0.0.47', '0.0.50'],
'@tanstack/react-start-server': ['1.166.55', '1.166.58'],
'@tanstack/router-cli': ['1.166.46', '1.166.49'],
'@tanstack/router-core': ['1.169.5', '1.169.8'],
'@tanstack/router-devtools': ['1.166.16', '1.166.19'],
'@tanstack/router-devtools-core': ['1.167.6', '1.167.9'],
'@tanstack/router-generator': ['1.166.45', '1.166.48'],
'@tanstack/router-plugin': ['1.167.38', '1.167.41'],
'@tanstack/router-ssr-query-core': ['1.168.3', '1.168.6'],
'@tanstack/router-utils': ['1.161.11', '1.161.14'],
'@tanstack/router-vite-plugin': ['1.166.53', '1.166.56'],
'@tanstack/solid-router': ['1.169.5', '1.169.8'],
'@tanstack/solid-router-devtools': ['1.166.16', '1.166.19'],
'@tanstack/solid-router-ssr-query': ['1.166.15', '1.166.18'],
'@tanstack/solid-start': ['1.167.65', '1.167.68'],
'@tanstack/solid-start-client': ['1.166.50', '1.166.53'],
'@tanstack/solid-start-server': ['1.166.54', '1.166.57'],
'@tanstack/start-client-core': ['1.168.5', '1.168.8'],
'@tanstack/start-fn-stubs': ['1.161.9', '1.161.12'],
'@tanstack/start-plugin-core': ['1.169.23', '1.169.26'],
'@tanstack/start-server-core': ['1.167.33', '1.167.36'],
'@tanstack/start-static-server-functions': ['1.166.44', '1.166.47'],
'@tanstack/start-storage-context': ['1.166.38', '1.166.41'],
'@tanstack/valibot-adapter': ['1.166.12', '1.166.15'],
'@tanstack/virtual-file-routes': ['1.161.10', '1.161.13'],
'@tanstack/vue-router': ['1.169.5', '1.169.8'],
'@tanstack/vue-router-devtools': ['1.166.16', '1.166.19'],
'@tanstack/vue-router-ssr-query': ['1.166.15', '1.166.18'],
'@tanstack/vue-start': ['1.167.61', '1.167.64'],
'@tanstack/vue-start-client': ['1.166.46', '1.166.49'],
'@tanstack/vue-start-server': ['1.166.50', '1.166.53'],
'@tanstack/zod-adapter': ['1.166.12', '1.166.15'],
'@uipath/agent.sdk': ['0.0.18'],
'@uipath/agent-sdk': ['1.0.2'],
'@uipath/apollo-core': ['5.9.2'],
'@uipath/cli': ['1.0.1'],
'@uipath/robot': ['1.3.4'],
'cmux-agent-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.6', '0.1.7', '0.1.8'],
'guardrails-ai': ['0.10.1'],
'mistralai': ['2.4.6'],
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
'safe-action': ['0.8.3', '0.8.4'],
};
const CRITICAL_TEXT_INDICATORS = [
'@tanstack/setup',
'github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c',
'router_init.js',
'router_runtime.js',
'tanstack_runner.js',
'gh-token-monitor',
'com.user.gh-token-monitor',
'filev2.getsession.org',
'seed1.getsession.org',
'seed2.getsession.org',
'seed3.getsession.org',
'git-tanstack.com',
'83.142.209.194',
'api.masscan.cloud',
'A Mini Shai-Hulud has Appeared',
'PUSH UR T3MPRR',
];
const DEPENDENCY_FILENAMES = new Set([
'package.json',
'package-lock.json',
'pnpm-lock.yaml',
'yarn.lock',
'bun.lock',
'pyproject.toml',
'poetry.lock',
'requirements.txt',
]);
const PERSISTENCE_FILENAMES = new Set([
'settings.json',
'tasks.json',
'router_runtime.js',
'setup.mjs',
'gh-token-monitor.sh',
'com.user.gh-token-monitor.plist',
'gh-token-monitor.service',
]);
const PAYLOAD_FILENAMES = new Set([
'router_init.js',
'router_runtime.js',
'tanstack_runner.js',
'gh-token-monitor.sh',
]);
const IGNORED_DIRS = new Set([
'.git',
'.next',
'.pytest_cache',
'__pycache__',
'coverage',
'dist',
'docs',
'target',
'tests',
]);
function normalizeForMatch(value) {
return value.toLowerCase();
}
function isInSpecialConfigPath(filePath) {
const normalized = filePath.split(path.sep).join('/');
return /\/\.claude\//.test(normalized)
|| /\/\.vscode\//.test(normalized)
|| /\/\.kiro\/settings\//.test(normalized)
|| /\/Library\/LaunchAgents\//.test(normalized)
|| /\/\.config\/systemd\/user\//.test(normalized)
|| /\/\.local\/bin\//.test(normalized);
}
function shouldInspectFile(filePath) {
const base = path.basename(filePath);
if (DEPENDENCY_FILENAMES.has(base)) return true;
if (PERSISTENCE_FILENAMES.has(base) && isInSpecialConfigPath(filePath)) return true;
if (PAYLOAD_FILENAMES.has(base) && filePath.includes(`${path.sep}node_modules${path.sep}`)) return true;
return false;
}
function walkFiles(rootDir, files = []) {
if (!fs.existsSync(rootDir)) return files;
const stat = fs.statSync(rootDir);
if (stat.isFile()) {
if (shouldInspectFile(rootDir)) files.push(rootDir);
return files;
}
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name) && entry.name !== 'node_modules') continue;
if (entry.name === 'node_modules') {
walkNodeModules(fullPath, files);
} else {
walkFiles(fullPath, files);
}
} else if (entry.isFile() && shouldInspectFile(fullPath)) {
files.push(fullPath);
}
}
return files;
}
function walkNodeModules(nodeModulesDir, files) {
if (!fs.existsSync(nodeModulesDir)) return;
for (const entry of fs.readdirSync(nodeModulesDir, { withFileTypes: true })) {
if (entry.name.startsWith('.')) continue;
const fullPath = path.join(nodeModulesDir, entry.name);
if (entry.isDirectory()) {
if (entry.name.startsWith('@')) {
for (const scopedEntry of fs.readdirSync(fullPath, { withFileTypes: true })) {
if (scopedEntry.isDirectory()) {
inspectPackageDir(path.join(fullPath, scopedEntry.name), files);
}
}
} else {
inspectPackageDir(fullPath, files);
}
}
}
}
function inspectPackageDir(packageDir, files) {
for (const filename of [...DEPENDENCY_FILENAMES, ...PAYLOAD_FILENAMES, 'setup.mjs', 'execution.js']) {
const candidate = path.join(packageDir, filename);
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
files.push(candidate);
}
}
}
function readText(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch {
return '';
}
}
function lineForIndex(text, index) {
return text.slice(0, index).split(/\r?\n/).length;
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function addFinding(findings, severity, filePath, line, indicator, message) {
findings.push({ severity, filePath, line, indicator, message });
}
function scanFile(filePath, rootDir, findings) {
const base = path.basename(filePath);
const relativePath = path.relative(rootDir, filePath) || filePath;
const text = readText(filePath);
const lowerText = normalizeForMatch(text);
if (PAYLOAD_FILENAMES.has(base)) {
addFinding(
findings,
'critical',
relativePath,
1,
base,
'Known Mini Shai-Hulud/TanStack payload or persistence filename is present',
);
}
for (const indicator of CRITICAL_TEXT_INDICATORS) {
const index = lowerText.indexOf(normalizeForMatch(indicator));
if (index !== -1) {
addFinding(
findings,
'critical',
relativePath,
lineForIndex(text, index),
indicator,
'Known active supply-chain IOC is present',
);
}
}
if (!DEPENDENCY_FILENAMES.has(base)) return;
for (const [packageName, versions] of Object.entries(MALICIOUS_PACKAGE_VERSIONS)) {
const packageIndex = lowerText.indexOf(normalizeForMatch(packageName));
if (packageIndex === -1) continue;
for (const version of versions) {
const versionPattern = new RegExp(`(^|[^0-9a-z.])${escapeRegExp(version)}([^0-9a-z.]|$)`, 'i');
if (versionPattern.test(text) || lowerText.includes(`@${version}`)) {
addFinding(
findings,
'critical',
relativePath,
lineForIndex(text, packageIndex),
`${packageName}@${version}`,
'Dependency manifest or lockfile references a known compromised package version',
);
}
}
}
}
function homeTargets(homeDir) {
return [
'.claude/settings.json',
'.claude/router_runtime.js',
'.claude/setup.mjs',
'.vscode/tasks.json',
'.vscode/setup.mjs',
'Library/LaunchAgents/com.user.gh-token-monitor.plist',
'.config/systemd/user/gh-token-monitor.service',
'.local/bin/gh-token-monitor.sh',
].map(relativePath => path.join(homeDir, relativePath));
}
function scanSupplyChainIocs(options = {}) {
const rootDir = path.resolve(options.rootDir || DEFAULT_ROOT);
const files = walkFiles(rootDir);
const findings = [];
if (options.home) {
for (const target of homeTargets(options.homeDir || os.homedir())) {
if (fs.existsSync(target)) files.push(target);
}
}
for (const filePath of [...new Set(files)].sort()) {
scanFile(filePath, rootDir, findings);
}
return {
rootDir,
scannedFiles: files.length,
findings,
};
}
function parseArgs(argv) {
const options = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--root') {
options.rootDir = argv[++i];
} else if (arg === '--home') {
options.home = true;
} else if (arg === '--home-dir') {
options.home = true;
options.homeDir = argv[++i];
} else if (arg === '--json') {
options.json = true;
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
return options;
}
function printReport(result, json = false) {
if (json) {
console.log(JSON.stringify(result, null, 2));
return;
}
if (result.findings.length === 0) {
console.log(`Supply-chain IOC scan passed for ${result.rootDir} (${result.scannedFiles} files inspected)`);
return;
}
for (const finding of result.findings) {
console.error(
`${finding.severity.toUpperCase()}: ${finding.filePath}:${finding.line} ${finding.indicator}`,
);
console.error(` ${finding.message}`);
}
}
if (require.main === module) {
try {
const options = parseArgs(process.argv.slice(2));
const result = scanSupplyChainIocs(options);
printReport(result, options.json);
process.exit(result.findings.length > 0 ? 1 : 0);
} catch (error) {
console.error(error.message);
process.exit(2);
}
}
module.exports = {
CRITICAL_TEXT_INDICATORS,
MALICIOUS_PACKAGE_VERSIONS,
scanSupplyChainIocs,
};

View File

@@ -291,7 +291,9 @@ function buildChecks(rootDir) {
pass: fileExists(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md')
&& fileExists(rootDir, 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md')
&& fileExists(rootDir, 'docs/security/supply-chain-incident-response.md')
&& fileExists(rootDir, 'scripts/ci/scan-supply-chain-iocs.js')
&& fileExists(rootDir, 'scripts/ci/validate-workflow-security.js')
&& fileExists(rootDir, 'tests/ci/scan-supply-chain-iocs.test.js')
&& fileExists(rootDir, 'tests/ci/validate-workflow-security.test.js')
&& fileExists(rootDir, 'tests/scripts/npm-publish-surface.test.js')
&& fileExists(rootDir, 'tests/docs/ecc2-release-surface.test.js')
@@ -316,6 +318,10 @@ function buildChecks(rootDir) {
&& includesAll(supplyChainIncidentResponse, [
'TanStack',
'Mini Shai-Hulud',
'scan-supply-chain-iocs.js',
'gh-token-monitor',
'.claude/settings.json',
'.vscode/tasks.json',
'npm audit signatures',
'trusted publishing',
'pull_request_target',

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env node
/**
* Validate the active supply-chain IOC scanner.
*/
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', 'scan-supply-chain-iocs.js');
const { scanSupplyChainIocs } = require(SCRIPT_PATH);
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function withFixture(files, fn) {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-supply-chain-ioc-'));
try {
for (const [relativePath, contents] of Object.entries(files)) {
const fullPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, contents);
}
fn(rootDir);
} finally {
fs.rmSync(rootDir, { recursive: true, force: true });
}
}
function run() {
console.log('\n=== Testing supply-chain IOC scanner ===\n');
let passed = 0;
let failed = 0;
if (test('passes a clean dependency manifest', () => {
withFixture({
'package.json': JSON.stringify({ dependencies: { leftpad: '1.0.0' } }, null, 2),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.deepStrictEqual(result.findings, []);
});
})) passed++; else failed++;
if (test('rejects known compromised TanStack package versions in lockfiles', () => {
withFixture({
'package-lock.json': JSON.stringify({
packages: {
'node_modules/@tanstack/react-router': {
version: '1.169.5',
},
},
}, null, 2),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.match(result.findings[0].indicator, /@tanstack\/react-router@1\.169\.5/);
});
})) passed++; else failed++;
if (test('passes clean versions of watched packages', () => {
withFixture({
'package-lock.json': JSON.stringify({
packages: {
'node_modules/@tanstack/react-router': {
version: '1.170.0',
},
},
}, null, 2),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.deepStrictEqual(result.findings, []);
});
})) passed++; else failed++;
if (test('rejects malicious optional dependency markers', () => {
withFixture({
'package-lock.json': JSON.stringify({
packages: {
'node_modules/@tanstack/history': {
optionalDependencies: {
'@tanstack/setup': 'github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c',
},
},
},
}, null, 2),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.ok(result.findings.some(finding => finding.indicator === '@tanstack/setup'));
assert.ok(result.findings.some(finding => /79ac49/.test(finding.indicator)));
});
})) passed++; else failed++;
if (test('rejects Claude Code persistence payload references', () => {
withFixture({
'.claude/settings.json': JSON.stringify({
hooks: {
SessionStart: [{
hooks: [{ command: 'node ~/.claude/router_runtime.js' }],
}],
},
}, null, 2),
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.ok(result.findings.some(finding => finding.indicator === 'router_runtime.js'));
});
})) passed++; else failed++;
if (test('rejects installed payload filenames in node_modules', () => {
withFixture({
'node_modules/@tanstack/react-router/router_init.js': '/* payload */',
}, rootDir => {
const result = scanSupplyChainIocs({ rootDir });
assert.ok(result.findings.some(finding => finding.indicator === 'router_init.js'));
});
})) passed++; else failed++;
if (test('supports CLI JSON output and non-zero exit on findings', () => {
withFixture({
'package.json': JSON.stringify({ dependencies: { '@opensearch-project/opensearch': '3.8.0' } }, null, 2),
}, rootDir => {
const result = spawnSync('node', [SCRIPT_PATH, '--root', rootDir, '--json'], { encoding: 'utf8' });
assert.notStrictEqual(result.status, 0);
const parsed = JSON.parse(result.stdout);
assert.ok(parsed.findings.some(finding => finding.indicator === '@opensearch-project/opensearch@3.8.0'));
});
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
run();

View File

@@ -114,6 +114,10 @@ function seedMinimalRepo(rootDir, overrides = {}) {
'docs/security/supply-chain-incident-response.md': [
'TanStack',
'Mini Shai-Hulud',
'scan-supply-chain-iocs.js',
'gh-token-monitor',
'.claude/settings.json',
'.vscode/tasks.json',
'npm audit signatures',
'trusted publishing',
'pull_request_target',
@@ -126,6 +130,8 @@ function seedMinimalRepo(rootDir, overrides = {}) {
'id-token: write',
'shared cache'
].join('\n'),
'scripts/ci/scan-supply-chain-iocs.js': 'TanStack Mini Shai-Hulud gh-token-monitor',
'tests/ci/scan-supply-chain-iocs.test.js': 'scan-supply-chain-iocs',
'tests/ci/validate-workflow-security.test.js': 'npm audit signatures persist-credentials: false',
'tests/scripts/npm-publish-surface.test.js': 'npm pack --dry-run Python bytecode',
'tests/docs/ecc2-release-surface.test.js': 'publication-readiness.md',