release: verify production portfolio deployment
This commit is contained in:
76
tests/deployment/headers.spec.ts
Normal file
76
tests/deployment/headers.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// @ts-expect-error The production release checker is an executable ESM module.
|
||||
import { verifySupplyChainEvidence } from '../../scripts/supply-chain-check.mjs';
|
||||
|
||||
const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex');
|
||||
|
||||
async function evidenceFixture(overrides: Record<string, unknown> = {}) {
|
||||
const root = await mkdtemp(join(tmpdir(), 'portfolio-supply-chain-'));
|
||||
const evidence = join(root, 'evidence');
|
||||
await mkdir(evidence);
|
||||
const packageJson = JSON.stringify({ name: 'portfolio', version: '1.0.0', devDependencies: { '@cyclonedx/cyclonedx-npm': '6.0.1', 'license-checker-rseidelsohn': '4.4.2' } });
|
||||
const packageLock = JSON.stringify({ name: 'portfolio', version: '1.0.0', lockfileVersion: 3, packages: {
|
||||
'': { name: 'portfolio', version: '1.0.0' },
|
||||
'node_modules/@cyclonedx/cyclonedx-npm': { version: '6.0.1', integrity: 'sha512-test' },
|
||||
'node_modules/license-checker-rseidelsohn': { version: '4.4.2', integrity: 'sha512-test' },
|
||||
} });
|
||||
await writeFile(join(root, 'package.json'), packageJson);
|
||||
await writeFile(join(root, 'package-lock.json'), packageLock);
|
||||
const reports = {
|
||||
'npm-audit-production.json': JSON.stringify(overrides.productionAudit ?? { auditReportVersion: 2, vulnerabilities: {}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0, total: 0 } } }),
|
||||
'npm-audit-development.json': JSON.stringify(overrides.developmentAudit ?? { auditReportVersion: 2, vulnerabilities: {}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0, total: 0 } } }),
|
||||
'licenses.json': JSON.stringify(overrides.licenses ?? { 'portfolio@1.0.0': { licenses: 'ISC' } }),
|
||||
'sbom.cdx.json': JSON.stringify(overrides.sbom ?? { bomFormat: 'CycloneDX', specVersion: '1.6', version: 1, metadata: { component: { type: 'application', name: 'portfolio', version: '1.0.0' } }, components: [] }),
|
||||
'medium-triage.json': JSON.stringify(overrides.triage ?? { schema: 'portfolio-medium-vulnerability-triage/v1', findings: [] }),
|
||||
};
|
||||
for (const [name, contents] of Object.entries(reports)) await writeFile(join(evidence, name), contents);
|
||||
const manifest = {
|
||||
schema: 'portfolio-supply-chain-evidence/v1',
|
||||
packageJsonSha256: sha256(packageJson),
|
||||
packageLockSha256: sha256(packageLock),
|
||||
reports: Object.fromEntries(Object.entries(reports).map(([name, contents]) => [name, sha256(contents)])),
|
||||
toolVersions: { npm: '11.0.0', cyclonedxNpm: '6.0.1', licenseChecker: '4.4.2' },
|
||||
};
|
||||
await writeFile(join(evidence, 'evidence-manifest.json'), JSON.stringify(manifest));
|
||||
return { root, evidence };
|
||||
}
|
||||
|
||||
describe('supply-chain evidence', () => {
|
||||
it('accepts a digest-bound clean audit, allowlisted licenses, and CycloneDX SBOM', async () => {
|
||||
const fixture = await evidenceFixture();
|
||||
await expect(verifySupplyChainEvidence({ projectRoot: fixture.root, evidenceDir: fixture.evidence, requireTrackedTriage: false })).resolves.toMatchObject({
|
||||
critical: 0, high: 0, medium: 0, componentCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed on altered evidence, Critical/High findings, or forbidden licenses', async () => {
|
||||
const fixture = await evidenceFixture();
|
||||
await writeFile(join(fixture.evidence, 'licenses.json'), JSON.stringify({ 'portfolio@1.0.0': { licenses: 'AGPL-3.0' } }));
|
||||
await expect(verifySupplyChainEvidence({ projectRoot: fixture.root, evidenceDir: fixture.evidence, requireTrackedTriage: false })).rejects.toThrow(/digest/u);
|
||||
|
||||
const high = await evidenceFixture({
|
||||
productionAudit: { auditReportVersion: 2, vulnerabilities: { unsafe: { name: 'unsafe', severity: 'high', via: [], effects: [], range: '*', nodes: [] } }, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 1, critical: 0, total: 1 } } },
|
||||
});
|
||||
await expect(verifySupplyChainEvidence({ projectRoot: high.root, evidenceDir: high.evidence, requireTrackedTriage: false })).rejects.toThrow(/High|high/u);
|
||||
|
||||
const license = await evidenceFixture({ licenses: { 'portfolio@1.0.0': { licenses: 'AGPL-3.0-only' } } });
|
||||
await expect(verifySupplyChainEvidence({ projectRoot: license.root, evidenceDir: license.evidence, requireTrackedTriage: false })).rejects.toThrow(/license/u);
|
||||
});
|
||||
|
||||
it('requires an exact, current triage record for every Medium finding', async () => {
|
||||
const audit = { auditReportVersion: 2, vulnerabilities: { packageA: { name: 'packageA', severity: 'moderate', via: [], effects: [], range: '<2', nodes: ['node_modules/packageA'] } }, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 1, high: 0, critical: 0, total: 1 } } };
|
||||
const missing = await evidenceFixture({ developmentAudit: audit });
|
||||
await expect(verifySupplyChainEvidence({ projectRoot: missing.root, evidenceDir: missing.evidence, requireTrackedTriage: false })).rejects.toThrow(/triage/u);
|
||||
|
||||
const accepted = await evidenceFixture({
|
||||
developmentAudit: audit,
|
||||
triage: { schema: 'portfolio-medium-vulnerability-triage/v1', findings: [{ scope: 'development', package: 'packageA', severity: 'moderate', range: '<2', disposition: 'accepted', rationale: 'Build-only tool; no deployed runtime exposure.', owner: 'Luca Pîslaru', reviewBy: '2027-01-01' }] },
|
||||
});
|
||||
await expect(verifySupplyChainEvidence({ projectRoot: accepted.root, evidenceDir: accepted.evidence, requireTrackedTriage: false, today: '2026-09-02' })).resolves.toMatchObject({ medium: 1 });
|
||||
});
|
||||
});
|
||||
17
tests/deployment/no-javascript.spec.ts
Normal file
17
tests/deployment/no-javascript.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// @ts-expect-error The production smoke checker is an executable ESM module.
|
||||
import { verifyNoJavaScriptDocument } from '../../scripts/smoke-production.mjs';
|
||||
|
||||
describe('JavaScript-disabled production document', () => {
|
||||
it('requires substantive semantic identity, work evidence, navigation, and contact content', () => {
|
||||
const html = '<!doctype html><html><head><title>Luca Pîslaru</title></head><body><nav><a href="#selected-builds">Selected builds</a><a href="#contact">Contact</a></nav><main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><section id="selected-builds"><h2>Selected builds</h2><p>Project evidence and technical decisions.</p></section><section id="contact"><h2>Contact</h2><a href="mailto:portfolio@example.test">Email Luca</a></section></main><script src="/assets/app.12345678.js"></script></body></html>';
|
||||
expect(verifyNoJavaScriptDocument(html)).toMatchObject({ headings: expect.any(Number), links: expect.any(Number) });
|
||||
});
|
||||
|
||||
it('rejects a JavaScript-only shell, source maps, and private identifiers', () => {
|
||||
expect(() => verifyNoJavaScriptDocument('<div id="app"></div><script src="app.js"></script>')).toThrow(/substantive|identity/u);
|
||||
expect(() => verifyNoJavaScriptDocument('<main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><h2>Selected builds</h2><p>Evidence</p><h2>Contact</h2><a href="mailto:x@y.z">Email</a><script src="app.js.map"></script></main>')).toThrow(/source map/u);
|
||||
expect(() => verifyNoJavaScriptDocument('<main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><h2>Selected builds</h2><p>SimpaSteel is private.</p><h2>Contact</h2><a href="mailto:x@y.z">Email</a></main>')).toThrow(/private identifier/u);
|
||||
});
|
||||
});
|
||||
82
tests/deployment/seo.spec.ts
Normal file
82
tests/deployment/seo.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { brotliCompressSync, gzipSync } from 'node:zlib';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// @ts-expect-error The production smoke checker is an executable ESM module.
|
||||
import { verifyProductionSmoke } from '../../scripts/smoke-production.mjs';
|
||||
|
||||
const digest = 'a'.repeat(64);
|
||||
const canonical = 'https://portfolio.example/';
|
||||
const alternate = 'http://www.portfolio.example/';
|
||||
const html = `<!doctype html><html><head>
|
||||
<title>Luca Pîslaru — System Administrator & Full-Stack Developer</title>
|
||||
<meta name="description" content="Systems administration and full-stack development portfolio." />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="${canonical}" />
|
||||
<meta property="og:title" content="Luca Pîslaru" />
|
||||
<meta property="og:description" content="Systems administration and full-stack development portfolio." />
|
||||
<meta property="og:url" content="${canonical}" />
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","name":"Luca Pîslaru","url":"${canonical}"}</script>
|
||||
<script type="module" src="/assets/app.12345678.js"></script></head>
|
||||
<body><main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><section><h2>Selected builds</h2><p>Evidence-led project work.</p></section><section><h2>Contact</h2><p>Discuss systems and software work.</p><a href="mailto:hello@example.com">Contact</a></section></main></body></html>`;
|
||||
|
||||
const servers: ReturnType<typeof createServer>[] = [];
|
||||
afterEach(async () => Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve())))));
|
||||
|
||||
async function fixtureFetch() {
|
||||
const server = createServer((request, response) => {
|
||||
const original = request.headers['x-original-url'];
|
||||
const url = new URL(String(original));
|
||||
if (url.origin === new URL(alternate).origin) {
|
||||
response.writeHead(308, { location: canonical }); response.end(); return;
|
||||
}
|
||||
const base = {
|
||||
'x-portfolio-manifest-digest': digest,
|
||||
'content-security-policy': "default-src 'self'; script-src 'self' 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; style-src 'self'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests",
|
||||
'strict-transport-security': 'max-age=31536000',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'permissions-policy': 'camera=(), microphone=(), geolocation=()',
|
||||
'x-frame-options': 'DENY',
|
||||
};
|
||||
if (url.pathname === '/') {
|
||||
response.writeHead(200, { ...base, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'public, max-age=0, must-revalidate', 'content-encoding': 'br' }); response.end(brotliCompressSync(html)); return;
|
||||
}
|
||||
if (url.pathname === '/robots.txt') {
|
||||
response.writeHead(200, { ...base, 'content-type': 'text/plain', 'cache-control': 'public, max-age=0, must-revalidate' }); response.end(`User-agent: *\nAllow: /\nSitemap: ${canonical}sitemap.xml\n`); return;
|
||||
}
|
||||
if (url.pathname === '/sitemap.xml') {
|
||||
response.writeHead(200, { ...base, 'content-type': 'application/xml', 'cache-control': 'public, max-age=0, must-revalidate' }); response.end(`<?xml version="1.0"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${canonical}</loc></url></urlset>`); return;
|
||||
}
|
||||
if (url.pathname === '/assets/app.12345678.js') {
|
||||
response.writeHead(200, { ...base, 'content-type': 'text/javascript', 'cache-control': 'public, max-age=31536000, immutable', 'content-encoding': 'gzip' }); response.end(gzipSync('console.log("portfolio")')); return;
|
||||
}
|
||||
if (url.pathname === '/assets/demo.12345678.webm') {
|
||||
if (request.headers.range !== 'bytes=0-1023') { response.writeHead(416, base); response.end(); return; }
|
||||
response.writeHead(206, { ...base, 'content-type': 'video/webm', 'cache-control': 'public, max-age=31536000, immutable', 'accept-ranges': 'bytes', 'content-range': 'bytes 0-1023/2048', 'content-length': '1024' }); response.end(Buffer.alloc(1024)); return;
|
||||
}
|
||||
response.writeHead(404, base); response.end();
|
||||
});
|
||||
servers.push(server);
|
||||
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === 'string') throw new Error('fixture server did not bind');
|
||||
return async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const requested = input instanceof Request ? input.url : input.toString();
|
||||
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
|
||||
headers.set('x-original-url', requested);
|
||||
return fetch(`http://127.0.0.1:${address.port}${new URL(requested).pathname}`, { ...init, redirect: 'manual', headers });
|
||||
};
|
||||
}
|
||||
|
||||
describe('production smoke and SEO contract', () => {
|
||||
it('verifies canonical metadata, response policy, compression, MIME, ranges, and one-hop redirects', async () => {
|
||||
const fetchImpl = await fixtureFetch();
|
||||
await expect(verifyProductionSmoke({ canonicalUrl: canonical, alternateUrls: [alternate], expectedManifestDigest: digest, videoPath: '/assets/demo.12345678.webm', fetchImpl })).resolves.toMatchObject({ manifestDigest: digest, canonicalUrl: canonical, jsDisabledContentVerified: true });
|
||||
});
|
||||
|
||||
it('rejects a deployed digest mismatch and redirect chains', async () => {
|
||||
const fetchImpl = await fixtureFetch();
|
||||
await expect(verifyProductionSmoke({ canonicalUrl: canonical, alternateUrls: [alternate], expectedManifestDigest: 'b'.repeat(64), videoPath: '/assets/demo.12345678.webm', fetchImpl })).rejects.toThrow(/manifest-digest|manifest digest/u);
|
||||
});
|
||||
});
|
||||
8
tests/deployment/vitest.config.ts
Normal file
8
tests/deployment/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['tests/deployment/**/*.spec.ts'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user