2022-07-16 18:15:23 +02:00
|
|
|
import * as plugins from './detector.plugins.js';
|
2025-05-26 09:40:16 +00:00
|
|
|
import { ServiceType } from './detector.interfaces.js';
|
|
|
|
|
import type { IDetectorResult, IDetectorOptions } from './detector.interfaces.js';
|
2021-04-13 08:30:33 +00:00
|
|
|
|
|
|
|
|
export class Detector {
|
|
|
|
|
private smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
|
|
|
|
|
2025-05-26 09:40:16 +00:00
|
|
|
/**
|
|
|
|
|
* Check if a port is active - simple boolean version for backward compatibility
|
|
|
|
|
*/
|
|
|
|
|
public async isActiveSimple(urlArg: string): Promise<boolean> {
|
|
|
|
|
const result = await this.isActive(urlArg);
|
|
|
|
|
return result.isActive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a port is active with detailed results
|
|
|
|
|
*/
|
|
|
|
|
public async isActive(urlArg: string, options?: IDetectorOptions): Promise<IDetectorResult> {
|
2022-07-16 18:15:23 +02:00
|
|
|
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg);
|
2021-04-14 11:47:51 +00:00
|
|
|
if (parsedUrl.hostname === 'localhost') {
|
2021-04-13 08:30:33 +00:00
|
|
|
console.log(`detector target is localhost on port ${parsedUrl.port}`);
|
2022-07-16 18:15:23 +02:00
|
|
|
const portUnused = await this.smartnetworkInstance.isLocalPortUnused(
|
2025-05-26 08:53:25 +00:00
|
|
|
parseInt(parsedUrl.port, 10),
|
2022-07-16 18:15:23 +02:00
|
|
|
);
|
2021-04-14 11:47:51 +00:00
|
|
|
const portAvailable = !portUnused;
|
2025-05-26 09:40:16 +00:00
|
|
|
|
|
|
|
|
const result: IDetectorResult = {
|
|
|
|
|
isActive: portAvailable
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (portAvailable && options?.detectServiceType) {
|
|
|
|
|
const serviceType = await this.detectType(urlArg);
|
|
|
|
|
result.serviceType = serviceType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
2021-04-13 08:30:33 +00:00
|
|
|
} else {
|
|
|
|
|
console.log(`detector target is remote domain ${parsedUrl.host} on port ${parsedUrl.port}`);
|
2025-05-26 09:40:16 +00:00
|
|
|
const portAvailable = await this.smartnetworkInstance.isRemotePortAvailable(
|
2022-07-16 18:15:23 +02:00
|
|
|
parsedUrl.host,
|
2025-05-26 08:53:25 +00:00
|
|
|
parseInt(parsedUrl.port, 10),
|
2022-07-16 18:15:23 +02:00
|
|
|
);
|
2025-05-26 09:40:16 +00:00
|
|
|
|
|
|
|
|
const result: IDetectorResult = {
|
|
|
|
|
isActive: portAvailable
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (portAvailable && options?.detectServiceType) {
|
|
|
|
|
const serviceType = await this.detectType(urlArg);
|
|
|
|
|
result.serviceType = serviceType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
2021-04-13 08:30:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 09:40:16 +00:00
|
|
|
public async detectType(urlArg: string): Promise<ServiceType> {
|
|
|
|
|
const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(urlArg);
|
|
|
|
|
const port = parseInt(parsedUrl.port, 10);
|
|
|
|
|
const hostname = parsedUrl.hostname;
|
|
|
|
|
|
|
|
|
|
// Check common ports first
|
|
|
|
|
const commonPorts: { [key: number]: ServiceType } = {
|
|
|
|
|
80: ServiceType.HTTP,
|
|
|
|
|
443: ServiceType.HTTPS,
|
|
|
|
|
22: ServiceType.SSH,
|
|
|
|
|
21: ServiceType.FTP,
|
|
|
|
|
25: ServiceType.SMTP,
|
|
|
|
|
110: ServiceType.POP3,
|
|
|
|
|
143: ServiceType.IMAP,
|
|
|
|
|
3306: ServiceType.MYSQL,
|
|
|
|
|
5432: ServiceType.POSTGRESQL,
|
|
|
|
|
27017: ServiceType.MONGODB,
|
|
|
|
|
6379: ServiceType.REDIS
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (commonPorts[port]) {
|
|
|
|
|
// Verify the service is actually what we expect
|
|
|
|
|
const verified = await this.verifyServiceType(hostname, port, commonPorts[port]);
|
|
|
|
|
if (verified) {
|
|
|
|
|
return commonPorts[port];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to detect service by banner/protocol
|
|
|
|
|
return await this.detectServiceByProtocol(hostname, port);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async verifyServiceType(hostname: string, port: number, expectedType: ServiceType): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
switch (expectedType) {
|
|
|
|
|
case ServiceType.HTTP:
|
|
|
|
|
case ServiceType.HTTPS:
|
|
|
|
|
return await this.checkHttpService(hostname, port, expectedType === ServiceType.HTTPS);
|
|
|
|
|
case ServiceType.SSH:
|
|
|
|
|
return await this.checkSshService(hostname, port);
|
|
|
|
|
default:
|
|
|
|
|
return true; // For now, trust the port number for other services
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async detectServiceByProtocol(hostname: string, port: number): Promise<ServiceType> {
|
|
|
|
|
// Try HTTPS first
|
|
|
|
|
if (await this.checkHttpService(hostname, port, true)) {
|
|
|
|
|
return ServiceType.HTTPS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try HTTP
|
|
|
|
|
if (await this.checkHttpService(hostname, port, false)) {
|
|
|
|
|
return ServiceType.HTTP;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try SSH
|
|
|
|
|
if (await this.checkSshService(hostname, port)) {
|
|
|
|
|
return ServiceType.SSH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to get banner for other services
|
|
|
|
|
const banner = await this.getBanner(hostname, port);
|
|
|
|
|
if (banner) {
|
|
|
|
|
return this.identifyServiceByBanner(banner);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ServiceType.UNKNOWN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkHttpService(hostname: string, port: number, isHttps: boolean): Promise<boolean> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const protocol = isHttps ? plugins.https : plugins.http;
|
|
|
|
|
const options = {
|
|
|
|
|
hostname,
|
|
|
|
|
port,
|
|
|
|
|
method: 'HEAD',
|
|
|
|
|
timeout: 5000,
|
|
|
|
|
rejectUnauthorized: false // Accept self-signed certificates
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const req = protocol.request(options, () => {
|
|
|
|
|
resolve(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.on('error', () => {
|
|
|
|
|
resolve(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.on('timeout', () => {
|
|
|
|
|
req.destroy();
|
|
|
|
|
resolve(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
req.end();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkSshService(hostname: string, port: number): Promise<boolean> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const socket = new plugins.net.Socket();
|
|
|
|
|
|
|
|
|
|
socket.setTimeout(5000);
|
|
|
|
|
|
|
|
|
|
socket.on('data', (data) => {
|
|
|
|
|
const banner = data.toString();
|
|
|
|
|
socket.destroy();
|
|
|
|
|
// SSH banners typically start with "SSH-"
|
|
|
|
|
resolve(banner.startsWith('SSH-'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('error', () => {
|
|
|
|
|
resolve(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('timeout', () => {
|
|
|
|
|
socket.destroy();
|
|
|
|
|
resolve(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.connect(port, hostname);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getBanner(hostname: string, port: number): Promise<string | null> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const socket = new plugins.net.Socket();
|
|
|
|
|
let banner = '';
|
|
|
|
|
|
|
|
|
|
socket.setTimeout(5000);
|
|
|
|
|
|
|
|
|
|
socket.on('data', (data) => {
|
|
|
|
|
banner += data.toString();
|
|
|
|
|
socket.destroy();
|
|
|
|
|
resolve(banner);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('error', () => {
|
|
|
|
|
resolve(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('timeout', () => {
|
|
|
|
|
socket.destroy();
|
|
|
|
|
resolve(banner || null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.connect(port, hostname);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private identifyServiceByBanner(banner: string): ServiceType {
|
|
|
|
|
const bannerLower = banner.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (bannerLower.includes('ssh')) return ServiceType.SSH;
|
|
|
|
|
if (bannerLower.includes('ftp')) return ServiceType.FTP;
|
|
|
|
|
if (bannerLower.includes('smtp')) return ServiceType.SMTP;
|
|
|
|
|
if (bannerLower.includes('pop3')) return ServiceType.POP3;
|
|
|
|
|
if (bannerLower.includes('imap')) return ServiceType.IMAP;
|
|
|
|
|
if (bannerLower.includes('mysql')) return ServiceType.MYSQL;
|
|
|
|
|
if (bannerLower.includes('postgresql')) return ServiceType.POSTGRESQL;
|
|
|
|
|
if (bannerLower.includes('mongodb')) return ServiceType.MONGODB;
|
|
|
|
|
if (bannerLower.includes('redis')) return ServiceType.REDIS;
|
|
|
|
|
|
|
|
|
|
return ServiceType.UNKNOWN;
|
2021-04-13 08:30:33 +00:00
|
|
|
}
|
2022-07-16 18:15:23 +02:00
|
|
|
}
|