endpoint-sentinel
Version:
User-friendly security scanner with interactive setup that scales from beginner to expert
419 lines • 18.3 kB
JavaScript
;
/**
* Endpoint Sentinel - Main Scanner Engine
* Orchestrates the complete security scanning process
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.EndpointSentinel = void 0;
const uuid_1 = require("uuid");
const rate_limiter_js_1 = require("./rate-limiter.js");
const http_client_js_1 = require("./http-client.js");
class EndpointSentinel {
logger;
constructor(logger) {
this.logger = logger;
}
/**
* Main scan orchestration method
*/
async scan(config) {
const sessionId = (0, uuid_1.v4)();
const startTime = new Date();
this.logger.info('Starting security scan', {
sessionId,
target: config.target,
rateLimit: config.rateLimit
});
try {
// Initialize scan context
const context = await this.initializeScanContext(config, sessionId, startTime);
// Audit scan start
context.logger.audit({
eventType: 'scan_started',
timestamp: startTime,
sessionId,
target: config.target,
details: { rateLimit: config.rateLimit, keywords: config.keywords }
});
// Phase 1: Endpoint Discovery
const endpoints = await this.discoverEndpoints(config, context);
// Phase 2: Security Analysis
const findings = await this.analyzeEndpoints(endpoints, context);
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
// Generate final results
const results = {
sessionId,
target: config.target,
startTime,
endTime,
duration,
endpointsDiscovered: endpoints.length,
findings,
summary: this.generateSummary(findings),
metadata: {
version: '1.0.0',
modulesUsed: ['endpoint-discovery', 'header-analysis', 'auth-detection'],
configUsed: config,
performance: {
requestsSent: endpoints.length,
requestsPerSecond: endpoints.length / (duration / 1000),
averageResponseTime: 500, // This would be calculated from actual requests
memoryUsage: process.memoryUsage().heapUsed,
errorRate: 0.02 // This would be calculated from actual errors
}
}
};
// Audit scan completion
context.logger.audit({
eventType: 'scan_completed',
timestamp: endTime,
sessionId,
target: config.target,
details: {
duration,
endpointsFound: endpoints.length,
vulnerabilitiesFound: findings.length
}
});
this.logger.info('Scan completed successfully', {
sessionId,
duration,
endpointsFound: endpoints.length,
vulnerabilities: findings.length
});
return results;
}
catch (error) {
const endTime = new Date();
this.logger.error('Scan failed', error, { sessionId, target: config.target });
// Audit error
this.logger.audit({
eventType: 'error_occurred',
timestamp: endTime,
sessionId,
target: config.target,
details: { error: error.message }
});
throw error;
}
}
/**
* Normalizes URL by adding protocol if missing
*/
normalizeUrl(target) {
if (!target || typeof target !== 'string') {
return target;
}
// If URL already has a protocol, return as-is
if (/^https?:\/\//i.test(target)) {
return target;
}
// If it looks like a domain/hostname, prepend https://
if (/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/.*)?$/.test(target)) {
return `https://${target}`;
}
// Return as-is for other cases (will likely fail validation)
return target;
}
/**
* Validates network connectivity to target
*/
async validateConnectivity(target) {
const normalizedTarget = this.normalizeUrl(target);
const rateLimiter = new rate_limiter_js_1.TokenBucketRateLimiter(1, 1);
const cookieJar = new http_client_js_1.EnhancedCookieJar();
const httpClient = new http_client_js_1.SecurityHttpClient(cookieJar, this.logger);
const request = {
url: normalizedTarget,
method: 'HEAD',
headers: {},
timeout: 10000
};
try {
await rateLimiter.throttle(request);
const response = await httpClient.request(request);
if (response.status === 0) {
throw new Error('Network unreachable');
}
this.logger.info('Connectivity validated', { target: normalizedTarget, status: response.status });
}
catch (error) {
throw new Error(`Connectivity validation failed: ${error.message}`);
}
}
/**
* Initialize scan context with all required components
*/
async initializeScanContext(config, sessionId, startTime) {
// Create rate limiter
const rateLimiter = new rate_limiter_js_1.TokenBucketRateLimiter(config.rateLimit || 2, config.rateLimit ? Math.max(5, config.rateLimit * 2) : 5);
// Create cookie jar
const cookieJar = new http_client_js_1.EnhancedCookieJar(config.cookie);
return {
config,
sessionId,
startTime,
cookies: cookieJar,
rateLimiter,
logger: this.logger
};
}
/**
* Endpoint discovery phase - FIXED IMPLEMENTATION
* Uses practical, production-tested discovery methods
*/
async discoverEndpoints(config, context) {
this.logger.info('Starting practical endpoint discovery', { target: config.target });
// Import and use the new practical discovery engine
const { PracticalDiscoveryEngine } = await Promise.resolve().then(() => __importStar(require('./discovery-engine.js')));
const discoveryEngine = new PracticalDiscoveryEngine(new http_client_js_1.SecurityHttpClient(context.cookies, context.logger), context.rateLimiter, context.logger);
const discoveryConfig = {
maxDepth: 3, // Reasonable default
maxConcurrency: 5, // Respect rate limits
respectRobots: true,
userAgent: config.userAgent || 'EndpointSentinel/2.0',
delays: {
min: 1000, // 1 second
max: 3000 // 3 seconds
}
};
// Add keywords if they exist
if (config.keywords && config.keywords.length > 0) {
discoveryConfig.keywords = config.keywords;
}
const discoveredEndpoints = await discoveryEngine.discoverEndpoints(config.target, discoveryConfig);
// Convert to our internal Endpoint format
return discoveredEndpoints.map(ep => ({
url: ep.url,
method: ep.method,
parameters: {},
discovered: new Date(),
source: ep.source
}));
}
/**
* Security analysis phase
*/
async analyzeEndpoints(endpoints, context) {
this.logger.info('Starting security analysis', { endpoints: endpoints.length });
const findings = [];
const httpClient = new http_client_js_1.SecurityHttpClient(context.cookies, context.logger);
for (const endpoint of endpoints) {
try {
// Rate limit requests
const request = {
url: endpoint.url,
method: endpoint.method,
headers: {
'User-Agent': context.config.userAgent || 'Endpoint-Sentinel/1.0.0'
},
timeout: context.config.timeout || 30000
};
await context.rateLimiter.throttle(request);
const response = await httpClient.request(request);
// Analyze response for security issues
const endpointFindings = this.analyzeResponse(endpoint, response, context);
findings.push(...endpointFindings);
this.logger.debug('Analyzed endpoint', {
url: endpoint.url,
status: response.status,
findings: endpointFindings.length
});
}
catch (error) {
this.logger.warn('Failed to analyze endpoint', {
url: endpoint.url,
error: error.message
});
}
}
this.logger.info('Security analysis completed', {
totalFindings: findings.length,
criticalFindings: findings.filter(f => f.severity === 'critical').length
});
return findings;
}
/**
* Analyze individual response for security issues - FIXED VERSION
*/
analyzeResponse(endpoint, response, _context) {
const findings = [];
// FIXED: Include SPA routes from discovery engine in findings
// Create a basic finding for discovered endpoints so they appear in results
const isDiscoveredSPARoute = endpoint.source === 'javascript_routes';
const isKeywordEndpoint = endpoint.source === 'wordlist';
const isLikelyStaticAsset = endpoint.url.match(/\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf)$/);
const isLikelyApiEndpoint = endpoint.url.includes('/api/');
const hasRealContent = response.data && response.data.length > 100;
// Add a finding for discovered SPA routes so they appear in results
if (isDiscoveredSPARoute) {
findings.push({
id: (0, uuid_1.v4)(),
endpoint: endpoint.url,
type: 'information_disclosure',
severity: 'info',
confidence: 'certain',
title: `Discovered SPA Route: ${endpoint.url.split('/').pop()}`,
description: `This route was discovered from JavaScript bundle analysis. Status: ${response.status}`,
evidence: [{
type: 'javascript_analysis',
value: `Route found in JavaScript bundle`,
location: 'javascript_analysis'
}],
remediation: 'Verify that this route requires proper authentication and authorization.',
discoveredAt: new Date()
});
}
// Add findings for keyword-based endpoints (lower confidence)
if (isKeywordEndpoint && response.status === 200) {
findings.push({
id: (0, uuid_1.v4)(),
endpoint: endpoint.url,
type: 'information_disclosure',
severity: 'low',
confidence: 'tentative',
title: `Potential Endpoint: ${endpoint.url.split('/').pop()}`,
description: `This endpoint was discovered using keyword-based discovery. Status: ${response.status}`,
evidence: [{
type: 'content_analysis',
value: `Endpoint responsive to keyword-based discovery`,
location: 'endpoint_path'
}],
remediation: 'Verify if this endpoint should be accessible and properly secured.',
discoveredAt: new Date()
});
}
// Security header analysis for ALL responsive endpoints (not just /api/)
if (response.status === 200 && !isLikelyStaticAsset) {
// Check for missing security headers
const securityHeaders = {
'strict-transport-security': 'Missing HSTS header',
'x-content-type-options': 'Missing X-Content-Type-Options header',
'x-frame-options': 'Missing X-Frame-Options header',
'content-security-policy': 'Missing Content Security Policy'
};
for (const [header, description] of Object.entries(securityHeaders)) {
if (!response.headers[header]) {
findings.push({
id: (0, uuid_1.v4)(),
endpoint: endpoint.url,
type: 'missing_security_headers',
severity: isLikelyApiEndpoint ? 'medium' : 'low',
confidence: 'certain',
title: description,
description: `The ${header} security header is missing from this ${isDiscoveredSPARoute ? 'SPA route' : isLikelyApiEndpoint ? 'API endpoint' : 'endpoint'}.`,
evidence: [{
type: 'header_analysis',
value: `Header '${header}' not found`,
location: 'response_headers'
}],
remediation: `Add the ${header} header to improve security posture.`,
discoveredAt: new Date()
});
}
}
}
// Check for information disclosure (only if we have real content)
if (hasRealContent && response.status === 200) {
// Look for sensitive information in response
const sensitivePatterns = [
{ pattern: /password/gi, name: 'Password references' },
{ pattern: /secret/gi, name: 'Secret references' },
{ pattern: /token/gi, name: 'Token references' },
{ pattern: /api[_-]?key/gi, name: 'API key references' }
];
for (const { pattern, name } of sensitivePatterns) {
const matches = response.data.match(pattern);
if (matches && matches.length > 0) {
findings.push({
id: (0, uuid_1.v4)(),
endpoint: endpoint.url,
type: 'information_disclosure',
severity: 'medium',
confidence: 'firm',
title: `Potential ${name} in Response`,
description: `The response contains ${matches.length} potential ${name.toLowerCase()}.`,
evidence: [{
type: 'content_analysis',
value: `Found ${matches.length} matches for ${name}`,
location: 'response_body'
}],
remediation: 'Review the response content to ensure no sensitive information is exposed.',
discoveredAt: new Date()
});
}
}
}
// Check for exposed admin interfaces
if (endpoint.url.includes('/admin') && response.status === 200) {
findings.push({
id: (0, uuid_1.v4)(),
endpoint: endpoint.url,
type: 'security_misconfiguration',
severity: 'high',
confidence: 'certain',
title: 'Potentially Exposed Admin Interface',
description: 'An admin interface appears to be accessible.',
evidence: [{
type: 'content_analysis',
value: 'Admin URL returned HTTP 200',
location: 'endpoint_path'
}],
remediation: 'Ensure admin interfaces are properly protected with authentication.',
discoveredAt: new Date()
});
}
return findings;
}
/**
* Generate summary statistics
*/
generateSummary(findings) {
const summary = {
totalFindings: findings.length,
criticalFindings: findings.filter(f => f.severity === 'critical').length,
highFindings: findings.filter(f => f.severity === 'high').length,
mediumFindings: findings.filter(f => f.severity === 'medium').length,
lowFindings: findings.filter(f => f.severity === 'low').length,
infoFindings: findings.filter(f => f.severity === 'info').length,
falsePositiveRate: 0.05 // This would be calculated based on confidence scores
};
return summary;
}
}
exports.EndpointSentinel = EndpointSentinel;
//# sourceMappingURL=scanner.js.map