Prerequisites
- Strong TypeScript fundamentals π
- Understanding of security concepts π
- Experience with TypeScript projects ποΈ
- Basic knowledge of CI/CD pipelines π
What you'll learn
- Set up static analysis tools for security π‘οΈ
- Identify common security vulnerabilities π
- Implement automated security checks β‘
- Create security-focused code reviews π
π― Introduction
Welcome to the world of code security reviews! π In todayβs digital landscape, security vulnerabilities can have devastating consequences. Static analysis tools are your first line of defense, helping you catch security issues before they reach production.
In this tutorial, weβll explore how to implement comprehensive security reviews using static analysis tools specifically designed for TypeScript projects. Get ready to become a security champion! πͺ
π Understanding Static Security Analysis
Think of static analysis as having a tireless security expert reviewing every line of your code 24/7! π€ These tools analyze your source code without executing it, identifying potential vulnerabilities based on patterns and rules.
Why Static Analysis for Security? π€
// π¨ Without static analysis, these vulnerabilities might slip through:
// β SQL Injection vulnerability
const getUserData = (userId: string) => {
const query = `SELECT * FROM users WHERE id = '${userId}'`; // π₯ Dangerous!
return database.execute(query);
};
// β XSS vulnerability
const displayMessage = (message: string) => {
document.innerHTML = message; // π¨ Unescaped input!
};
// β Path traversal vulnerability
const readFile = (filename: string) => {
return fs.readFileSync(`./uploads/${filename}`); // π No validation!
};
π§ Essential Security Analysis Tools
Letβs set up a comprehensive security toolkit for TypeScript! π οΈ
1. ESLint Security Plugin
# π¦ Install security-focused ESLint plugins
npm install --save-dev @typescript-eslint/eslint-plugin eslint-plugin-security
# π§ Configure .eslintrc.json
{
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:security/recommended"
],
"plugins": ["@typescript-eslint", "security"],
"rules": {
// π‘οΈ Security rules
"security/detect-object-injection": "error",
"security/detect-non-literal-regexp": "error",
"security/detect-unsafe-regex": "error",
"security/detect-buffer-noassert": "error",
"security/detect-child-process": "warn",
"security/detect-disable-mustache-escape": "error",
"security/detect-eval-with-expression": "error",
"security/detect-no-csrf-before-method-override": "error",
"security/detect-non-literal-fs-filename": "warn",
"security/detect-non-literal-require": "warn",
"security/detect-possible-timing-attacks": "warn"
}
}
2. Semgrep for TypeScript
# π Install Semgrep
pip install semgrep
# π Create .semgrep.yml configuration
# .semgrep.yml
rules:
- id: typescript-sql-injection
patterns:
- pattern: |
$QUERY = `... ${$USER_INPUT} ...`
$DB.$METHOD($QUERY)
message: "Potential SQL injection vulnerability"
languages: [typescript]
severity: ERROR
- id: typescript-xss
patterns:
- pattern: |
$ELEMENT.innerHTML = $USER_INPUT
message: "Potential XSS vulnerability"
languages: [typescript]
severity: ERROR
- id: typescript-hardcoded-secret
patterns:
- pattern-regex: '(password|secret|api[_-]?key)\s*[:=]\s*["\'][^"\']+["\']'
message: "Hardcoded secret detected"
languages: [typescript]
severity: ERROR
3. SonarQube with TypeScript Support
// π sonar-project.properties
sonar.projectKey=my-typescript-project
sonar.sources=src
sonar.exclusions=**/*.test.ts,**/*.spec.ts
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.javascript.globals=angular,goog,google,moment,JSON
π‘ Practical Security Analysis Examples
Example 1: Authentication Vulnerability Scanner π
// π security-scanner.ts
import { ESLint } from 'eslint';
import * as fs from 'fs';
import * as path from 'path';
interface SecurityIssue {
file: string;
line: number;
column: number;
severity: 'low' | 'medium' | 'high' | 'critical';
message: string;
rule: string;
}
class SecurityScanner {
private eslint: ESLint;
private customRules: Map<string, RegExp>;
constructor() {
// π§ Initialize ESLint with security configuration
this.eslint = new ESLint({
overrideConfig: {
extends: ['plugin:security/recommended'],
plugins: ['security']
}
});
// π― Custom security patterns
this.customRules = new Map([
['hardcoded-jwt-secret', /jwt\.sign\([^,]+,\s*["'][^"']+["']/g],
['weak-crypto', /crypto\.createHash\(['"]md5['"]\)/g],
['unsafe-random', /Math\.random\(\)/g],
['no-rate-limiting', /app\.(get|post|put|delete)\([^)]+\)\s*(?!.*rateLimit)/g]
]);
}
async scanProject(projectPath: string): Promise<SecurityIssue[]> {
const issues: SecurityIssue[] = [];
// π Run ESLint security checks
const results = await this.eslint.lintFiles([`${projectPath}/**/*.ts`]);
for (const result of results) {
for (const message of result.messages) {
if (message.ruleId?.includes('security')) {
issues.push({
file: result.filePath,
line: message.line,
column: message.column,
severity: this.mapSeverity(message.severity),
message: message.message,
rule: message.ruleId
});
}
}
}
// π― Run custom security checks
const customIssues = await this.runCustomChecks(projectPath);
issues.push(...customIssues);
return issues;
}
private async runCustomChecks(projectPath: string): Promise<SecurityIssue[]> {
const issues: SecurityIssue[] = [];
const files = await this.getTypeScriptFiles(projectPath);
for (const file of files) {
const content = await fs.promises.readFile(file, 'utf-8');
const lines = content.split('\n');
// π Check each custom rule
for (const [ruleName, pattern] of this.customRules) {
const matches = content.matchAll(pattern);
for (const match of matches) {
const position = this.getLineAndColumn(content, match.index!);
issues.push({
file,
line: position.line,
column: position.column,
severity: this.getRuleSeverity(ruleName),
message: this.getRuleMessage(ruleName),
rule: ruleName
});
}
}
}
return issues;
}
private mapSeverity(eslintSeverity: number): SecurityIssue['severity'] {
return eslintSeverity === 2 ? 'high' : 'medium';
}
private getRuleSeverity(ruleName: string): SecurityIssue['severity'] {
const criticalRules = ['hardcoded-jwt-secret', 'weak-crypto'];
return criticalRules.includes(ruleName) ? 'critical' : 'high';
}
private getRuleMessage(ruleName: string): string {
const messages: Record<string, string> = {
'hardcoded-jwt-secret': 'JWT secret should not be hardcoded',
'weak-crypto': 'MD5 is cryptographically broken, use SHA-256 or stronger',
'unsafe-random': 'Math.random() is not cryptographically secure',
'no-rate-limiting': 'API endpoint lacks rate limiting'
};
return messages[ruleName] || 'Security issue detected';
}
private getLineAndColumn(content: string, index: number): { line: number; column: number } {
const lines = content.substring(0, index).split('\n');
return {
line: lines.length,
column: lines[lines.length - 1].length + 1
};
}
private async getTypeScriptFiles(dir: string): Promise<string[]> {
const files: string[] = [];
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.includes('node_modules')) {
files.push(...await this.getTypeScriptFiles(fullPath));
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
files.push(fullPath);
}
}
return files;
}
}
// π Usage example
const scanner = new SecurityScanner();
scanner.scanProject('./src').then(issues => {
console.log(`π Found ${issues.length} security issues`);
// π Group by severity
const bySeverity = issues.reduce((acc, issue) => {
acc[issue.severity] = (acc[issue.severity] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log('π Issues by severity:', bySeverity);
// π― Show critical issues
const critical = issues.filter(i => i.severity === 'critical');
if (critical.length > 0) {
console.log('\nπ¨ Critical issues:');
critical.forEach(issue => {
console.log(` ${issue.file}:${issue.line} - ${issue.message}`);
});
}
});
Example 2: Input Validation Analyzer π‘οΈ
// π input-validation-analyzer.ts
import * as ts from 'typescript';
interface ValidationIssue {
fileName: string;
line: number;
functionName: string;
parameter: string;
issue: string;
}
class InputValidationAnalyzer {
private program: ts.Program;
private checker: ts.TypeChecker;
constructor(files: string[]) {
// π§ Create TypeScript program for analysis
this.program = ts.createProgram(files, {
noEmit: true,
allowJs: false,
target: ts.ScriptTarget.ES2020
});
this.checker = this.program.getTypeChecker();
}
analyzeValidation(): ValidationIssue[] {
const issues: ValidationIssue[] = [];
// π Analyze each source file
for (const sourceFile of this.program.getSourceFiles()) {
if (sourceFile.isDeclarationFile) continue;
ts.forEachChild(sourceFile, node => {
this.visitNode(node, sourceFile, issues);
});
}
return issues;
}
private visitNode(node: ts.Node, sourceFile: ts.SourceFile, issues: ValidationIssue[]): void {
// π― Look for function declarations
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
this.analyzeFunctionValidation(node, sourceFile, issues);
}
// π Recursively visit child nodes
ts.forEachChild(node, child => this.visitNode(child, sourceFile, issues));
}
private analyzeFunctionValidation(
node: ts.FunctionDeclaration | ts.MethodDeclaration,
sourceFile: ts.SourceFile,
issues: ValidationIssue[]
): void {
const functionName = node.name?.getText() || 'anonymous';
// π Check if function handles user input
if (this.isUserInputHandler(node)) {
const hasValidation = this.checkForValidation(node);
if (!hasValidation) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
// π¨ Check each parameter
node.parameters.forEach(param => {
const paramName = param.name.getText();
const paramType = this.checker.getTypeAtLocation(param);
if (this.needsValidation(paramType)) {
issues.push({
fileName: sourceFile.fileName,
line: line + 1,
functionName,
parameter: paramName,
issue: 'User input parameter lacks validation'
});
}
});
}
}
}
private isUserInputHandler(node: ts.FunctionDeclaration | ts.MethodDeclaration): boolean {
const name = node.name?.getText() || '';
// π― Common patterns for user input handlers
const patterns = [
/^handle/i,
/^process/i,
/^submit/i,
/^save/i,
/^create/i,
/^update/i,
/Controller$/,
/Route$/,
/^api/i
];
return patterns.some(pattern => pattern.test(name));
}
private checkForValidation(node: ts.FunctionDeclaration | ts.MethodDeclaration): boolean {
let hasValidation = false;
const checkNode = (n: ts.Node): void => {
// π Look for validation patterns
if (ts.isCallExpression(n)) {
const expression = n.expression.getText();
const validationPatterns = [
/validate/i,
/sanitize/i,
/escape/i,
/check/i,
/assert/i,
/\.trim\(\)/,
/\.test\(/,
/joi\./,
/yup\./,
/zod\./,
/class-validator/
];
if (validationPatterns.some(pattern => pattern.test(expression))) {
hasValidation = true;
}
}
// π Check for if statements that might be validation
if (ts.isIfStatement(n)) {
const condition = n.expression.getText();
if (/length|match|test|includes/.test(condition)) {
hasValidation = true;
}
}
ts.forEachChild(n, checkNode);
};
if (node.body) {
checkNode(node.body);
}
return hasValidation;
}
private needsValidation(type: ts.Type): boolean {
const typeString = this.checker.typeToString(type);
// π― Types that typically need validation
const needsValidationTypes = [
'string',
'any',
'unknown',
'object',
'Record<'
];
return needsValidationTypes.some(t => typeString.includes(t));
}
}
// π Usage
const analyzer = new InputValidationAnalyzer(['./src/**/*.ts']);
const validationIssues = analyzer.analyzeValidation();
if (validationIssues.length > 0) {
console.log('β οΈ Input validation issues found:');
validationIssues.forEach(issue => {
console.log(` ${issue.fileName}:${issue.line} - ${issue.functionName}(${issue.parameter}) - ${issue.issue}`);
});
}
Example 3: Dependency Security Audit π¦
// π dependency-security-audit.ts
import { execSync } from 'child_process';
import * as fs from 'fs';
interface Vulnerability {
id: string;
severity: 'low' | 'moderate' | 'high' | 'critical';
title: string;
packageName: string;
installedVersion: string;
fixedVersion?: string;
path: string[];
}
class DependencySecurityAudit {
private knownVulnerablePackages = new Set([
'event-stream', // π¨ Known malicious package
'flatmap-stream', // π¨ Known malicious package
'left-pad' // π¦ Famously problematic
]);
async runAudit(): Promise<{
vulnerabilities: Vulnerability[];
statistics: Record<string, number>;
recommendations: string[];
}> {
const vulnerabilities: Vulnerability[] = [];
// π Run npm audit
try {
const auditOutput = execSync('npm audit --json', { encoding: 'utf-8' });
const auditData = JSON.parse(auditOutput);
// π― Process vulnerabilities
if (auditData.vulnerabilities) {
for (const [pkgName, vulnData] of Object.entries(auditData.vulnerabilities as any)) {
vulnData.via.forEach((via: any) => {
if (typeof via === 'object') {
vulnerabilities.push({
id: via.id || 'unknown',
severity: via.severity,
title: via.title,
packageName: pkgName,
installedVersion: vulnData.version,
fixedVersion: vulnData.fixedIn?.[0],
path: vulnData.paths?.[0] || []
});
}
});
}
}
} catch (error) {
console.warn('β οΈ npm audit failed, trying alternative methods...');
}
// π Check for known vulnerable packages
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
for (const [pkg, version] of Object.entries(allDeps)) {
if (this.knownVulnerablePackages.has(pkg)) {
vulnerabilities.push({
id: 'known-vulnerable',
severity: 'critical',
title: `Known vulnerable package: ${pkg}`,
packageName: pkg,
installedVersion: version as string,
path: ['direct']
});
}
}
// π Generate statistics
const statistics = this.generateStatistics(vulnerabilities);
// π‘ Generate recommendations
const recommendations = this.generateRecommendations(vulnerabilities, allDeps);
return { vulnerabilities, statistics, recommendations };
}
private generateStatistics(vulnerabilities: Vulnerability[]): Record<string, number> {
const stats: Record<string, number> = {
total: vulnerabilities.length,
critical: 0,
high: 0,
moderate: 0,
low: 0
};
vulnerabilities.forEach(vuln => {
stats[vuln.severity]++;
});
return stats;
}
private generateRecommendations(
vulnerabilities: Vulnerability[],
dependencies: Record<string, string>
): string[] {
const recommendations: string[] = [];
// π¨ Critical vulnerabilities
const criticalVulns = vulnerabilities.filter(v => v.severity === 'critical');
if (criticalVulns.length > 0) {
recommendations.push(
`π¨ URGENT: Fix ${criticalVulns.length} critical vulnerabilities immediately!`
);
criticalVulns.forEach(vuln => {
if (vuln.fixedVersion) {
recommendations.push(
` β Update ${vuln.packageName} to ${vuln.fixedVersion} or higher`
);
} else {
recommendations.push(
` β Remove or replace ${vuln.packageName} (no fix available)`
);
}
});
}
// π Check for outdated packages
const majorOutdated = this.checkOutdatedPackages(dependencies);
if (majorOutdated.length > 0) {
recommendations.push(
`π¦ Consider updating ${majorOutdated.length} packages with major version updates`
);
}
// π‘οΈ Security best practices
recommendations.push(
'π‘οΈ Security best practices:',
' β Run npm audit fix regularly',
' β Use npm audit in CI/CD pipeline',
' β Consider using Snyk or similar tools',
' β Review and minimize dependencies',
' β Use lockfiles (package-lock.json)'
);
return recommendations;
}
private checkOutdatedPackages(dependencies: Record<string, string>): string[] {
// π This is a simplified check - in real world, use npm outdated
const outdated: string[] = [];
for (const [pkg, version] of Object.entries(dependencies)) {
// Check if using very old syntax (e.g., ~0.x.x or ^1.x.x for major packages)
if (/^[~^]?[0-1]\./.test(version) && this.isCommonPackage(pkg)) {
outdated.push(pkg);
}
}
return outdated;
}
private isCommonPackage(pkg: string): boolean {
const commonPackages = [
'react', 'vue', 'angular', 'express', 'lodash',
'axios', 'moment', 'webpack', 'babel', 'eslint'
];
return commonPackages.some(common => pkg.includes(common));
}
}
// π Run security audit
const audit = new DependencySecurityAudit();
audit.runAudit().then(results => {
console.log('\nπ Dependency Security Audit Report\n');
console.log('π Statistics:', results.statistics);
if (results.vulnerabilities.length > 0) {
console.log('\nβ οΈ Vulnerabilities found:');
results.vulnerabilities
.sort((a, b) => {
const severityOrder = { critical: 0, high: 1, moderate: 2, low: 3 };
return severityOrder[a.severity] - severityOrder[b.severity];
})
.forEach(vuln => {
const icon = vuln.severity === 'critical' ? 'π¨' :
vuln.severity === 'high' ? 'β οΈ' :
vuln.severity === 'moderate' ? 'β‘' : 'π‘';
console.log(`${icon} [${vuln.severity.toUpperCase()}] ${vuln.packageName}@${vuln.installedVersion}`);
console.log(` ${vuln.title}`);
if (vuln.fixedVersion) {
console.log(` β
Fix available: upgrade to ${vuln.fixedVersion}`);
}
});
}
console.log('\nπ‘ Recommendations:');
results.recommendations.forEach(rec => console.log(rec));
});
π Advanced Security Analysis Techniques
Taint Analysis for Data Flow π
// π taint-analysis.ts
import * as ts from 'typescript';
class TaintAnalysis {
private sourceFile: ts.SourceFile;
private checker: ts.TypeChecker;
private taintedVariables = new Set<string>();
private sinkFunctions = new Set([
'eval',
'innerHTML',
'outerHTML',
'document.write',
'execSync',
'exec',
'spawn',
'query', // Database queries
'execute' // Database execution
]);
constructor(program: ts.Program, sourceFile: ts.SourceFile) {
this.sourceFile = sourceFile;
this.checker = program.getTypeChecker();
}
analyze(): Array<{ line: number; message: string }> {
const issues: Array<{ line: number; message: string }> = [];
// π First pass: identify tainted sources
this.identifyTaintedSources(this.sourceFile);
// π Second pass: check for tainted data reaching sinks
this.checkTaintFlow(this.sourceFile, issues);
return issues;
}
private identifyTaintedSources(node: ts.Node): void {
// π― Common taint sources
if (ts.isPropertyAccessExpression(node)) {
const text = node.getText();
const taintSources = [
'req.body',
'req.query',
'req.params',
'req.headers',
'process.argv',
'location.search',
'window.location',
'document.URL'
];
if (taintSources.some(source => text.includes(source))) {
// Mark variable as tainted
const parent = node.parent;
if (ts.isVariableDeclaration(parent)) {
const varName = parent.name.getText();
this.taintedVariables.add(varName);
}
}
}
ts.forEachChild(node, child => this.identifyTaintedSources(child));
}
private checkTaintFlow(node: ts.Node, issues: Array<{ line: number; message: string }>): void {
// π¨ Check if tainted data reaches dangerous sinks
if (ts.isCallExpression(node)) {
const funcName = node.expression.getText();
if (this.isSinkFunction(funcName)) {
// Check if any argument is tainted
node.arguments.forEach(arg => {
if (this.isTainted(arg)) {
const { line } = this.sourceFile.getLineAndCharacterOfPosition(node.getStart());
issues.push({
line: line + 1,
message: `Tainted data flows to dangerous sink: ${funcName}`
});
}
});
}
}
ts.forEachChild(node, child => this.checkTaintFlow(child, issues));
}
private isSinkFunction(funcName: string): boolean {
return Array.from(this.sinkFunctions).some(sink => funcName.includes(sink));
}
private isTainted(node: ts.Node): boolean {
if (ts.isIdentifier(node)) {
return this.taintedVariables.has(node.getText());
}
// π Check for tainted property access
if (ts.isPropertyAccessExpression(node)) {
return this.isTainted(node.expression);
}
// π Check for tainted template literals
if (ts.isTemplateExpression(node)) {
return node.templateSpans.some(span => this.isTainted(span.expression));
}
return false;
}
}
β οΈ Common Security Pitfalls and Solutions
1. Hardcoded Secrets π
// β WRONG: Hardcoded secrets
const config = {
apiKey: 'sk_live_abcd1234efgh5678', // π¨ Never do this!
dbPassword: 'super_secret_password' // π₯ Security disaster!
};
// β
CORRECT: Use environment variables
const config = {
apiKey: process.env.API_KEY || '',
dbPassword: process.env.DB_PASSWORD || ''
};
// π‘οΈ Even better: Use a secrets manager
import { SecretsManager } from 'aws-sdk';
class SecureConfig {
private secretsManager = new SecretsManager();
async getApiKey(): Promise<string> {
const secret = await this.secretsManager.getSecretValue({
SecretId: 'api-keys/main'
}).promise();
return JSON.parse(secret.SecretString!).apiKey;
}
}
2. Injection Vulnerabilities π
// β WRONG: SQL injection vulnerability
const getUser = async (userId: string) => {
const query = `SELECT * FROM users WHERE id = '${userId}'`;
return db.query(query); // π¨ Vulnerable to SQL injection!
};
// β
CORRECT: Use parameterized queries
const getUser = async (userId: string) => {
const query = 'SELECT * FROM users WHERE id = $1';
return db.query(query, [userId]); // π‘οΈ Safe from injection
};
// π― Using query builders for extra safety
import { QueryBuilder } from 'typeorm';
const getUser = async (userId: string) => {
return await userRepository
.createQueryBuilder('user')
.where('user.id = :id', { id: userId })
.getOne();
};
3. Insecure Random Values π²
// β WRONG: Math.random() for security
const generateToken = () => {
return Math.random().toString(36).substring(2); // π¨ Predictable!
};
// β
CORRECT: Use crypto for randomness
import { randomBytes } from 'crypto';
const generateToken = (): string => {
return randomBytes(32).toString('hex'); // π‘οΈ Cryptographically secure
};
// π― For UUIDs
import { randomUUID } from 'crypto';
const generateId = (): string => {
return randomUUID(); // π Secure UUID v4
};
π οΈ Best Practices for Security Reviews
1. Automated Security Pipeline π
// π security-pipeline.ts
import { execSync } from 'child_process';
class SecurityPipeline {
async run(): Promise<boolean> {
console.log('π Starting security pipeline...\n');
const steps = [
{
name: 'TypeScript Strict Mode',
command: 'tsc --noEmit --strict',
critical: true
},
{
name: 'ESLint Security',
command: 'eslint . --ext .ts,.tsx',
critical: true
},
{
name: 'Dependency Audit',
command: 'npm audit --audit-level=moderate',
critical: false
},
{
name: 'License Check',
command: 'license-checker --onlyAllow "MIT;Apache-2.0;BSD"',
critical: false
},
{
name: 'Secrets Scan',
command: 'truffleHog --regex --entropy=False .',
critical: true
}
];
let allPassed = true;
for (const step of steps) {
console.log(`π Running: ${step.name}`);
try {
execSync(step.command, { stdio: 'inherit' });
console.log(`β
${step.name} passed\n`);
} catch (error) {
console.log(`β ${step.name} failed\n`);
if (step.critical) {
allPassed = false;
}
}
}
return allPassed;
}
}
// π Run in CI/CD
const pipeline = new SecurityPipeline();
pipeline.run().then(success => {
if (!success) {
console.error('π¨ Security pipeline failed!');
process.exit(1);
}
console.log('β
All security checks passed!');
});
2. Security Review Checklist π
// π security-checklist.ts
interface SecurityCheckItem {
category: string;
checks: Array<{
item: string;
automated: boolean;
severity: 'low' | 'medium' | 'high' | 'critical';
}>;
}
const securityChecklist: SecurityCheckItem[] = [
{
category: 'π Authentication & Authorization',
checks: [
{ item: 'No hardcoded credentials', automated: true, severity: 'critical' },
{ item: 'Proper session management', automated: false, severity: 'high' },
{ item: 'Rate limiting implemented', automated: true, severity: 'medium' },
{ item: 'RBAC properly configured', automated: false, severity: 'high' }
]
},
{
category: 'π Input Validation',
checks: [
{ item: 'All inputs validated', automated: true, severity: 'high' },
{ item: 'SQL injection prevention', automated: true, severity: 'critical' },
{ item: 'XSS prevention', automated: true, severity: 'critical' },
{ item: 'File upload restrictions', automated: false, severity: 'high' }
]
},
{
category: 'π Cryptography',
checks: [
{ item: 'Strong algorithms used', automated: true, severity: 'high' },
{ item: 'Secure random generation', automated: true, severity: 'high' },
{ item: 'Proper key management', automated: false, severity: 'critical' },
{ item: 'TLS properly configured', automated: false, severity: 'high' }
]
},
{
category: 'π¦ Dependencies',
checks: [
{ item: 'No known vulnerabilities', automated: true, severity: 'high' },
{ item: 'Licenses reviewed', automated: true, severity: 'low' },
{ item: 'Minimal dependencies', automated: false, severity: 'medium' },
{ item: 'Lock files committed', automated: true, severity: 'medium' }
]
}
];
// π― Generate review report
function generateSecurityReport(checklist: SecurityCheckItem[]): void {
console.log('π Security Review Checklist\n');
let totalChecks = 0;
let automatedChecks = 0;
checklist.forEach(category => {
console.log(`\n${category.category}`);
category.checks.forEach(check => {
totalChecks++;
if (check.automated) automatedChecks++;
const icon = check.automated ? 'π€' : 'π€';
const severity = check.severity.toUpperCase();
console.log(` ${icon} [${severity}] ${check.item}`);
});
});
console.log(`\nπ Summary: ${automatedChecks}/${totalChecks} checks automated`);
console.log('π‘ Tip: Automate more checks to improve coverage!');
}
generateSecurityReport(securityChecklist);
π§ͺ Hands-On Exercise: Build Your Security Scanner
Your challenge: Create a comprehensive security scanner that combines multiple analysis techniques! π―
The Challenge π
Build a security scanner that:
- Detects hardcoded secrets using regex patterns
- Identifies SQL injection vulnerabilities
- Finds insecure randomness usage
- Checks for missing input validation
- Generates a security report with severity levels
π‘ Solution
// π comprehensive-security-scanner.ts
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
interface SecurityFinding {
type: 'secret' | 'injection' | 'crypto' | 'validation';
severity: 'low' | 'medium' | 'high' | 'critical';
file: string;
line: number;
message: string;
code?: string;
}
class ComprehensiveSecurityScanner {
private findings: SecurityFinding[] = [];
// π Regex patterns for secrets
private secretPatterns = [
{ pattern: /api[_-]?key\s*[:=]\s*["'][^"']{20,}["']/gi, type: 'API Key' },
{ pattern: /secret\s*[:=]\s*["'][^"']{10,}["']/gi, type: 'Secret' },
{ pattern: /password\s*[:=]\s*["'][^"']+["']/gi, type: 'Password' },
{ pattern: /token\s*[:=]\s*["'][^"']{20,}["']/gi, type: 'Token' },
{ pattern: /private[_-]?key\s*[:=]\s*["'][^"']+["']/gi, type: 'Private Key' }
];
// π― SQL injection patterns
private sqlInjectionPatterns = [
/query\s*\([`"'].*\$\{[^}]+\}.*[`"']\)/g,
/execute\s*\([`"'].*\+.*[`"']\)/g,
/\.query\s*\(\s*[`"'].*\+/g
];
async scanDirectory(dirPath: string): Promise<SecurityFinding[]> {
this.findings = [];
await this.walkDirectory(dirPath);
return this.findings;
}
private async walkDirectory(dir: string): Promise<void> {
const files = await fs.promises.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = await fs.promises.stat(filePath);
if (stat.isDirectory() && !file.includes('node_modules')) {
await this.walkDirectory(filePath);
} else if (file.endsWith('.ts') || file.endsWith('.tsx')) {
await this.scanFile(filePath);
}
}
}
private async scanFile(filePath: string): Promise<void> {
const content = await fs.promises.readFile(filePath, 'utf-8');
const lines = content.split('\n');
// π Scan for hardcoded secrets
this.scanForSecrets(content, filePath, lines);
// π Scan for SQL injections
this.scanForSQLInjection(content, filePath, lines);
// π Scan for weak crypto
this.scanForWeakCrypto(content, filePath, lines);
// π Advanced TypeScript analysis
this.performTypeScriptAnalysis(filePath, content);
}
private scanForSecrets(content: string, filePath: string, lines: string[]): void {
for (const { pattern, type } of this.secretPatterns) {
const matches = content.matchAll(pattern);
for (const match of matches) {
const lineNumber = this.getLineNumber(content, match.index!);
this.findings.push({
type: 'secret',
severity: 'critical',
file: filePath,
line: lineNumber,
message: `Hardcoded ${type} detected`,
code: lines[lineNumber - 1].trim()
});
}
}
}
private scanForSQLInjection(content: string, filePath: string, lines: string[]): void {
for (const pattern of this.sqlInjectionPatterns) {
const matches = content.matchAll(pattern);
for (const match of matches) {
const lineNumber = this.getLineNumber(content, match.index!);
this.findings.push({
type: 'injection',
severity: 'critical',
file: filePath,
line: lineNumber,
message: 'Potential SQL injection vulnerability',
code: lines[lineNumber - 1].trim()
});
}
}
}
private scanForWeakCrypto(content: string, filePath: string, lines: string[]): void {
const weakCryptoPatterns = [
{ pattern: /Math\.random\(\)/g, message: 'Insecure randomness: Math.random()' },
{ pattern: /createHash\(['"]md5['"]\)/g, message: 'Weak hash algorithm: MD5' },
{ pattern: /createHash\(['"]sha1['"]\)/g, message: 'Weak hash algorithm: SHA1' }
];
for (const { pattern, message } of weakCryptoPatterns) {
const matches = content.matchAll(pattern);
for (const match of matches) {
const lineNumber = this.getLineNumber(content, match.index!);
this.findings.push({
type: 'crypto',
severity: 'high',
file: filePath,
line: lineNumber,
message,
code: lines[lineNumber - 1].trim()
});
}
}
}
private performTypeScriptAnalysis(filePath: string, content: string): void {
const sourceFile = ts.createSourceFile(
filePath,
content,
ts.ScriptTarget.Latest,
true
);
const visit = (node: ts.Node): void => {
// π Check for missing validation
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
const name = node.name?.getText() || '';
if (/^handle|^process|^api/i.test(name)) {
const hasValidation = this.checkForValidation(node);
if (!hasValidation) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
this.findings.push({
type: 'validation',
severity: 'medium',
file: filePath,
line: line + 1,
message: `Function '${name}' may lack input validation`
});
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
private checkForValidation(node: ts.FunctionDeclaration | ts.MethodDeclaration): boolean {
let hasValidation = false;
const checkNode = (n: ts.Node): void => {
if (ts.isCallExpression(n)) {
const text = n.expression.getText();
if (/validate|check|assert|test/.test(text)) {
hasValidation = true;
}
}
ts.forEachChild(n, checkNode);
};
if (node.body) checkNode(node.body);
return hasValidation;
}
private getLineNumber(content: string, index: number): number {
return content.substring(0, index).split('\n').length;
}
generateReport(): string {
const report: string[] = ['# π Security Scan Report\n'];
// π Summary
const summary = {
total: this.findings.length,
critical: this.findings.filter(f => f.severity === 'critical').length,
high: this.findings.filter(f => f.severity === 'high').length,
medium: this.findings.filter(f => f.severity === 'medium').length,
low: this.findings.filter(f => f.severity === 'low').length
};
report.push('## π Summary\n');
report.push(`- Total findings: ${summary.total}`);
report.push(`- Critical: ${summary.critical} π¨`);
report.push(`- High: ${summary.high} β οΈ`);
report.push(`- Medium: ${summary.medium} β‘`);
report.push(`- Low: ${summary.low} π‘\n`);
// π― Findings by type
const byType = this.findings.reduce((acc, f) => {
acc[f.type] = (acc[f.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
report.push('## π― Findings by Type\n');
Object.entries(byType).forEach(([type, count]) => {
report.push(`- ${type}: ${count}`);
});
report.push('');
// π Detailed findings
report.push('## π Detailed Findings\n');
const sortedFindings = [...this.findings].sort((a, b) => {
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
return severityOrder[a.severity] - severityOrder[b.severity];
});
sortedFindings.forEach((finding, index) => {
const icon = finding.severity === 'critical' ? 'π¨' :
finding.severity === 'high' ? 'β οΈ' :
finding.severity === 'medium' ? 'β‘' : 'π‘';
report.push(`### ${index + 1}. ${icon} ${finding.message}`);
report.push(`- **File**: ${finding.file}`);
report.push(`- **Line**: ${finding.line}`);
report.push(`- **Severity**: ${finding.severity.toUpperCase()}`);
report.push(`- **Type**: ${finding.type}`);
if (finding.code) {
report.push(`- **Code**: \`${finding.code}\``);
}
report.push('');
});
// π‘ Recommendations
report.push('## π‘ Recommendations\n');
if (summary.critical > 0) {
report.push('1. **URGENT**: Address all critical findings immediately!');
}
report.push('2. Implement automated security scanning in CI/CD');
report.push('3. Use environment variables for all secrets');
report.push('4. Enable TypeScript strict mode');
report.push('5. Regular dependency updates and audits');
report.push('6. Code review all security-sensitive changes');
return report.join('\n');
}
}
// π Usage
async function runSecurityScan() {
const scanner = new ComprehensiveSecurityScanner();
console.log('π Starting security scan...\n');
const findings = await scanner.scanDirectory('./src');
const report = scanner.generateReport();
// πΎ Save report
await fs.promises.writeFile('security-report.md', report);
console.log('π Report saved to security-report.md');
// π― Exit with error if critical findings
const criticalCount = findings.filter(f => f.severity === 'critical').length;
if (criticalCount > 0) {
console.error(`\nπ¨ Found ${criticalCount} critical security issues!`);
process.exit(1);
}
console.log('\nβ
Security scan completed!');
}
runSecurityScan().catch(console.error);
π Key Takeaways
Youβve mastered code security reviews with static analysis! Hereβs what youβve learned:
π Static Analysis Tools - ESLint, Semgrep, and custom analyzers for security π‘οΈ Vulnerability Detection - Identifying injection, XSS, and other security flaws π Security Metrics - Tracking and reporting security issues effectively π€ Automation - Integrating security checks into CI/CD pipelines π― Best Practices - Implementing comprehensive security reviews
Remember:
- π¨ Shift Left - Catch security issues early in development
- π Automate Everything - Manual reviews miss things
- π Stay Updated - Security threats evolve constantly
- π€ Team Effort - Security is everyoneβs responsibility
π€ Next Steps
Ready to enhance your security game? Hereβs whatβs next:
- π Explore penetration testing techniques for TypeScript apps
- π‘οΈ Implement security headers and CSP policies
- π Master OAuth and modern authentication patterns
- π Build a security-first TypeScript application
Keep securing! Your code (and users) will thank you! πͺβ¨
Remember: Security is not a feature, itβs a mindset! Always think like an attacker to build better defenses. ππ