- Eltern-Ordner ist jetzt EIN Git-Repo (statt getrennter Repos). - root .gitignore haelt Secrets (.env), node_modules, DB und Build-Artefakte raus. - release.ps1: manueller Release (ZIP bauen + ans Backend laden). - root README mit Struktur und Release-Ablauf. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
5.0 KiB
JavaScript
88 lines
5.0 KiB
JavaScript
import { extractResources, analyze, isPublicHost, isPrivateIp } from '../src/scan.js';
|
|
|
|
let fail = 0;
|
|
const ok = (name, cond, extra = '') => {
|
|
console.log((cond ? '[PASS] ' : '[FAIL] ') + name + (cond ? '' : ' ' + extra));
|
|
if (!cond) fail++;
|
|
};
|
|
|
|
const html = `
|
|
<!DOCTYPE html><html><head>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
|
|
<script src="/wp-includes/js/jquery.js"></script>
|
|
<script src="https://www.googletagmanager.com/gtag/js?id=G-XYZ"></script>
|
|
</head><body>
|
|
<img src="https://cdn.example.com/logo.png">
|
|
<img src="/local/pic.png">
|
|
<iframe src="https://www.google.com/maps/embed?pb=1" width="600" height="450"></iframe>
|
|
<iframe src="https://www.youtube-nocookie.com/embed/abc"></iframe>
|
|
<iframe src="data:text/html,nope"></iframe>
|
|
<object data="https://player.vimeo.com/video/123"></object>
|
|
<a href="https://twitter.com/foo">x</a>
|
|
</body></html>`;
|
|
|
|
const base = 'https://example.com/page/';
|
|
const resources = extractResources(html, base);
|
|
|
|
ok('finds google maps iframe', resources.some((r) => r.type === 'iframe' && r.url.includes('google.com/maps')));
|
|
ok('finds youtube-nocookie iframe', resources.some((r) => r.url.includes('youtube-nocookie.com/embed')));
|
|
ok('skips data: iframe', !resources.some((r) => r.url.startsWith('data:')));
|
|
ok('finds external script (gtm)', resources.some((r) => r.type === 'script' && r.url.includes('googletagmanager.com')));
|
|
ok('resolves relative script to absolute', resources.some((r) => r.url === 'https://example.com/wp-includes/js/jquery.js'));
|
|
ok('finds external img', resources.some((r) => r.type === 'img' && r.url.includes('cdn.example.com')));
|
|
ok('finds link stylesheet', resources.some((r) => r.type === 'link' && r.url.includes('fonts.googleapis.com')));
|
|
ok('finds object data (vimeo)', resources.some((r) => r.type === 'object' && r.url.includes('player.vimeo.com')));
|
|
ok('ignores anchor href', !resources.some((r) => r.url.includes('twitter.com')));
|
|
|
|
const findings = analyze([{ url: base, resources }], 'example.com');
|
|
const byHost = Object.fromEntries(findings.map((f) => [f.host, f]));
|
|
|
|
ok('example.com is first-party', byHost['example.com'] && byHost['example.com'].third_party === false);
|
|
ok('cdn.example.com is first-party (subdomain)', byHost['cdn.example.com'].third_party === false);
|
|
ok('google.com is third-party', byHost['google.com'].third_party === true);
|
|
ok('googletagmanager third-party', byHost['googletagmanager.com'].third_party === true);
|
|
ok('third-party sorted first', findings[0].third_party === true);
|
|
ok('suggested_pattern = host', byHost['player.vimeo.com'].suggested_pattern === 'player.vimeo.com');
|
|
ok('types aggregated', Array.isArray(byHost['google.com'].types) && byHost['google.com'].types.includes('iframe'));
|
|
ok('sample_urls capped at 3', findings.every((f) => f.sample_urls.length <= 3));
|
|
ok('pages list source page', Array.isArray(byHost['google.com'].pages) && byHost['google.com'].pages.includes(base));
|
|
|
|
// pages aggregate across multiple scanned pages
|
|
const multi = analyze([
|
|
{ url: 'https://example.com/a', resources: [{ url: 'https://google.com/maps', type: 'iframe' }] },
|
|
{ url: 'https://example.com/b', resources: [{ url: 'https://google.com/maps', type: 'iframe' }] },
|
|
], 'example.com');
|
|
ok('host found on both pages', multi[0].pages.length === 2 && multi[0].pages.includes('https://example.com/a') && multi[0].pages.includes('https://example.com/b'));
|
|
|
|
// www normalization: www.google.com counts as google.com
|
|
const f2 = analyze([{ url: base, resources: [{ url: 'https://www.google.com/x', type: 'iframe' }] }], 'example.com');
|
|
ok('www stripped in host grouping', f2[0].host === 'google.com');
|
|
|
|
// isPublicHost
|
|
ok('localhost not public', isPublicHost('localhost') === false);
|
|
ok('127.0.0.1 not public', isPublicHost('127.0.0.1') === false);
|
|
ok('10.x not public', isPublicHost('10.1.2.3') === false);
|
|
ok('192.168 not public', isPublicHost('192.168.0.1') === false);
|
|
ok('172.16 not public', isPublicHost('172.16.0.1') === false);
|
|
ok('172.32 IS public', isPublicHost('172.32.0.1') === true);
|
|
ok('public domain ok', isPublicHost('kunde-a.de') === true);
|
|
|
|
// isPrivateIp (post-DNS-resolution SSRF guard)
|
|
ok('metadata 169.254.169.254 private', isPrivateIp('169.254.169.254') === true);
|
|
ok('127.0.0.1 private', isPrivateIp('127.0.0.1') === true);
|
|
ok('10.x private', isPrivateIp('10.0.0.5') === true);
|
|
ok('192.168 private', isPrivateIp('192.168.1.1') === true);
|
|
ok('172.16 private', isPrivateIp('172.16.5.5') === true);
|
|
ok('172.32 public ip', isPrivateIp('172.32.0.1') === false);
|
|
ok('CGNAT 100.64 private', isPrivateIp('100.64.0.1') === true);
|
|
ok('public ip allowed', isPrivateIp('93.184.216.34') === false);
|
|
ok('::1 private', isPrivateIp('::1') === true);
|
|
ok('fe80 link-local private', isPrivateIp('fe80::1') === true);
|
|
ok('fd00 ula private', isPrivateIp('fd12::1') === true);
|
|
ok('ipv4-mapped metadata private', isPrivateIp('::ffff:169.254.169.254') === true);
|
|
ok('global ipv6 allowed', isPrivateIp('2606:2800:220:1:248:1893:25c8:1946') === false);
|
|
ok('empty ip unsafe', isPrivateIp('') === true);
|
|
|
|
console.log('\n' + (fail === 0 ? 'ALL PASSED' : fail + ' FAILED'));
|
|
process.exit(fail === 0 ? 0 : 1);
|