BREAKING CHANGE(rust-bridge): make Rust the primary security backend, remove all TS fallbacks
Some checks failed
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Type Check & Lint (push) Failing after 6s
CI / Build All Platforms (push) Failing after 4s

Phase 3 of the Rust migration: the Rust security bridge is now mandatory
and all TypeScript security fallback implementations have been removed.

- UnifiedEmailServer.start() throws if Rust bridge fails to start
- SpfVerifier gutted to thin wrapper (parseSpfRecord stays in TS)
- DKIMVerifier gutted to thin wrapper delegating to bridge.verifyDkim()
- IPReputationChecker delegates to bridge.checkIpReputation(), keeps LRU cache
- DmarcVerifier keeps alignment logic (works with pre-computed results)
- DKIM signing via bridge.signDkim() in all 4 locations
- Removed mailauth and ip packages from plugins.ts (~1,200 lines deleted)
This commit is contained in:
2026-02-10 20:30:43 +00:00
parent ffe294643c
commit b82468ab1e
24 changed files with 457 additions and 2695 deletions

View File

@@ -1,24 +1,22 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { IPReputationChecker, ReputationThreshold, IPType } from '../ts/security/classes.ipreputationchecker.js';
import * as plugins from '../ts/plugins.js';
import { IPReputationChecker, ReputationThreshold } from '../ts/security/classes.ipreputationchecker.js';
import { RustSecurityBridge } from '../ts/security/classes.rustsecuritybridge.js';
// Mock for dns lookup
const originalDnsResolve = plugins.dns.promises.resolve;
let mockDnsResolveImpl: (hostname: string) => Promise<string[]> = async () => ['127.0.0.1'];
let bridge: RustSecurityBridge;
// Setup mock DNS resolver with proper typing
(plugins.dns.promises as any).resolve = async (hostname: string) => {
return mockDnsResolveImpl(hostname);
};
// Start the Rust bridge before tests
tap.test('setup - start Rust security bridge', async () => {
bridge = RustSecurityBridge.getInstance();
const ok = await bridge.start();
expect(ok).toEqual(true);
});
// Test instantiation
tap.test('IPReputationChecker - should be instantiable', async () => {
const checker = IPReputationChecker.getInstance({
enableDNSBL: false,
enableIPInfo: false,
enableLocalCache: false
});
expect(checker).toBeTruthy();
});
@@ -26,92 +24,62 @@ tap.test('IPReputationChecker - should be instantiable', async () => {
tap.test('IPReputationChecker - should use singleton pattern', async () => {
const checker1 = IPReputationChecker.getInstance();
const checker2 = IPReputationChecker.getInstance();
// Both instances should be the same object
expect(checker1 === checker2).toEqual(true);
});
// Test IP validation
tap.test('IPReputationChecker - should validate IP address format', async () => {
const checker = IPReputationChecker.getInstance({
enableDNSBL: false,
enableIPInfo: false,
enableLocalCache: false
});
// Valid IP should work
const result = await checker.checkReputation('192.168.1.1');
expect(result.score).toBeGreaterThan(0);
expect(result.error).toBeUndefined();
const checker = IPReputationChecker.getInstance();
// Invalid IP should fail with error
const invalidResult = await checker.checkReputation('invalid.ip');
expect(invalidResult.error).toBeTruthy();
});
// Test DNSBL lookups
tap.test('IPReputationChecker - should check IP against DNSBL', async () => {
try {
// Setup mock implementation for DNSBL
mockDnsResolveImpl = async (hostname: string) => {
// Listed in DNSBL if IP contains 2
if (hostname.includes('2.1.168.192') && hostname.includes('zen.spamhaus.org')) {
return ['127.0.0.2'];
}
throw { code: 'ENOTFOUND' };
};
// Test reputation check via Rust bridge
tap.test('IPReputationChecker - should check IP reputation via Rust', async () => {
const testInstance = new IPReputationChecker({
enableLocalCache: false,
maxCacheSize: 10
});
// Create a new instance with specific settings for this test
const testInstance = new IPReputationChecker({
dnsblServers: ['zen.spamhaus.org'],
enableIPInfo: false,
enableLocalCache: false,
maxCacheSize: 1 // Small cache for testing
});
// Clean IP should have good score
const cleanResult = await testInstance.checkReputation('192.168.1.1');
expect(cleanResult.isSpam).toEqual(false);
expect(cleanResult.score).toEqual(100);
// Blacklisted IP should have reduced score
const blacklistedResult = await testInstance.checkReputation('192.168.1.2');
expect(blacklistedResult.isSpam).toEqual(true);
expect(blacklistedResult.score < 100).toEqual(true); // Less than 100
expect(blacklistedResult.blacklists).toBeTruthy();
expect((blacklistedResult.blacklists || []).length > 0).toEqual(true);
} catch (err) {
console.error('Test error:', err);
throw err;
}
// Check a public IP (Google DNS) — should get a result with a score
const result = await testInstance.checkReputation('8.8.8.8');
expect(result).toBeTruthy();
expect(result.score).toBeGreaterThan(0);
expect(result.score).toBeLessThanOrEqual(100);
expect(typeof result.isSpam).toEqual('boolean');
expect(typeof result.isProxy).toEqual('boolean');
expect(typeof result.isTor).toEqual('boolean');
expect(typeof result.isVPN).toEqual('boolean');
expect(result.timestamp).toBeGreaterThan(0);
});
// Test caching behavior
tap.test('IPReputationChecker - should cache reputation results', async () => {
// Create a fresh instance for this test
const testInstance = new IPReputationChecker({
enableIPInfo: false,
enableLocalCache: false,
maxCacheSize: 10 // Small cache for testing
maxCacheSize: 10
});
// Check that first look performs a lookup and second uses cache
const ip = '192.168.1.10';
const ip = '1.1.1.1';
// First check should add to cache
const result1 = await testInstance.checkReputation(ip);
expect(result1).toBeTruthy();
// Manually verify it's in cache - access private member for testing
// Verify it's in cache
const hasInCache = (testInstance as any).reputationCache.has(ip);
expect(hasInCache).toEqual(true);
// Call again, should use cache
const result2 = await testInstance.checkReputation(ip);
expect(result2).toBeTruthy();
// Results should be identical
// Results should be identical (from cache)
expect(result1.score).toEqual(result2.score);
expect(result1.isSpam).toEqual(result2.isSpam);
});
// Test risk level classification
@@ -122,58 +90,27 @@ tap.test('IPReputationChecker - should classify risk levels correctly', async ()
expect(IPReputationChecker.getRiskLevel(90)).toEqual('trusted');
});
// Test IP type detection
tap.test('IPReputationChecker - should detect special IP types', async () => {
// Test error handling for error result
tap.test('IPReputationChecker - should handle errors gracefully', async () => {
const testInstance = new IPReputationChecker({
enableDNSBL: false,
enableIPInfo: true,
enableLocalCache: false,
maxCacheSize: 5 // Small cache for testing
maxCacheSize: 5
});
// Test Tor exit node detection
const torResult = await testInstance.checkReputation('171.25.1.1');
expect(torResult.isTor).toEqual(true);
expect(torResult.score < 90).toEqual(true);
// Test VPN detection
const vpnResult = await testInstance.checkReputation('185.156.1.1');
expect(vpnResult.isVPN).toEqual(true);
expect(vpnResult.score < 90).toEqual(true);
// Test proxy detection
const proxyResult = await testInstance.checkReputation('34.92.1.1');
expect(proxyResult.isProxy).toEqual(true);
expect(proxyResult.score < 90).toEqual(true);
});
// Test error handling
tap.test('IPReputationChecker - should handle DNS lookup errors gracefully', async () => {
// Setup mock implementation to simulate error
mockDnsResolveImpl = async () => {
throw new Error('DNS server error');
};
const checker = IPReputationChecker.getInstance({
dnsblServers: ['zen.spamhaus.org'],
enableIPInfo: false,
enableLocalCache: false,
maxCacheSize: 300 // Force new instance
});
// Should return a result despite errors
const result = await checker.checkReputation('192.168.1.1');
expect(result.score).toEqual(100); // No blacklist hits found due to error
// Invalid format should return error result with neutral score
const result = await testInstance.checkReputation('not-an-ip');
expect(result.score).toEqual(50);
expect(result.error).toBeTruthy();
expect(result.isSpam).toEqual(false);
});
// Restore original implementation at the end
tap.test('Cleanup - restore mocks', async () => {
plugins.dns.promises.resolve = originalDnsResolve;
// Stop bridge
tap.test('cleanup - stop Rust security bridge', async () => {
await bridge.stop();
});
tap.test('stop', async () => {
await tap.stopForcefully();
});
export default tap.start();
export default tap.start();