Files
url_tracker_tool/apps/api/dist/services/security-analyzer.service.js
Andrei 58f8093689 Rebrand from 'Redirect Intelligence v2' to 'URL Tracker Tool V2' throughout UI
- Updated all component headers and documentation
- Changed navbar and footer branding
- Updated homepage hero badge
- Modified page title in index.html
- Simplified footer text to 'Built with ❤️'
- Consistent V2 capitalization across all references
2025-08-19 19:12:23 +00:00

313 lines
13 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecurityAnalyzerService = void 0;
const axios_1 = __importDefault(require("axios"));
const url_1 = require("url");
const logger_1 = require("../lib/logger");
class SecurityAnalyzerService {
async analyzeSecurity(url, redirectChain) {
logger_1.logger.info(`Starting security analysis for: ${url}`);
const result = {
url,
flags: {
safeBrowsingStatus: 'unknown',
mixedContent: 'NONE',
httpsToHttp: false,
securityHeaders: { score: 0 },
weakCiphers: false,
openRedirects: false,
},
mixedContentAnalysis: {
status: 'none',
insecureResources: [],
httpsToHttpRedirect: false,
},
safeBrowsing: { status: 'unknown' },
vulnerabilities: [],
recommendations: [],
securityScore: 0,
};
try {
if (redirectChain && redirectChain.length > 0) {
this.analyzeRedirectChainSecurity(redirectChain, result);
}
const headersAnalysis = await this.analyzeSecurityHeaders(url);
result.flags.securityHeaders = headersAnalysis;
const mixedContentAnalysis = await this.analyzeMixedContent(url);
result.mixedContentAnalysis = mixedContentAnalysis;
result.flags.mixedContent = mixedContentAnalysis.status === 'none' ? 'NONE' :
mixedContentAnalysis.status === 'final_to_http' ? 'FINAL_TO_HTTP' : 'PRESENT';
result.flags.httpsToHttp = mixedContentAnalysis.httpsToHttpRedirect;
const safeBrowsingResult = await this.checkSafeBrowsing(url);
result.safeBrowsing = safeBrowsingResult;
result.flags.safeBrowsingStatus = safeBrowsingResult.status;
this.generateSecurityRecommendations(result);
logger_1.logger.info(`Security analysis completed for: ${url}`, {
score: result.securityScore,
safeBrowsing: result.safeBrowsing.status,
mixedContent: result.flags.mixedContent,
headersScore: result.flags.securityHeaders.score
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown security analysis error';
result.vulnerabilities.push(`Security analysis failed: ${errorMessage}`);
logger_1.logger.error(`Security analysis failed for ${url}:`, error);
}
return result;
}
analyzeRedirectChainSecurity(redirectChain, result) {
for (let i = 0; i < redirectChain.length - 1; i++) {
const currentUrl = redirectChain[i];
const nextUrl = redirectChain[i + 1];
try {
const current = new url_1.URL(currentUrl);
const next = new url_1.URL(nextUrl);
if (current.protocol === 'https:' && next.protocol === 'http:') {
result.flags.httpsToHttp = true;
result.vulnerabilities.push(`Insecure redirect: HTTPS to HTTP (${currentUrl}${nextUrl})`);
}
if (current.hostname !== next.hostname) {
if (this.isOpenRedirectVulnerable(currentUrl, nextUrl)) {
result.flags.openRedirects = true;
result.vulnerabilities.push(`Potential open redirect vulnerability: ${currentUrl}${nextUrl}`);
}
}
}
catch (error) {
logger_1.logger.warn(`Failed to parse URLs in redirect chain: ${currentUrl}${nextUrl}`);
}
}
}
isOpenRedirectVulnerable(fromUrl, toUrl) {
try {
const from = new url_1.URL(fromUrl);
const to = new url_1.URL(toUrl);
if (from.hostname !== to.hostname) {
const urlParams = new URLSearchParams(from.search);
for (const [key, value] of urlParams.entries()) {
if (key.toLowerCase().includes('url') ||
key.toLowerCase().includes('redirect') ||
key.toLowerCase().includes('return')) {
if (value.includes(to.hostname)) {
return true;
}
}
}
}
return false;
}
catch {
return false;
}
}
async analyzeSecurityHeaders(url) {
try {
const response = await axios_1.default.head(url, {
timeout: 5000,
headers: {
'User-Agent': 'RedirectIntelligence-Bot/2.0 (Security Analysis)',
},
});
const headers = response.headers;
let score = 0;
const analysis = {
score: 0,
};
if (headers['strict-transport-security']) {
analysis.strictTransportSecurity = headers['strict-transport-security'];
score += 20;
}
if (headers['content-security-policy']) {
analysis.contentSecurityPolicy = headers['content-security-policy'];
score += 25;
}
if (headers['x-frame-options']) {
analysis.xFrameOptions = headers['x-frame-options'];
score += 15;
}
if (headers['x-content-type-options']) {
analysis.xContentTypeOptions = headers['x-content-type-options'];
score += 10;
}
if (headers['referrer-policy']) {
analysis.referrerPolicy = headers['referrer-policy'];
score += 10;
}
if (headers['permissions-policy'] || headers['feature-policy']) {
analysis.permissionsPolicy = headers['permissions-policy'] || headers['feature-policy'];
score += 10;
}
analysis.score = score;
return analysis;
}
catch (error) {
logger_1.logger.warn(`Failed to analyze security headers for ${url}:`, error);
return { score: 0 };
}
}
async analyzeMixedContent(url) {
const result = {
status: 'none',
insecureResources: [],
httpsToHttpRedirect: false,
};
try {
const parsedUrl = new url_1.URL(url);
if (parsedUrl.protocol === 'http:') {
result.status = 'final_to_http';
result.httpsToHttpRedirect = true;
return result;
}
if (parsedUrl.protocol === 'https:') {
const mixedContentCheck = await this.checkPageMixedContent(url);
if (mixedContentCheck.length > 0) {
result.status = 'present';
result.insecureResources = mixedContentCheck;
}
}
}
catch (error) {
logger_1.logger.warn(`Mixed content analysis failed for ${url}:`, error);
}
return result;
}
async checkPageMixedContent(url) {
try {
const response = await axios_1.default.get(url, {
timeout: 10000,
headers: {
'User-Agent': 'RedirectIntelligence-Bot/2.0 (Mixed Content Analysis)',
},
maxContentLength: 512 * 1024,
});
const html = response.data;
const insecureResources = [];
const httpResourceRegex = /(?:src|href|action)=['"]http:\/\/[^'"]+['"]|url\(http:\/\/[^)]+\)/gi;
let match;
while ((match = httpResourceRegex.exec(html)) !== null) {
const resource = match[0];
const urlMatch = resource.match(/http:\/\/[^'")\s]+/);
if (urlMatch) {
insecureResources.push(urlMatch[0]);
}
}
return [...new Set(insecureResources)];
}
catch (error) {
logger_1.logger.warn(`Failed to check mixed content for ${url}:`, error);
return [];
}
}
async checkSafeBrowsing(url) {
try {
const parsedUrl = new url_1.URL(url);
const hostname = parsedUrl.hostname.toLowerCase();
const suspiciousPatterns = [
/phishing/i,
/malware/i,
/virus/i,
/hack/i,
/suspicious/i,
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(hostname)) {
return {
status: 'phishing',
details: 'Suspicious hostname pattern detected',
};
}
}
const suspiciousTlds = ['.tk', '.ml', '.ga', '.cf'];
const tld = hostname.substring(hostname.lastIndexOf('.'));
if (suspiciousTlds.includes(tld)) {
return {
status: 'unwanted_software',
details: 'Suspicious top-level domain',
};
}
return { status: 'safe' };
}
catch (error) {
logger_1.logger.warn(`Safe browsing check failed for ${url}:`, error);
return {
status: 'error',
details: 'Safe browsing check failed',
};
}
}
generateSecurityRecommendations(result) {
let score = 100;
if (result.safeBrowsing.status === 'malware' || result.safeBrowsing.status === 'phishing') {
result.vulnerabilities.push(`Site flagged as ${result.safeBrowsing.status}`);
result.recommendations.push('Immediately investigate and resolve security issues');
score -= 50;
}
else if (result.safeBrowsing.status === 'unwanted_software') {
result.vulnerabilities.push('Site may contain unwanted software');
result.recommendations.push('Review site content and remove unwanted software');
score -= 30;
}
if (result.flags.httpsToHttp) {
result.vulnerabilities.push('HTTPS to HTTP downgrade detected');
result.recommendations.push('Ensure all redirects maintain HTTPS encryption');
score -= 25;
}
if (result.mixedContentAnalysis.insecureResources.length > 0) {
result.vulnerabilities.push(`${result.mixedContentAnalysis.insecureResources.length} insecure resources found`);
result.recommendations.push('Update all HTTP resources to use HTTPS');
score -= 15;
}
const headersScore = result.flags.securityHeaders.score;
if (headersScore < 50) {
result.recommendations.push('Implement security headers (CSP, HSTS, X-Frame-Options)');
score -= 20;
}
else if (headersScore < 80) {
result.recommendations.push('Add additional security headers for better protection');
score -= 10;
}
if (result.flags.openRedirects) {
result.vulnerabilities.push('Potential open redirect vulnerability detected');
result.recommendations.push('Validate and restrict redirect destinations');
score -= 20;
}
score = Math.min(score, score + (headersScore * 0.2));
result.securityScore = Math.max(0, score);
if (result.securityScore >= 90) {
result.recommendations.push('Excellent security posture!');
}
else if (result.securityScore >= 70) {
result.recommendations.push('Good security with minor improvements possible');
}
else if (result.securityScore >= 50) {
result.recommendations.push('Security needs improvement for better protection');
}
else {
result.recommendations.push('Security requires immediate attention');
}
}
async quickSecurityCheck(url) {
try {
const analysis = await this.analyzeSecurity(url);
return {
httpsToHttp: analysis.flags.httpsToHttp,
safeBrowsingStatus: analysis.flags.safeBrowsingStatus,
hasSecurityHeaders: analysis.flags.securityHeaders.score > 0,
};
}
catch (error) {
logger_1.logger.warn(`Quick security check failed for ${url}:`, error);
return {
httpsToHttp: false,
safeBrowsingStatus: 'unknown',
hasSecurityHeaders: false,
};
}
}
}
exports.SecurityAnalyzerService = SecurityAnalyzerService;
//# sourceMappingURL=security-analyzer.service.js.map