mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-13 22:54:53 +03:00
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:
@@ -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()
|
||||
Reference in New Issue
Block a user