189 lines
7 KiB
Bash
Executable file
189 lines
7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -o pipefail
|
|
export LC_ALL=C
|
|
|
|
# COLOURS
|
|
# standard terminal output hues
|
|
red='\033[0;31m'
|
|
green='\033[0;32m'
|
|
yellow='\033[1;33m'
|
|
cyan='\033[0;36m'
|
|
nc='\033[0m'
|
|
|
|
# CONFIGURATION
|
|
# defaults to mullvad dns for privacy-respecting resolution
|
|
resolver="@194.242.2.2"
|
|
timeout_val=2
|
|
ua="mozilla/5.0 (x11; linux x86_64) applewebkit/537.36"
|
|
|
|
# USAGE GUIDE
|
|
# displays instructions if arguments are missing
|
|
usage() {
|
|
printf "${cyan}usage:${nc} reconctl [-t timeout] <domain>\n"
|
|
printf " -t set network timeout in seconds (default: 2)\n"
|
|
printf " -h show help menu\n\n"
|
|
exit 1
|
|
}
|
|
|
|
while getopts "t:h" opt; do
|
|
case ${opt} in
|
|
t) timeout_val=$OPTARG ;;
|
|
h) usage ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
shift $((OPTIND - 1))
|
|
|
|
domain="$1"
|
|
[[ -z "$domain" ]] && usage
|
|
|
|
# INPUT VALIDATION
|
|
# rejects malformed hostnames before they reach dig/curl/openssl/whois
|
|
if [[ ! "$domain" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]]; then
|
|
printf "${red}error: '%s' is not a valid domain${nc}\n" "$domain"
|
|
exit 1
|
|
fi
|
|
|
|
# HELPER FUNCTIONS
|
|
# standardises section headers for clean output
|
|
section_header() {
|
|
printf "\n${cyan}%s${nc}\n" "$1"
|
|
}
|
|
|
|
# DEPENDENCY AUDIT
|
|
# validates required packages before execution
|
|
for cmd in dig curl jq openssl whois awk; do
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
printf "${red}error: missing dependency '%s'${nc}\n" "$cmd"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# DNS ANALYSIS
|
|
# extracts primary records and security configurations
|
|
section_header "DNS RECORDS"
|
|
ipv4=$(dig $resolver +short A "$domain")
|
|
ipv6=$(dig $resolver +short AAAA "$domain")
|
|
cname=$(dig $resolver +short CNAME "$domain")
|
|
mx=$(dig $resolver +short MX "$domain")
|
|
caa=$(dig $resolver +short CAA "$domain")
|
|
txt=$(dig $resolver +short TXT "$domain" | grep -Ei "spf|dmarc" || true)
|
|
dnskey=$(dig $resolver +short DNSKEY "$domain")
|
|
|
|
printf " ipv4:\n%s\n" "$([[ -n "$ipv4" ]] && echo "$ipv4" | sed 's/^/ /' || echo " n/a")"
|
|
printf " ipv6:\n%s\n" "$([[ -n "$ipv6" ]] && echo "$ipv6" | sed 's/^/ /' || echo " n/a")"
|
|
printf " cname:\n %s\n" "${cname:-none detected}"
|
|
printf " nameservers:\n%s\n" "$(dig $resolver +short NS "$domain" | sed 's/^/ /')"
|
|
printf " mail exchangers:\n%s\n" "$([[ -n "$mx" ]] && echo "$mx" | sed 's/^/ /' || echo " none detected")"
|
|
printf " caa records:\n%s\n" "$([[ -n "$caa" ]] && echo "$caa" | sed 's/^/ /' || echo " none detected")"
|
|
printf " security txt:\n%s\n" "$([[ -n "$txt" ]] && echo "$txt" | sed 's/^/ /' || echo " none detected")"
|
|
printf " dnssec: %s\n" "$([[ -n "$dnskey" ]] && echo "enabled" || echo "not detected")"
|
|
|
|
# REVERSE LOOKUP
|
|
# checks ptr records for resolved v4 and v6 addresses
|
|
section_header "REVERSE DNS"
|
|
if [[ -n "$ipv4" || -n "$ipv6" ]]; then
|
|
for ip in $ipv4 $ipv6; do
|
|
ptr=$(dig $resolver +short -x "$ip")
|
|
printf " %s -> %s\n" "$ip" "${ptr:-n/a}"
|
|
done
|
|
else
|
|
printf " n/a\n"
|
|
fi
|
|
|
|
# HTTP PERFORMANCE
|
|
# tests routing responses and redirection chains
|
|
section_header "HTTP ROUTING"
|
|
http_status=$(timeout "$timeout_val" curl -A "$ua" -sL -o /dev/null -w "%{http_code}" "http://$domain" || echo "timeout")
|
|
https_status=$(timeout "$timeout_val" curl -A "$ua" -sL -o /dev/null -w "%{http_code}" "https://$domain" 2>/dev/null || echo "timeout")
|
|
redirects=$(timeout "$timeout_val" curl -A "$ua" -sI -L "http://$domain" 2>/dev/null | grep -i "^location:" | sed 's/[Ll]ocation: //g' | tr -d '\r')
|
|
|
|
printf " http status: %s\n" "$http_status"
|
|
printf " https status: %s\n" "$https_status"
|
|
printf " redirects:\n%s\n" "$([[ -n "$redirects" ]] && echo "$redirects" | sed 's/^/ -> /' || echo " none")"
|
|
|
|
# ENCRYPTION AUDIT
|
|
# pulls subject, issuer, and expiration from the tls certificate
|
|
section_header "TLS CERTIFICATE"
|
|
tls=$(timeout "$timeout_val" openssl s_client -connect "$domain:443" -servername "$domain" -showcerts </dev/null 2>/dev/null)
|
|
expiry=$(echo "$tls" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
|
subject_issuer=$(echo "$tls" | grep -E "subject=|issuer=" | sed 's/^[ \t]*//')
|
|
printf " subject/issuer:\n%s\n" "$([[ -n "$subject_issuer" ]] && echo "$subject_issuer" | sed 's/^/ /' || echo " unknown")"
|
|
printf " expiry date: %s\n" "${expiry:-unknown}"
|
|
|
|
# SECURITY HEADERS
|
|
# evaluates presence of critical web security directives
|
|
section_header "SECURITY POSTURE"
|
|
headers=$(timeout "$timeout_val" curl -A "$ua" -sI "https://$domain" 2>/dev/null | tr -d '\r')
|
|
for h in "strict-transport-security" "content-security-policy" "x-frame-options" "x-content-type-options"; do
|
|
val=$(echo "$headers" | grep -i "^$h:" | cut -d: -f2- | xargs)
|
|
if [[ -n "$val" ]]; then
|
|
printf " %-25s ${green}present${nc}\n" "$h"
|
|
else
|
|
printf " %-25s ${yellow}missing${nc}\n" "$h"
|
|
fi
|
|
done
|
|
|
|
# INFRASTRUCTURE PROBE
|
|
# checks common critical ports on the primary resolved ip, results sorted after concurrent scan
|
|
section_header "PORT SCAN"
|
|
ports=(21 22 25 53 80 443 3306 8080 8443)
|
|
target_ip=$(echo "$ipv4" | head -n1)
|
|
|
|
if [[ -n "$target_ip" ]]; then
|
|
scan_tmp=$(mktemp)
|
|
exec 3>&2
|
|
exec 2>/dev/null
|
|
for port in "${ports[@]}"; do
|
|
(timeout 1 bash -c "</dev/tcp/$target_ip/$port" &>/dev/null && echo "$port" >> "$scan_tmp") &
|
|
done
|
|
wait
|
|
exec 2>&3
|
|
if [[ -s "$scan_tmp" ]]; then
|
|
sort -n "$scan_tmp" | while read -r port; do
|
|
printf " port %-5s ${green}open${nc}\n" "$port"
|
|
done
|
|
else
|
|
printf " no open ports detected\n"
|
|
fi
|
|
rm -f "$scan_tmp"
|
|
else
|
|
printf " ${red}no ipv4 address for scanning.${nc}\n"
|
|
fi
|
|
|
|
# ASSET ENUMERATION
|
|
# queries certificate transparency logs for child infrastructure via crt.sh json endpoint
|
|
section_header "SUBDOMAINS"
|
|
subdomains=$(timeout 15 curl -s "https://crt.sh/?q=%25.$domain&output=json" | jq -r '.[].name_value' 2>/dev/null | tr ',' '\n' | sed 's/\*\.//' | sort -u | head -n 15)
|
|
[[ -n "$subdomains" ]] && echo "$subdomains" | sed 's/^/ /' || echo " enumeration failed or none found"
|
|
|
|
# REGISTRY AUDIT
|
|
# extracts baseline domain registration metadata
|
|
section_header "WHOIS DATA"
|
|
whois_data=$(timeout "$timeout_val" whois "$domain" 2>/dev/null | grep -Ei "^registrar:|^creation date:|^updated date:" | tr -s ' ' | head -n 5)
|
|
[[ -n "$whois_data" ]] && echo "$whois_data" | sed 's/^/ /' || echo " extraction limited"
|
|
|
|
# RISK SCORE
|
|
# calculates a basic hazard rating based on missing protections and open ports
|
|
section_header "RISK SUMMARY"
|
|
risk=0
|
|
[[ ! "$https_status" =~ ^(2|3) ]] && ((risk += 2))
|
|
echo "$headers" | grep -qi "^strict-transport-security:" || ((risk += 1))
|
|
echo "$headers" | grep -qi "^content-security-policy:" || ((risk += 1))
|
|
[[ -z "$dnskey" ]] && ((risk += 1))
|
|
timeout 1 bash -c "</dev/tcp/$target_ip/22" &>/dev/null && ((risk += 1))
|
|
timeout 1 bash -c "</dev/tcp/$target_ip/21" &>/dev/null && ((risk += 2))
|
|
|
|
if [ $risk -le 1 ]; then
|
|
posture_color=$green
|
|
posture_text="low risk"
|
|
else
|
|
posture_color=$red
|
|
posture_text="risk identified"
|
|
fi
|
|
|
|
printf " domain: %s\n" "$domain"
|
|
printf " ip: %s\n" "${target_ip:-n/a}"
|
|
printf " posture: ${posture_color}%s${nc} (score: %d)\n\n" "$posture_text" "$risk"
|
|
|