Add 30 new production-grade cybersecurity skills: AI security, supply chain, firmware, cloud-native, compliance, deception, crypto, threat hunting, purple team, OT, privacy

This commit is contained in:
mukul975
2026-03-19 19:14:23 +01:00
parent d43cc7a766
commit d833f0eab9
125 changed files with 47874 additions and 334 deletions
@@ -0,0 +1,19 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright 2025 mukul975
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,211 @@
---
name: implementing-attack-surface-management
description: >
Implements external attack surface management (EASM) using Shodan, Censys, and
ProjectDiscovery tools (subfinder, httpx, nuclei) for asset discovery, subdomain
enumeration, service fingerprinting, and exposure scoring. Includes a weighted
risk scoring algorithm based on OWASP attack surface analysis methodology and
the Relative Attack Surface Quotient (RSQ). Use when building continuous ASM
programs or performing external reconnaissance for security assessments.
domain: cybersecurity
subdomain: offensive-security
tags: [attack-surface, reconnaissance, shodan, censys, subfinder, nuclei, asset-discovery]
version: "1.0"
author: mukul975
license: Apache-2.0
---
# Implementing Attack Surface Management
## When to Use
- When building an external attack surface management (EASM) program from scratch
- When performing authorized external reconnaissance for penetration testing engagements
- When continuously monitoring organizational exposure across internet-facing assets
- When scoring and prioritizing external attack surface risks for remediation
- When integrating multiple discovery tools into an automated ASM pipeline
## Prerequisites
- Python 3.8+ with requests, shodan, censys libraries installed
- Shodan API key (free tier provides 100 queries/month)
- Censys API ID and Secret (free tier available)
- ProjectDiscovery tools installed: subfinder, httpx, nuclei
- Go 1.21+ for building ProjectDiscovery tools from source
- Appropriate authorization for all external scanning activities
- Target domains and IP ranges with written scope documentation
## Instructions
### Phase 1: Subdomain Enumeration with Multiple Sources
Use subfinder for passive subdomain discovery leveraging dozens of data sources
including certificate transparency logs, DNS datasets, and search engines.
```bash
# Install ProjectDiscovery tools
go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Basic subdomain enumeration
subfinder -d example.com -o subdomains.txt
# Verbose with all sources and recursive enumeration
subfinder -d example.com -all -recursive -o subdomains_full.txt
# Multi-domain enumeration from file
subfinder -dL domains.txt -o all_subdomains.txt
# Using OWASP Amass for deeper enumeration
amass enum -d example.com -passive -o amass_subdomains.txt
# Merge and deduplicate results
cat subdomains.txt amass_subdomains.txt | sort -u > combined_subdomains.txt
```
### Phase 2: Live Host Discovery and Service Fingerprinting
Probe discovered subdomains to identify live hosts, technologies, and services.
```bash
# HTTP probing with technology detection
cat combined_subdomains.txt | httpx -sc -cl -ct -title -tech-detect \
-follow-redirects -json -o httpx_results.json
# Detailed service fingerprinting
cat combined_subdomains.txt | httpx -sc -cl -ct -title -tech-detect \
-favicon -hash sha256 -jarm -cdn -cname \
-follow-redirects -json -o httpx_detailed.json
```
### Phase 3: Shodan Asset Discovery
Query Shodan for exposed services, open ports, and known vulnerabilities
associated with discovered assets.
```python
import shodan
api = shodan.Shodan("YOUR_SHODAN_API_KEY")
# Search by organization
results = api.search("org:\"Example Corp\"")
for service in results["matches"]:
print(f"{service['ip_str']}:{service['port']} - {service.get('product', 'unknown')}")
if service.get("vulns"):
for cve in service["vulns"]:
print(f" CVE: {cve}")
# Search by hostname
results = api.search("hostname:example.com")
# Search by SSL certificate
results = api.search("ssl.cert.subject.cn:example.com")
# Get host details with all services
host = api.host("93.184.216.34")
print(f"IP: {host['ip_str']}")
print(f"Ports: {host['ports']}")
print(f"Vulns: {host.get('vulns', [])}")
```
### Phase 4: Censys Asset Discovery
Use Censys to discover internet-facing assets through certificate and host search.
```python
from censys.search import CensysHosts, CensysCerts
# Host search
hosts = CensysHosts()
query = hosts.search("services.tls.certificates.leaf.subject.common_name: example.com")
for page in query:
for host in page:
print(f"IP: {host['ip']}")
for service in host.get("services", []):
print(f" Port: {service['port']} Protocol: {service['transport_protocol']}")
print(f" Service: {service.get('service_name', 'unknown')}")
# Certificate transparency search
certs = CensysCerts()
query = certs.search("parsed.names: example.com")
for page in query:
for cert in page:
print(f"Fingerprint: {cert['fingerprint_sha256']}")
print(f"Names: {cert.get('parsed', {}).get('names', [])}")
```
### Phase 5: Vulnerability Scanning with Nuclei
Run targeted vulnerability scans against discovered assets using Nuclei templates.
```bash
# Update nuclei templates
nuclei -ut
# Scan with all templates
cat combined_subdomains.txt | httpx -silent | nuclei -o nuclei_results.txt
# Scan with specific severity
cat combined_subdomains.txt | httpx -silent | \
nuclei -severity critical,high -o critical_findings.txt
# Scan with specific template categories
cat combined_subdomains.txt | httpx -silent | \
nuclei -tags cve,misconfig,exposure -o categorized_findings.txt
# Scan for exposed panels and sensitive files
cat combined_subdomains.txt | httpx -silent | \
nuclei -tags panel,exposure,config -o exposed_panels.txt
```
### Phase 6: Exposure Scoring Algorithm
Score each asset based on OWASP attack surface analysis principles, using
a weighted formula derived from the Relative Attack Surface Quotient (RSQ)
and damage-potential-to-effort ratio.
The scoring algorithm considers:
1. **Open ports and services** - weighted by service risk (management ports score higher)
2. **Known vulnerabilities** - weighted by CVSS score
3. **Technology age** - outdated software increases score
4. **Exposure level** - internet-facing vs. authenticated access
5. **Data sensitivity** - based on service type and content indicators
```python
# Exposure Score = sum of weighted factors, normalized to 0-100
# See agent.py for the full implementation
```
## Examples
```bash
# Run complete ASM pipeline against a target domain
python agent.py \
--domain example.com \
--action full_scan \
--shodan-key YOUR_KEY \
--censys-id YOUR_ID \
--censys-secret YOUR_SECRET \
--output asm_report.json
# Subdomain enumeration only
python agent.py \
--domain example.com \
--action enumerate \
--output subdomains.json
# Exposure scoring on previously discovered assets
python agent.py \
--domain example.com \
--action score \
--input previous_scan.json \
--output scored_assets.json
# Multi-domain scan from file
python agent.py \
--domain-list targets.txt \
--action full_scan \
--output multi_domain_report.json
```
@@ -0,0 +1,171 @@
# Reference: Attack Surface Management
## Exposure Scoring Algorithm
### Weighted Formula
The exposure score uses a weighted composite of five factors, each normalized to 0-100:
```
Exposure Score = (Port_Score * 0.25) + (Vuln_Score * 0.30) + (Tech_Score * 0.15)
+ (Exposure_Score * 0.15) + (Data_Score * 0.15)
```
### Component Scoring
**Open Ports (25% weight)**
- Each port has a risk weight from PORT_RISK_WEIGHTS (1.0-9.5)
- Management ports (SSH, RDP, Telnet): 8.0-9.5
- Database ports (MySQL, MongoDB, Redis): 9.0-9.5
- Web ports (HTTP, HTTPS): 2.5-3.0
- Formula: `min(100, (avg_weight * 10) * log2(count + 1))`
**Vulnerabilities (30% weight)**
- Weighted by CVSS score bands: Critical=10, High=7, Medium=4, Low=2
- Diminishing returns via logarithmic scaling
- Formula: `min(100, total_weight * log2(count + 1))`
**Technology Risk (15% weight)**
- Known high-risk technologies scored 2.0-8.0
- Struts (8.0), phpMyAdmin (8.0), WebLogic (7.0), Jenkins (7.0)
- Unknown technologies get baseline score of 10.0
**Exposure Level (15% weight)**
- Base score 50 for internet-facing
- HTTP-only: +15 | CDN protected: -20
- Auth required (401/403): -25
- Admin/login panel detected: +20
**Data Sensitivity (15% weight)**
- Exposed database ports: +20 each
- File sharing ports (FTP, SMB): +15 each
- Sensitive service indicators: +15 each
### Risk Levels
| Score Range | Risk Level |
|-------------|------------|
| 80-100 | CRITICAL |
| 60-79 | HIGH |
| 40-59 | MEDIUM |
| 20-39 | LOW |
| 0-19 | INFORMATIONAL |
## OWASP Attack Surface Analysis
### Entry Points to Catalog
Per OWASP Attack Surface Analysis Cheat Sheet:
- Network-accessible ports and services
- Web application endpoints and parameters
- Authentication mechanisms
- File upload functions
- Administrative interfaces
- API endpoints
- Form fields and query parameters
### Relative Attack Surface Quotient (RSQ)
Microsoft's RSQ methodology counts:
1. **Channels**: TCP/UDP ports, RPC endpoints, named pipes
2. **Methods**: HTTP verbs, RPC methods, API functions
3. **Data Items**: Files, registry keys, database records
RSQ = sum of (damage_potential / effort) for each attack vector
## Shodan Search Operators
| Operator | Description | Example |
|----------|-------------|---------|
| `hostname:` | Search by hostname | `hostname:example.com` |
| `org:` | Search by organization | `org:"Example Corp"` |
| `net:` | Search by CIDR | `net:93.184.216.0/24` |
| `port:` | Filter by port | `port:3389` |
| `product:` | Filter by product | `product:nginx` |
| `os:` | Filter by OS | `os:"Windows Server 2019"` |
| `ssl.cert.subject.cn:` | SSL cert CN | `ssl.cert.subject.cn:example.com` |
| `vuln:` | Search by CVE | `vuln:CVE-2021-44228` |
| `country:` | Filter by country | `country:US` |
| `has_vuln:true` | Has known vulns | `hostname:example.com has_vuln:true` |
## Censys Search Syntax
| Query | Description |
|-------|-------------|
| `services.port: 443` | Hosts with port 443 open |
| `services.tls.certificates.leaf.subject.common_name: example.com` | SSL cert match |
| `services.http.response.html_title: "Admin"` | Page title match |
| `services.software.product: "Apache"` | Software product |
| `location.country: "United States"` | Geographic filter |
| `autonomous_system.asn: 13335` | ASN filter |
## ProjectDiscovery Tool Chain
### subfinder
Passive subdomain discovery using 50+ data sources:
- Certificate transparency (crt.sh, Certspotter)
- DNS datasets (DNSdumpster, SecurityTrails)
- Search engines (Google, Bing, Yahoo)
- Web archives (Wayback Machine, CommonCrawl)
- Shodan, Censys, VirusTotal APIs
```bash
subfinder -d example.com -all -recursive -o subs.txt
```
### httpx
HTTP toolkit for probing and fingerprinting:
- Status codes, content length, content type
- Technology detection (Wappalyzer)
- Favicon hash, JARM fingerprint
- CDN detection, CNAME resolution
```bash
cat subs.txt | httpx -sc -cl -ct -title -tech-detect -json -o httpx.json
```
### nuclei
Template-based vulnerability scanner:
- 10,000+ community templates
- Severity-based filtering
- Protocol support: HTTP, DNS, TCP, SSL, File
- Automatic template updates
```bash
cat live_hosts.txt | nuclei -severity critical,high -tags cve -o findings.txt
```
## Port Risk Classification
### Critical Exposure (Score 9.0+)
- 23 (Telnet): Unencrypted remote access
- 27017 (MongoDB): Often misconfigured without auth
- 6379 (Redis): Commonly exposed without auth
- 445 (SMB): Ransomware propagation vector
### High Exposure (Score 7.0-8.9)
- 22 (SSH): Brute force target
- 3389 (RDP): BlueKeep, credential attacks
- 3306/5432/1433 (Databases): Data exfiltration
- 21 (FTP): Anonymous access, credential theft
- 161 (SNMP): Community string exposure
### Medium Exposure (Score 4.0-6.9)
- 8080/8443 (Alt HTTP/S): Dev/staging environments
- 25 (SMTP): Open relay, spoofing
- 53 (DNS): Zone transfer, cache poisoning
- 8888 (Various): Development panels
### Low Exposure (Score 2.0-3.9)
- 80 (HTTP): Standard web
- 443 (HTTPS): Standard secure web
### References
- OWASP Attack Surface Analysis: https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html
- OWASP ASM Top 10: https://owasp.org/www-project-attack-surface-management-top-10/
- ProjectDiscovery ASM blog: https://blog.projectdiscovery.io/asm-platform-using-projectdiscovery-tools/
- Shodan API documentation: https://developer.shodan.io/api
- Censys API documentation: https://search.censys.io/api
- subfinder GitHub: https://github.com/projectdiscovery/subfinder
- nuclei GitHub: https://github.com/projectdiscovery/nuclei
@@ -0,0 +1,921 @@
#!/usr/bin/env python3
"""Agent for implementing external attack surface management (EASM).
Combines Shodan, Censys, ProjectDiscovery tools (subfinder, httpx, nuclei),
and a custom exposure scoring algorithm for comprehensive ASM.
DISCLAIMER: This tool is intended for authorized security testing and attack
surface management only. Ensure you have written authorization before scanning
any targets. Unauthorized scanning of systems you do not own or have explicit
permission to test is illegal and unethical.
"""
import json
import subprocess
import argparse
import math
from datetime import datetime
from collections import defaultdict
try:
import shodan
except ImportError:
shodan = None
try:
from censys.search import CensysHosts, CensysCerts
except ImportError:
CensysHosts = None
CensysCerts = None
# --------------------------------------------------------------------------- #
# Port risk weights based on OWASP attack surface analysis methodology
# --------------------------------------------------------------------------- #
PORT_RISK_WEIGHTS = {
# Management / remote access (highest risk)
22: 8.0, # SSH
23: 9.5, # Telnet (unencrypted)
3389: 8.5, # RDP
5900: 8.0, # VNC
5985: 7.5, # WinRM HTTP
5986: 7.0, # WinRM HTTPS
# Web services
80: 3.0, # HTTP
443: 2.5, # HTTPS
8080: 5.0, # Alt HTTP (often dev/admin)
8443: 4.5, # Alt HTTPS
8888: 6.0, # Often dev panels
# Databases (high risk if exposed)
3306: 9.0, # MySQL
5432: 9.0, # PostgreSQL
1433: 9.0, # MSSQL
1521: 9.0, # Oracle
27017: 9.5, # MongoDB
6379: 9.5, # Redis
9200: 8.5, # Elasticsearch
5601: 8.0, # Kibana
# Message queues
5672: 7.5, # RabbitMQ
9092: 7.5, # Kafka
# File sharing
21: 8.0, # FTP
445: 9.0, # SMB
139: 8.5, # NetBIOS
# Email
25: 6.0, # SMTP
110: 6.5, # POP3
143: 6.0, # IMAP
# DNS
53: 5.0, # DNS
# SNMP
161: 8.0, # SNMP
162: 7.5, # SNMP Trap
}
# Services that indicate sensitive data handling
SENSITIVE_SERVICE_INDICATORS = {
"mysql", "postgresql", "mongodb", "redis", "elasticsearch",
"oracle", "mssql", "couchdb", "cassandra", "memcached",
"rabbitmq", "kafka", "activemq",
}
# Technologies known to have frequent vulnerabilities
HIGH_RISK_TECHNOLOGIES = {
"apache": 3.0,
"nginx": 2.0,
"iis": 4.0,
"tomcat": 5.0,
"jboss": 6.0,
"weblogic": 7.0,
"wordpress": 6.0,
"drupal": 5.0,
"joomla": 5.5,
"phpmyadmin": 8.0,
"jenkins": 7.0,
"gitlab": 5.0,
"grafana": 4.0,
"kibana": 5.0,
"solr": 6.0,
"struts": 8.0,
"coldfusion": 7.0,
"exchange": 7.5,
"sharepoint": 6.0,
}
class SubdomainEnumerator:
"""Discovers subdomains using subfinder and amass."""
def __init__(self, domain):
self.domain = domain
self.subdomains = set()
def run_subfinder(self):
"""Run subfinder for passive subdomain enumeration."""
print(f"[+] Running subfinder against {self.domain}")
try:
result = subprocess.run(
["subfinder", "-d", self.domain, "-all", "-silent"],
capture_output=True, text=True, timeout=300,
)
found = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
self.subdomains.update(found)
print(f"[+] subfinder found {len(found)} subdomains")
except FileNotFoundError:
print("[-] subfinder not installed. Install: go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest")
except subprocess.TimeoutExpired:
print("[-] subfinder timed out after 300s")
return self.subdomains
def run_amass(self):
"""Run amass for deeper passive enumeration."""
print(f"[+] Running amass passive enum against {self.domain}")
try:
result = subprocess.run(
["amass", "enum", "-d", self.domain, "-passive"],
capture_output=True, text=True, timeout=600,
)
found = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
self.subdomains.update(found)
print(f"[+] amass found {len(found)} subdomains")
except FileNotFoundError:
print("[-] amass not installed. Install: go install -v github.com/owasp-amass/amass/v4/...@master")
except subprocess.TimeoutExpired:
print("[-] amass timed out after 600s")
return self.subdomains
def enumerate_all(self):
"""Run all enumeration tools and merge results."""
self.run_subfinder()
self.run_amass()
self.subdomains.discard("")
print(f"[+] Total unique subdomains: {len(self.subdomains)}")
return sorted(self.subdomains)
class ServiceFingerprinter:
"""Probes live hosts and fingerprints services using httpx."""
def __init__(self, subdomains):
self.subdomains = subdomains
self.results = []
def run_httpx(self):
"""Run httpx for HTTP probing and technology detection."""
if not self.subdomains:
print("[-] No subdomains to probe")
return []
print(f"[+] Running httpx against {len(self.subdomains)} subdomains")
input_data = "\n".join(self.subdomains)
try:
result = subprocess.run(
[
"httpx", "-sc", "-cl", "-ct", "-title", "-tech-detect",
"-favicon", "-cdn", "-cname", "-follow-redirects",
"-json", "-silent",
],
input=input_data,
capture_output=True, text=True, timeout=600,
)
for line in result.stdout.strip().split("\n"):
if line.strip():
try:
self.results.append(json.loads(line))
except json.JSONDecodeError:
continue
print(f"[+] httpx found {len(self.results)} live hosts")
except FileNotFoundError:
print("[-] httpx not installed. Install: go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest")
except subprocess.TimeoutExpired:
print("[-] httpx timed out after 600s")
return self.results
class ShodanScanner:
"""Discovers exposed services and vulnerabilities via Shodan API."""
def __init__(self, api_key):
if shodan is None:
raise ImportError("pip install shodan")
self.api = shodan.Shodan(api_key)
self.results = []
def search_domain(self, domain):
"""Search Shodan for all hosts associated with a domain."""
print(f"[+] Searching Shodan for hostname:{domain}")
try:
results = self.api.search(f"hostname:{domain}", limit=500)
self.results.extend(results.get("matches", []))
print(f"[+] Shodan returned {results['total']} results")
except shodan.APIError as e:
print(f"[-] Shodan API error: {e}")
return self.results
def search_org(self, org_name):
"""Search Shodan for all hosts in an organization."""
print(f'[+] Searching Shodan for org:"{org_name}"')
try:
results = self.api.search(f'org:"{org_name}"', limit=500)
self.results.extend(results.get("matches", []))
print(f"[+] Shodan returned {results['total']} results")
except shodan.APIError as e:
print(f"[-] Shodan API error: {e}")
return self.results
def search_ssl_cert(self, domain):
"""Search Shodan for hosts with SSL certificates matching domain."""
print(f"[+] Searching Shodan for ssl.cert.subject.cn:{domain}")
try:
results = self.api.search(f"ssl.cert.subject.cn:{domain}", limit=500)
self.results.extend(results.get("matches", []))
print(f"[+] Shodan SSL cert search returned {results['total']} results")
except shodan.APIError as e:
print(f"[-] Shodan API error: {e}")
return self.results
def get_host_details(self, ip):
"""Get detailed information for a specific IP."""
try:
return self.api.host(ip)
except shodan.APIError as e:
print(f"[-] Shodan host lookup failed for {ip}: {e}")
return None
def get_all_results(self):
"""Return deduplicated results."""
seen_ips = set()
deduped = []
for result in self.results:
ip = result.get("ip_str", "")
port = result.get("port", 0)
key = f"{ip}:{port}"
if key not in seen_ips:
seen_ips.add(key)
deduped.append(result)
return deduped
class CensysScanner:
"""Discovers internet-facing assets through Censys host and cert search."""
def __init__(self, api_id, api_secret):
if CensysHosts is None:
raise ImportError("pip install censys")
self.hosts_api = CensysHosts(api_id=api_id, api_secret=api_secret)
self.certs_api = CensysCerts(api_id=api_id, api_secret=api_secret)
self.results = []
def search_hosts(self, domain, max_pages=5):
"""Search Censys for hosts matching domain."""
print(f"[+] Searching Censys hosts for {domain}")
query = f"services.tls.certificates.leaf.subject.common_name: {domain}"
try:
count = 0
for page in self.hosts_api.search(query, per_page=100, pages=max_pages):
for host in page:
self.results.append({
"ip": host.get("ip"),
"services": host.get("services", []),
"location": host.get("location", {}),
"autonomous_system": host.get("autonomous_system", {}),
"source": "censys",
})
count += 1
print(f"[+] Censys returned {count} hosts")
except Exception as e:
print(f"[-] Censys search error: {e}")
return self.results
def search_certificates(self, domain, max_pages=3):
"""Search Censys certificate transparency logs."""
print(f"[+] Searching Censys certificates for {domain}")
subdomains = set()
try:
for page in self.certs_api.search(
f"parsed.names: {domain}", per_page=100, pages=max_pages
):
for cert in page:
names = cert.get("parsed", {}).get("names", [])
for name in names:
if name.endswith(domain):
subdomains.add(name)
print(f"[+] Censys certs revealed {len(subdomains)} subdomains")
except Exception as e:
print(f"[-] Censys cert search error: {e}")
return subdomains
class VulnerabilityScanner:
"""Runs vulnerability scans using Nuclei."""
def __init__(self, targets):
self.targets = targets
self.findings = []
def run_nuclei(self, severity="critical,high", tags=None):
"""Run nuclei against targets with specified severity/tags."""
if not self.targets:
print("[-] No targets for nuclei scan")
return []
print(f"[+] Running nuclei against {len(self.targets)} targets")
input_data = "\n".join(self.targets)
cmd = ["nuclei", "-json", "-silent", "-severity", severity]
if tags:
cmd.extend(["-tags", tags])
try:
result = subprocess.run(
cmd, input=input_data,
capture_output=True, text=True, timeout=1800,
)
for line in result.stdout.strip().split("\n"):
if line.strip():
try:
finding = json.loads(line)
self.findings.append({
"template_id": finding.get("template-id", ""),
"name": finding.get("info", {}).get("name", ""),
"severity": finding.get("info", {}).get("severity", ""),
"host": finding.get("host", ""),
"matched_at": finding.get("matched-at", ""),
"type": finding.get("type", ""),
"description": finding.get("info", {}).get("description", ""),
"tags": finding.get("info", {}).get("tags", []),
"reference": finding.get("info", {}).get("reference", []),
"cvss_score": finding.get("info", {}).get(
"classification", {}
).get("cvss-score", 0),
"cve_id": finding.get("info", {}).get(
"classification", {}
).get("cve-id", ""),
})
except json.JSONDecodeError:
continue
print(f"[+] nuclei found {len(self.findings)} vulnerabilities")
except FileNotFoundError:
print("[-] nuclei not installed. Install: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest")
except subprocess.TimeoutExpired:
print("[-] nuclei timed out after 1800s")
return self.findings
class ExposureScorer:
"""Calculates exposure scores using OWASP attack surface analysis principles.
The scoring algorithm implements a weighted formula derived from:
- OWASP Relative Attack Surface Quotient (RSQ)
- Carnegie Mellon damage-potential-to-effort ratio
- CVSS-based vulnerability weighting
Final score is normalized to 0-100 range.
"""
def __init__(self):
self.weights = {
"open_ports": 0.25,
"vulnerabilities": 0.30,
"technology_risk": 0.15,
"exposure_level": 0.15,
"data_sensitivity": 0.15,
}
def score_open_ports(self, ports):
"""Score based on open ports and their associated risk.
Uses PORT_RISK_WEIGHTS to assign higher scores to management ports,
databases, and legacy protocols.
"""
if not ports:
return 0.0
total_risk = 0.0
for port in ports:
weight = PORT_RISK_WEIGHTS.get(port, 4.0)
total_risk += weight
# Normalize: more ports = higher risk, but with diminishing returns
# Using log scale to prevent linear explosion with many ports
normalized = min(100.0, (total_risk / len(ports)) * 10 * math.log2(len(ports) + 1))
return round(normalized, 2)
def score_vulnerabilities(self, vulns):
"""Score based on discovered vulnerabilities weighted by CVSS.
Critical (9.0-10.0): weight 10
High (7.0-8.9): weight 7
Medium (4.0-6.9): weight 4
Low (0.1-3.9): weight 2
"""
if not vulns:
return 0.0
total_weight = 0.0
for vuln in vulns:
cvss = vuln.get("cvss_score", 0)
if isinstance(cvss, str):
try:
cvss = float(cvss)
except ValueError:
cvss = 5.0
if cvss >= 9.0:
total_weight += 10.0
elif cvss >= 7.0:
total_weight += 7.0
elif cvss >= 4.0:
total_weight += 4.0
else:
total_weight += 2.0
# Normalize with diminishing returns
normalized = min(100.0, total_weight * math.log2(len(vulns) + 1))
return round(normalized, 2)
def score_technology_risk(self, technologies):
"""Score based on technology stack risk assessment."""
if not technologies:
return 0.0
total_risk = 0.0
matched = 0
for tech in technologies:
tech_lower = tech.lower()
for known_tech, risk in HIGH_RISK_TECHNOLOGIES.items():
if known_tech in tech_lower:
total_risk += risk
matched += 1
break
if matched == 0:
return 10.0 # Unknown tech gets baseline risk
normalized = min(100.0, (total_risk / matched) * 12 * math.log2(matched + 1))
return round(normalized, 2)
def score_exposure_level(self, asset):
"""Score based on how exposed the asset is.
Factors: internet-reachable, authentication required, CDN protection.
"""
score = 50.0 # Base score for internet-facing asset
# No HTTPS = higher risk
if asset.get("scheme") == "http":
score += 15.0
# CDN protection reduces exposure
if asset.get("cdn"):
score -= 20.0
# Authentication indicators reduce exposure
status_code = asset.get("status_code", 200)
if status_code in (401, 403):
score -= 25.0
# Default/login pages increase risk
title = (asset.get("title") or "").lower()
if any(kw in title for kw in ["login", "admin", "dashboard", "panel", "console"]):
score += 20.0
return round(max(0.0, min(100.0, score)), 2)
def score_data_sensitivity(self, services, ports):
"""Score based on potential data sensitivity.
Database ports, email services, and file shares indicate sensitive data handling.
"""
score = 0.0
service_set = set()
for svc in (services or []):
service_set.add(svc.lower() if isinstance(svc, str) else "")
# Check for sensitive service indicators
for indicator in SENSITIVE_SERVICE_INDICATORS:
if indicator in service_set:
score += 15.0
# Check for database ports
db_ports = {3306, 5432, 1433, 1521, 27017, 6379, 9200}
exposed_db_ports = set(ports or []) & db_ports
score += len(exposed_db_ports) * 20.0
# File sharing ports
file_ports = {21, 445, 139, 2049}
exposed_file_ports = set(ports or []) & file_ports
score += len(exposed_file_ports) * 15.0
return round(min(100.0, score), 2)
def calculate_asset_score(self, asset):
"""Calculate the overall exposure score for an asset.
Returns a dict with component scores and weighted total (0-100).
"""
ports = asset.get("ports", [])
vulns = asset.get("vulnerabilities", [])
technologies = asset.get("technologies", [])
services = asset.get("services", [])
component_scores = {
"open_ports": self.score_open_ports(ports),
"vulnerabilities": self.score_vulnerabilities(vulns),
"technology_risk": self.score_technology_risk(technologies),
"exposure_level": self.score_exposure_level(asset),
"data_sensitivity": self.score_data_sensitivity(services, ports),
}
weighted_total = sum(
component_scores[key] * self.weights[key]
for key in self.weights
)
return {
"host": asset.get("host", asset.get("ip", "unknown")),
"total_score": round(weighted_total, 2),
"risk_level": self._risk_level(weighted_total),
"component_scores": component_scores,
"weights": self.weights,
}
def _risk_level(self, score):
if score >= 80:
return "CRITICAL"
elif score >= 60:
return "HIGH"
elif score >= 40:
return "MEDIUM"
elif score >= 20:
return "LOW"
return "INFORMATIONAL"
def score_all_assets(self, assets):
"""Score all assets and return sorted by risk."""
scored = [self.calculate_asset_score(a) for a in assets]
scored.sort(key=lambda x: x["total_score"], reverse=True)
return scored
class ASMPipeline:
"""Orchestrates the full attack surface management pipeline."""
def __init__(self, domain, shodan_key=None, censys_id=None, censys_secret=None):
self.domain = domain
self.shodan_key = shodan_key
self.censys_id = censys_id
self.censys_secret = censys_secret
self.subdomains = []
self.live_hosts = []
self.shodan_results = []
self.censys_results = []
self.nuclei_findings = []
self.assets = []
def enumerate_subdomains(self):
"""Phase 1: Discover subdomains."""
enumerator = SubdomainEnumerator(self.domain)
self.subdomains = enumerator.enumerate_all()
# Enrich with Censys certificate transparency
if self.censys_id and self.censys_secret:
try:
censys = CensysScanner(self.censys_id, self.censys_secret)
ct_subdomains = censys.search_certificates(self.domain)
combined = set(self.subdomains) | ct_subdomains
self.subdomains = sorted(combined)
print(f"[+] After CT enrichment: {len(self.subdomains)} subdomains")
except Exception as e:
print(f"[-] Censys CT search failed: {e}")
return self.subdomains
def fingerprint_services(self):
"""Phase 2: Probe live hosts and fingerprint technologies."""
fingerprinter = ServiceFingerprinter(self.subdomains)
self.live_hosts = fingerprinter.run_httpx()
return self.live_hosts
def discover_shodan(self):
"""Phase 3: Enrich with Shodan data."""
if not self.shodan_key:
print("[!] Shodan API key not provided, skipping")
return []
try:
scanner = ShodanScanner(self.shodan_key)
scanner.search_domain(self.domain)
scanner.search_ssl_cert(self.domain)
self.shodan_results = scanner.get_all_results()
except Exception as e:
print(f"[-] Shodan scanning failed: {e}")
return self.shodan_results
def discover_censys(self):
"""Phase 4: Enrich with Censys data."""
if not self.censys_id or not self.censys_secret:
print("[!] Censys API credentials not provided, skipping")
return []
try:
scanner = CensysScanner(self.censys_id, self.censys_secret)
self.censys_results = scanner.search_hosts(self.domain)
except Exception as e:
print(f"[-] Censys scanning failed: {e}")
return self.censys_results
def scan_vulnerabilities(self):
"""Phase 5: Run vulnerability scans."""
targets = []
for host in self.live_hosts:
url = host.get("url", "")
if url:
targets.append(url)
if not targets:
targets = [f"https://{sub}" for sub in self.subdomains[:100]]
scanner = VulnerabilityScanner(targets)
self.nuclei_findings = scanner.run_nuclei()
return self.nuclei_findings
def _build_asset_inventory(self):
"""Merge all data sources into a unified asset inventory."""
asset_map = defaultdict(lambda: {
"host": "",
"ip": "",
"ports": [],
"services": [],
"technologies": [],
"vulnerabilities": [],
"status_code": 200,
"title": "",
"cdn": False,
"scheme": "https",
})
# Merge httpx results
for host in self.live_hosts:
key = host.get("host", host.get("input", ""))
asset = asset_map[key]
asset["host"] = key
asset["status_code"] = host.get("status_code", 200)
asset["title"] = host.get("title", "")
asset["cdn"] = host.get("cdn", False)
asset["scheme"] = host.get("scheme", "https")
techs = host.get("tech", [])
if isinstance(techs, list):
asset["technologies"].extend(techs)
port = host.get("port", 0)
if port:
asset["ports"].append(port)
# Merge Shodan results
for result in self.shodan_results:
ip = result.get("ip_str", "")
hostnames = result.get("hostnames", [])
key = hostnames[0] if hostnames else ip
asset = asset_map[key]
asset["ip"] = ip
asset["host"] = asset["host"] or key
port = result.get("port", 0)
if port and port not in asset["ports"]:
asset["ports"].append(port)
product = result.get("product", "")
if product and product not in asset["services"]:
asset["services"].append(product)
for cve in result.get("vulns", []):
asset["vulnerabilities"].append({
"cve_id": cve,
"cvss_score": result.get("vulns", {}).get(cve, {}).get(
"cvss", 5.0
) if isinstance(result.get("vulns"), dict) else 5.0,
"source": "shodan",
})
# Merge Censys results
for result in self.censys_results:
ip = result.get("ip", "")
key = ip
asset = asset_map[key]
asset["ip"] = ip
asset["host"] = asset["host"] or ip
for svc in result.get("services", []):
port = svc.get("port", 0)
if port and port not in asset["ports"]:
asset["ports"].append(port)
svc_name = svc.get("service_name", "")
if svc_name and svc_name not in asset["services"]:
asset["services"].append(svc_name)
# Merge Nuclei findings
for finding in self.nuclei_findings:
host = finding.get("host", "")
# Match to existing asset or create new entry
matched_key = None
for key in asset_map:
if key in host or host in key:
matched_key = key
break
if matched_key is None:
matched_key = host
asset_map[matched_key]["vulnerabilities"].append({
"cve_id": finding.get("cve_id", ""),
"name": finding.get("name", ""),
"severity": finding.get("severity", ""),
"cvss_score": finding.get("cvss_score", 5.0),
"template_id": finding.get("template_id", ""),
"source": "nuclei",
})
# Deduplicate technologies and ports
for asset in asset_map.values():
asset["ports"] = sorted(set(asset["ports"]))
asset["technologies"] = list(set(asset["technologies"]))
asset["services"] = list(set(asset["services"]))
self.assets = list(asset_map.values())
return self.assets
def score_assets(self):
"""Phase 6: Calculate exposure scores for all assets."""
if not self.assets:
self._build_asset_inventory()
scorer = ExposureScorer()
return scorer.score_all_assets(self.assets)
def run_full_scan(self):
"""Execute the complete ASM pipeline."""
print(f"\n{'='*60}")
print(f" ATTACK SURFACE MANAGEMENT SCAN: {self.domain}")
print(f"{'='*60}\n")
# Phase 1: Subdomain enumeration
print("[*] Phase 1: Subdomain Enumeration")
self.enumerate_subdomains()
# Phase 2: Service fingerprinting
print("\n[*] Phase 2: Service Fingerprinting")
self.fingerprint_services()
# Phase 3: Shodan enrichment
print("\n[*] Phase 3: Shodan Asset Discovery")
self.discover_shodan()
# Phase 4: Censys enrichment
print("\n[*] Phase 4: Censys Asset Discovery")
self.discover_censys()
# Phase 5: Vulnerability scanning
print("\n[*] Phase 5: Vulnerability Scanning")
self.scan_vulnerabilities()
# Phase 6: Build inventory and score
print("\n[*] Phase 6: Asset Inventory and Exposure Scoring")
self._build_asset_inventory()
scored_assets = self.score_assets()
# Build final report
report = {
"scan_id": f"asm-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}",
"domain": self.domain,
"generated_at": datetime.utcnow().isoformat(),
"summary": {
"total_subdomains": len(self.subdomains),
"live_hosts": len(self.live_hosts),
"shodan_services": len(self.shodan_results),
"censys_hosts": len(self.censys_results),
"total_vulnerabilities": len(self.nuclei_findings),
"total_assets": len(self.assets),
"critical_assets": sum(
1 for a in scored_assets if a["risk_level"] == "CRITICAL"
),
"high_risk_assets": sum(
1 for a in scored_assets if a["risk_level"] == "HIGH"
),
"medium_risk_assets": sum(
1 for a in scored_assets if a["risk_level"] == "MEDIUM"
),
"low_risk_assets": sum(
1 for a in scored_assets if a["risk_level"] in ("LOW", "INFORMATIONAL")
),
"average_score": round(
sum(a["total_score"] for a in scored_assets) / max(len(scored_assets), 1), 2
),
},
"scored_assets": scored_assets,
"subdomains": self.subdomains,
"vulnerabilities": self.nuclei_findings,
"raw_data": {
"httpx_hosts": len(self.live_hosts),
"shodan_matches": len(self.shodan_results),
"censys_matches": len(self.censys_results),
},
}
return report
def main():
parser = argparse.ArgumentParser(
description="Attack Surface Management Agent"
)
parser.add_argument("--domain", help="Target domain")
parser.add_argument("--domain-list", help="File with list of target domains")
parser.add_argument(
"--action",
required=True,
choices=["enumerate", "fingerprint", "shodan", "censys", "vuln_scan", "score", "full_scan"],
)
parser.add_argument("--shodan-key", help="Shodan API key")
parser.add_argument("--censys-id", help="Censys API ID")
parser.add_argument("--censys-secret", help="Censys API secret")
parser.add_argument("--input", help="Input file from previous scan (JSON)")
parser.add_argument("--output", default="asm_report.json")
args = parser.parse_args()
domains = []
if args.domain:
domains.append(args.domain)
elif args.domain_list:
with open(args.domain_list) as f:
domains = [line.strip() for line in f if line.strip()]
else:
print("[-] Provide --domain or --domain-list")
return
all_reports = []
for domain in domains:
pipeline = ASMPipeline(
domain=domain,
shodan_key=args.shodan_key,
censys_id=args.censys_id,
censys_secret=args.censys_secret,
)
if args.action == "enumerate":
subdomains = pipeline.enumerate_subdomains()
report = {
"domain": domain,
"subdomains": subdomains,
"count": len(subdomains),
}
elif args.action == "fingerprint":
pipeline.enumerate_subdomains()
hosts = pipeline.fingerprint_services()
report = {"domain": domain, "live_hosts": hosts, "count": len(hosts)}
elif args.action == "shodan":
results = pipeline.discover_shodan()
report = {"domain": domain, "shodan_results": results, "count": len(results)}
elif args.action == "censys":
results = pipeline.discover_censys()
report = {"domain": domain, "censys_results": results, "count": len(results)}
elif args.action == "vuln_scan":
pipeline.enumerate_subdomains()
pipeline.fingerprint_services()
findings = pipeline.scan_vulnerabilities()
report = {"domain": domain, "vulnerabilities": findings, "count": len(findings)}
elif args.action == "score":
if args.input:
with open(args.input) as f:
prev_data = json.load(f)
assets = prev_data.get("scored_assets", prev_data.get("assets", []))
scorer = ExposureScorer()
scored = scorer.score_all_assets(assets)
report = {"domain": domain, "scored_assets": scored}
else:
report = pipeline.run_full_scan()
elif args.action == "full_scan":
report = pipeline.run_full_scan()
else:
print(f"[-] Unknown action: {args.action}")
continue
all_reports.append(report)
output = all_reports[0] if len(all_reports) == 1 else {"domains": all_reports}
with open(args.output, "w") as f:
json.dump(output, f, indent=2, default=str)
print(f"\n[+] Report saved to {args.output}")
# Print summary
for report in all_reports:
if "summary" in report:
s = report["summary"]
print(f"\n{'='*60}")
print(f" ASM SUMMARY: {report.get('domain', 'N/A')}")
print(f"{'='*60}")
print(f" Subdomains discovered: {s.get('total_subdomains', 0)}")
print(f" Live hosts: {s.get('live_hosts', 0)}")
print(f" Total vulnerabilities: {s.get('total_vulnerabilities', 0)}")
print(f" Assets scored: {s.get('total_assets', 0)}")
print(f" Average exposure score: {s.get('average_score', 0)}")
print(f" CRITICAL: {s.get('critical_assets', 0)}")
print(f" HIGH: {s.get('high_risk_assets', 0)}")
print(f" MEDIUM: {s.get('medium_risk_assets', 0)}")
print(f" LOW: {s.get('low_risk_assets', 0)}")
if __name__ == "__main__":
main()