mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Block fabricated pulse-branded domains with a lint audit
Offline guard for the pulseapp.io class: any domain-shaped token
containing 'pulse' in published surfaces (root markdown, docs and
mirrors, non-test frontend src and Go source) must be pulserelay.pro,
1mk.app, or a documented placeholder. Catches the invented
security@pulseapp.io / docs.pulseapp.io (10 months as the published
security contact, bouncing every disclosure) and the pulse.app
OpenRouter referer fixed in b1240c6ca. Runs in npm run lint, so the
pre-push hook enforces it.
This commit is contained in:
@@ -35,11 +35,12 @@
|
||||
"test:coverage": "vitest run --coverage --coverage.provider=v8 --coverage.include=src/**/*.ts --coverage.include=src/**/*.tsx --coverage.exclude=src/index.tsx",
|
||||
"test:coverage:ai": "vitest run --coverage --coverage.provider=v8 --coverage.thresholds.100 --coverage.thresholds.perFile --coverage.include=src/components/AI/aiChatUtils.ts",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "npm run lint:eslint && npm run lint:theme && npm run lint:copy && npm run lint:canonical-platforms && npm run lint:form-labels",
|
||||
"lint": "npm run lint:eslint && npm run lint:theme && npm run lint:copy && npm run lint:canonical-platforms && npm run lint:form-labels && npm run lint:external-domains",
|
||||
"lint:canonical-platforms": "node scripts/canonical-platform-audit.mjs",
|
||||
"lint:eslint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"lint:copy": "node scripts/copy-style-audit.mjs",
|
||||
"lint:form-labels": "node scripts/form-label-audit.mjs",
|
||||
"lint:external-domains": "node scripts/external-domain-audit.mjs",
|
||||
"lint:theme": "node scripts/theme-audit.mjs",
|
||||
"lint:headers": "node scripts/header-audit.mjs",
|
||||
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
// Blocks fabricated Pulse-branded domains from entering published surfaces.
|
||||
//
|
||||
// Context: commit 524f42cc28 invented security@pulseapp.io and docs.pulseapp.io
|
||||
// (pulseapp.io was never a Pulse domain); the address shipped as the published
|
||||
// security contact for ~10 months and bounced every disclosure. A follow-up
|
||||
// audit on 2026-09-01 found https://pulse.app claimed as the OpenRouter
|
||||
// attribution referer. This audit is the offline guard for that class: any
|
||||
// domain-shaped token containing "pulse" that is not on the owned/placeholder
|
||||
// allowlist fails the push. It cannot judge ownership of non-Pulse-branded
|
||||
// domains; the periodic link audit covers those.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const REPO_ROOT = path.resolve(ROOT, '..');
|
||||
|
||||
// Real public suffixes a fabricated brand domain would plausibly use. Tokens
|
||||
// ending in anything else (.role, .local, .example, .sh script names, Go
|
||||
// identifiers) are not treated as domains.
|
||||
const PUBLIC_TLDS = new Set([
|
||||
'com', 'net', 'org', 'io', 'app', 'dev', 'pro', 'cloud', 'ai', 'me',
|
||||
'co', 'eu', 'de', 'es', 'fr', 'uk', 'us', 'xyz', 'info', 'tech', 'online',
|
||||
'site', 'blog',
|
||||
]);
|
||||
|
||||
function isAllowedHost(host) {
|
||||
if (host === 'pulserelay.pro' || host.endsWith('.pulserelay.pro')) return true;
|
||||
if (host === '1mk.app' || host.endsWith('.1mk.app')) return true;
|
||||
// Cloudflare Pages previews of the landing site.
|
||||
if (host.endsWith('.pages.dev') && host.includes('pulserelay-landing')) return true;
|
||||
const labels = host.split('.');
|
||||
// Documentation placeholders.
|
||||
if (labels.includes('example') || labels.some((l) => l.includes('yourdomain'))) return true;
|
||||
if (host.startsWith('your-') || host.startsWith('yourname.')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const SCAN_ROOT_FILES = ['SECURITY.md', 'README.md', 'CONTRIBUTING.md', 'TERMS.md'];
|
||||
const SCAN_DIRS = [
|
||||
{ dir: 'docs', extensions: null },
|
||||
{ dir: 'frontend-modern/public/docs', extensions: null },
|
||||
{ dir: 'frontend-modern/src', extensions: /\.(ts|tsx)$/ },
|
||||
{ dir: 'internal', extensions: /\.go$/ },
|
||||
{ dir: 'cmd', extensions: /\.go$/ },
|
||||
{ dir: 'pkg', extensions: /\.go$/ },
|
||||
];
|
||||
const SKIP_DIR_NAMES = new Set(['node_modules', '__tests__', 'testdata', '.git']);
|
||||
const SKIP_FILE_PATTERNS = [/_test\.go$/, /\.test\.(ts|tsx)$/, /\.(png|jpg|jpeg|gif|webp|svg|ico|woff2?|mp4|pdf|zip|gz)$/i];
|
||||
const DOMAIN_TOKEN = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+/gi;
|
||||
|
||||
const findings = [];
|
||||
|
||||
function toRelative(absPath) {
|
||||
return path.relative(REPO_ROOT, absPath).replaceAll(path.sep, '/');
|
||||
}
|
||||
|
||||
function collectFiles(directory, extensions) {
|
||||
const files = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(directory, { withFileTypes: true });
|
||||
} catch {
|
||||
return files;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (SKIP_DIR_NAMES.has(entry.name)) continue;
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectFiles(fullPath, extensions));
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
if (extensions && !extensions.test(entry.name)) continue;
|
||||
if (SKIP_FILE_PATTERNS.some((p) => p.test(entry.name))) continue;
|
||||
files.push(fullPath);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function inspectFile(absPath) {
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(absPath, 'utf8');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (content.includes('\u0000')) return;
|
||||
const lines = content.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
for (const match of lines[i].matchAll(DOMAIN_TOKEN)) {
|
||||
// Capitalized single-label .app tokens are macOS/iOS bundle names
|
||||
// (Pulse.app), not domains; a fabricated domain reads lowercase.
|
||||
if (/^[A-Z][A-Za-z0-9-]*\.app$/.test(match[0])) continue;
|
||||
const host = match[0].toLowerCase();
|
||||
if (!host.includes('pulse')) continue;
|
||||
const labels = host.split('.');
|
||||
if (!PUBLIC_TLDS.has(labels[labels.length - 1])) continue;
|
||||
if (isAllowedHost(host)) continue;
|
||||
findings.push({ file: toRelative(absPath), line: i + 1, host });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const targets = SCAN_ROOT_FILES.map((f) => path.join(REPO_ROOT, f)).filter((f) => fs.existsSync(f));
|
||||
for (const { dir, extensions } of SCAN_DIRS) {
|
||||
targets.push(...collectFiles(path.join(REPO_ROOT, dir), extensions));
|
||||
}
|
||||
for (const file of targets) inspectFile(file);
|
||||
|
||||
if (findings.length > 0) {
|
||||
console.error(
|
||||
'External domain audit failed. Pulse-branded domains must be pulserelay.pro or 1mk.app;',
|
||||
);
|
||||
console.error(
|
||||
'anything else is fabricated (see the pulseapp.io incident). Fix the reference or, for a',
|
||||
);
|
||||
console.error('legitimate new Pulse-owned domain, add it to isAllowedHost with a comment.');
|
||||
for (const finding of findings) {
|
||||
console.error(`- ${finding.file}:${finding.line}: ${finding.host}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('External domain audit passed with no unrecognized Pulse-branded domains.');
|
||||
Reference in New Issue
Block a user