mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-16 16:03:17 +03:00
343 lines
13 KiB
Python
343 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Cryptographic Audit Scanner
|
|
|
|
Scans Python source files and configuration files for cryptographic
|
|
weaknesses including deprecated algorithms, insecure modes, hardcoded
|
|
secrets, and weak key derivation parameters.
|
|
|
|
Requirements:
|
|
pip install cryptography
|
|
|
|
Usage:
|
|
python process.py scan --target ./myapp --output audit_report.json
|
|
python process.py scan --target ./myapp/crypto.py --output report.json
|
|
python process.py test-samples # Run against built-in test samples
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import json
|
|
import argparse
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
from dataclasses import dataclass, asdict
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class Finding:
|
|
"""A cryptographic audit finding."""
|
|
severity: str # CRITICAL, HIGH, MEDIUM, LOW, INFO
|
|
category: str
|
|
title: str
|
|
description: str
|
|
file_path: str
|
|
line_number: int
|
|
code_snippet: str
|
|
remediation: str
|
|
cwe_id: Optional[str] = None
|
|
|
|
|
|
WEAK_HASH_PATTERNS = [
|
|
(r"\bmd5\b", "MD5", "CWE-328"),
|
|
(r"\bsha1\b", "SHA-1", "CWE-328"),
|
|
(r"\bSHA1\b", "SHA-1", "CWE-328"),
|
|
(r"\bMD5\b", "MD5", "CWE-328"),
|
|
(r"hashlib\.md5", "MD5 via hashlib", "CWE-328"),
|
|
(r"hashlib\.sha1", "SHA-1 via hashlib", "CWE-328"),
|
|
(r"hashes\.MD5", "MD5 via cryptography", "CWE-328"),
|
|
(r"hashes\.SHA1", "SHA-1 via cryptography", "CWE-328"),
|
|
]
|
|
|
|
WEAK_CIPHER_PATTERNS = [
|
|
(r"\bDES\b(?!3)", "DES", "CWE-327"),
|
|
(r"\bRC4\b", "RC4", "CWE-327"),
|
|
(r"\bRC2\b", "RC2", "CWE-327"),
|
|
(r"\bBlowfish\b", "Blowfish", "CWE-327"),
|
|
(r"algorithms\.TripleDES", "3DES", "CWE-327"),
|
|
(r"MODE_ECB", "ECB mode", "CWE-327"),
|
|
(r"AES\.MODE_ECB", "AES-ECB mode", "CWE-327"),
|
|
]
|
|
|
|
HARDCODED_SECRET_PATTERNS = [
|
|
(r'(?:password|passwd|pwd)\s*=\s*["\'][^"\']{4,}["\']', "Hardcoded password", "CWE-798"),
|
|
(r'(?:secret|api_?key|token)\s*=\s*["\'][^"\']{8,}["\']', "Hardcoded secret/key", "CWE-798"),
|
|
(r'(?:private_?key|priv_?key)\s*=\s*["\'][^"\']+["\']', "Hardcoded private key", "CWE-798"),
|
|
(r'["\']-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', "Embedded private key", "CWE-798"),
|
|
(r'AKIA[0-9A-Z]{16}', "AWS Access Key", "CWE-798"),
|
|
]
|
|
|
|
WEAK_KDF_PATTERNS = [
|
|
(r'iterations\s*=\s*(\d+)', "KDF iterations check", "CWE-916"),
|
|
(r'PBKDF2.*iterations.*?(\d+)', "PBKDF2 iterations", "CWE-916"),
|
|
]
|
|
|
|
INSECURE_RANDOM_PATTERNS = [
|
|
(r'\brandom\.random\b', "Insecure random (use secrets/os.urandom)", "CWE-338"),
|
|
(r'\brandom\.randint\b', "Insecure random for crypto", "CWE-338"),
|
|
(r'\brandom\.choice\b(?!.*secrets)', "Insecure random choice", "CWE-338"),
|
|
(r'\brandom\.seed\b', "Seeded random (predictable)", "CWE-338"),
|
|
]
|
|
|
|
DEPRECATED_TLS_PATTERNS = [
|
|
(r'SSLv2', "SSLv2 protocol", "CWE-326"),
|
|
(r'SSLv3', "SSLv3 protocol (POODLE)", "CWE-326"),
|
|
(r'TLSv1[^._23]', "TLS 1.0 protocol", "CWE-326"),
|
|
(r'TLSv1_1\b', "TLS 1.1 protocol", "CWE-326"),
|
|
(r'PROTOCOL_SSLv23', "Legacy SSL context", "CWE-326"),
|
|
(r'ssl\.PROTOCOL_TLS(?!_CLIENT)', "Permissive TLS context", "CWE-326"),
|
|
(r'verify_mode\s*=\s*ssl\.CERT_NONE', "TLS certificate verification disabled", "CWE-295"),
|
|
(r'check_hostname\s*=\s*False', "TLS hostname checking disabled", "CWE-297"),
|
|
]
|
|
|
|
|
|
def scan_file(file_path: str) -> List[Finding]:
|
|
"""Scan a single file for cryptographic weaknesses."""
|
|
findings = []
|
|
|
|
try:
|
|
content = Path(file_path).read_text(encoding="utf-8", errors="ignore")
|
|
except Exception as e:
|
|
logger.warning(f"Cannot read {file_path}: {e}")
|
|
return findings
|
|
|
|
lines = content.split("\n")
|
|
|
|
for line_num, line in enumerate(lines, 1):
|
|
stripped = line.strip()
|
|
if stripped.startswith("#") or stripped.startswith("//"):
|
|
continue
|
|
|
|
# Weak hash algorithms
|
|
for pattern, algo_name, cwe in WEAK_HASH_PATTERNS:
|
|
if re.search(pattern, line, re.IGNORECASE):
|
|
findings.append(Finding(
|
|
severity="HIGH",
|
|
category="Weak Hashing",
|
|
title=f"Use of weak hash algorithm: {algo_name}",
|
|
description=f"{algo_name} is cryptographically broken and should not be used for security purposes (signatures, integrity, password hashing).",
|
|
file_path=file_path,
|
|
line_number=line_num,
|
|
code_snippet=stripped[:200],
|
|
remediation=f"Replace {algo_name} with SHA-256 or SHA-3. For password hashing, use Argon2id or bcrypt.",
|
|
cwe_id=cwe,
|
|
))
|
|
|
|
# Weak ciphers and modes
|
|
for pattern, algo_name, cwe in WEAK_CIPHER_PATTERNS:
|
|
if re.search(pattern, line):
|
|
findings.append(Finding(
|
|
severity="HIGH",
|
|
category="Weak Encryption",
|
|
title=f"Use of insecure cipher or mode: {algo_name}",
|
|
description=f"{algo_name} is deprecated or insecure. ECB mode leaks patterns in ciphertext.",
|
|
file_path=file_path,
|
|
line_number=line_num,
|
|
code_snippet=stripped[:200],
|
|
remediation="Use AES-256-GCM for authenticated encryption. Never use ECB mode.",
|
|
cwe_id=cwe,
|
|
))
|
|
|
|
# Hardcoded secrets
|
|
for pattern, desc, cwe in HARDCODED_SECRET_PATTERNS:
|
|
if re.search(pattern, line, re.IGNORECASE):
|
|
sanitized = re.sub(r'["\'][^"\']+["\']', '"***REDACTED***"', stripped)
|
|
findings.append(Finding(
|
|
severity="CRITICAL",
|
|
category="Hardcoded Secret",
|
|
title=f"Potential hardcoded secret: {desc}",
|
|
description="Secrets should never be hardcoded in source code. They can be extracted from version control history.",
|
|
file_path=file_path,
|
|
line_number=line_num,
|
|
code_snippet=sanitized[:200],
|
|
remediation="Store secrets in environment variables, AWS Secrets Manager, HashiCorp Vault, or similar.",
|
|
cwe_id=cwe,
|
|
))
|
|
|
|
# Weak KDF parameters
|
|
for pattern, desc, cwe in WEAK_KDF_PATTERNS:
|
|
match = re.search(pattern, line)
|
|
if match:
|
|
try:
|
|
iterations = int(match.group(1))
|
|
if iterations < 100_000:
|
|
findings.append(Finding(
|
|
severity="HIGH",
|
|
category="Weak Key Derivation",
|
|
title=f"Insufficient KDF iterations: {iterations}",
|
|
description=f"PBKDF2 with {iterations} iterations is too low. OWASP recommends minimum 600,000 for PBKDF2-SHA256.",
|
|
file_path=file_path,
|
|
line_number=line_num,
|
|
code_snippet=stripped[:200],
|
|
remediation="Increase PBKDF2 iterations to 600,000+ or switch to Argon2id.",
|
|
cwe_id=cwe,
|
|
))
|
|
except (ValueError, IndexError):
|
|
pass
|
|
|
|
# Insecure random
|
|
for pattern, desc, cwe in INSECURE_RANDOM_PATTERNS:
|
|
if re.search(pattern, line):
|
|
if "import" not in stripped:
|
|
findings.append(Finding(
|
|
severity="HIGH",
|
|
category="Insecure Randomness",
|
|
title=f"Insecure random number generation: {desc}",
|
|
description="Python's random module is not cryptographically secure. Use os.urandom() or secrets module.",
|
|
file_path=file_path,
|
|
line_number=line_num,
|
|
code_snippet=stripped[:200],
|
|
remediation="Use os.urandom(), secrets.token_bytes(), or secrets.token_hex() for cryptographic operations.",
|
|
cwe_id=cwe,
|
|
))
|
|
|
|
# Deprecated TLS
|
|
for pattern, desc, cwe in DEPRECATED_TLS_PATTERNS:
|
|
if re.search(pattern, line):
|
|
findings.append(Finding(
|
|
severity="HIGH",
|
|
category="Deprecated Protocol",
|
|
title=f"Insecure TLS/SSL configuration: {desc}",
|
|
description=f"Deprecated or insecure protocol/configuration detected. {desc} is vulnerable to known attacks.",
|
|
file_path=file_path,
|
|
line_number=line_num,
|
|
code_snippet=stripped[:200],
|
|
remediation="Use TLS 1.2+ with strong cipher suites. Enable certificate verification and hostname checking.",
|
|
cwe_id=cwe,
|
|
))
|
|
|
|
return findings
|
|
|
|
|
|
def scan_directory(target_dir: str, extensions: Optional[List[str]] = None) -> List[Finding]:
|
|
"""Scan all matching files in a directory."""
|
|
if extensions is None:
|
|
extensions = [".py", ".js", ".ts", ".java", ".go", ".yml", ".yaml", ".json", ".conf", ".cfg", ".ini", ".env"]
|
|
|
|
all_findings = []
|
|
target_path = Path(target_dir)
|
|
|
|
if target_path.is_file():
|
|
return scan_file(str(target_path))
|
|
|
|
for ext in extensions:
|
|
for file in target_path.rglob(f"*{ext}"):
|
|
if ".git" in str(file) or "node_modules" in str(file) or "__pycache__" in str(file):
|
|
continue
|
|
findings = scan_file(str(file))
|
|
all_findings.extend(findings)
|
|
|
|
return all_findings
|
|
|
|
|
|
def generate_report(findings: List[Finding], target: str) -> Dict:
|
|
"""Generate a structured audit report."""
|
|
severity_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
|
|
category_counts = {}
|
|
|
|
for f in findings:
|
|
severity_counts[f.severity] = severity_counts.get(f.severity, 0) + 1
|
|
category_counts[f.category] = category_counts.get(f.category, 0) + 1
|
|
|
|
overall_risk = "LOW"
|
|
if severity_counts["CRITICAL"] > 0:
|
|
overall_risk = "CRITICAL"
|
|
elif severity_counts["HIGH"] > 0:
|
|
overall_risk = "HIGH"
|
|
elif severity_counts["MEDIUM"] > 0:
|
|
overall_risk = "MEDIUM"
|
|
|
|
return {
|
|
"audit_target": target,
|
|
"total_findings": len(findings),
|
|
"overall_risk": overall_risk,
|
|
"severity_summary": severity_counts,
|
|
"category_summary": category_counts,
|
|
"findings": [asdict(f) for f in findings],
|
|
"recommendations": [
|
|
"Replace all deprecated hash algorithms (MD5, SHA-1) with SHA-256 or SHA-3",
|
|
"Replace DES/3DES/RC4 with AES-256-GCM",
|
|
"Never use ECB mode; use GCM for authenticated encryption",
|
|
"Move all hardcoded secrets to a secrets manager",
|
|
"Use PBKDF2 with 600,000+ iterations or switch to Argon2id",
|
|
"Use os.urandom() or secrets module for cryptographic randomness",
|
|
"Enforce TLS 1.2+ and disable all legacy protocols",
|
|
"Enable certificate verification and hostname checking",
|
|
],
|
|
}
|
|
|
|
|
|
def test_with_samples():
|
|
"""Test the scanner with known-bad samples."""
|
|
samples = '''
|
|
import hashlib
|
|
import random
|
|
|
|
password = "SuperSecret123!"
|
|
api_key = "sk-abcdef1234567890abcdef"
|
|
|
|
def hash_password(pw):
|
|
return hashlib.md5(pw.encode()).hexdigest()
|
|
|
|
def generate_token():
|
|
return random.randint(100000, 999999)
|
|
|
|
from Crypto.Cipher import AES
|
|
cipher = AES.new(key, AES.MODE_ECB)
|
|
|
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=1000)
|
|
|
|
import ssl
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
ctx.check_hostname = False
|
|
'''
|
|
# Write temp sample file
|
|
sample_path = Path("__crypto_audit_test_sample.py")
|
|
sample_path.write_text(samples)
|
|
|
|
findings = scan_file(str(sample_path))
|
|
report = generate_report(findings, str(sample_path))
|
|
|
|
print(json.dumps(report, indent=2))
|
|
print(f"\nTotal findings: {report['total_findings']}")
|
|
print(f"Overall risk: {report['overall_risk']}")
|
|
|
|
sample_path.unlink()
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Cryptographic Audit Scanner")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
scan = subparsers.add_parser("scan", help="Scan target for crypto weaknesses")
|
|
scan.add_argument("--target", "-t", required=True, help="File or directory to scan")
|
|
scan.add_argument("--output", "-o", help="Output report file (JSON)")
|
|
|
|
subparsers.add_parser("test-samples", help="Test with built-in weak samples")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "scan":
|
|
findings = scan_directory(args.target)
|
|
report = generate_report(findings, args.target)
|
|
if args.output:
|
|
Path(args.output).write_text(json.dumps(report, indent=2))
|
|
logger.info(f"Report saved to {args.output}")
|
|
print(json.dumps(report, indent=2))
|
|
elif args.command == "test-samples":
|
|
test_with_samples()
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|