diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30a70ce..3101a73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,7 @@ jobs: command_exists ensure_command ensure_repo prompt_value prompt_yesno press_enter_to_continue kraken_term_width kraken_clear_screen kraken_print_separator + kraken_valid_target kraken_display_banner kraken_display_menu kraken_display_config kraken_initialize_session test_connectivity kraken_recon_run kraken_scan_run kraken_web_run diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..e28cbe5 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,10 @@ +# Kraken shellcheck configuration. +# Keep this in sync with SHELLCHECK_OPTS in .github/workflows/ci.yml so +# local linting matches CI. + +# SC1091: sourcing dynamic paths (lib/*.sh discovered at runtime). +disable=SC1091 + +# SC2034: color variables are exported for other modules and may not all +# be used inside a given file. +disable=SC2034 diff --git a/docs/IMPROVEMENTS.md b/docs/IMPROVEMENTS.md new file mode 100644 index 0000000..d355d11 --- /dev/null +++ b/docs/IMPROVEMENTS.md @@ -0,0 +1,70 @@ +# Kraken Improvements (v1.2) + +This document tracks the improvements applied to Kraken on top of the +v1.1 modular refactor. Each item maps to a single, self-contained +commit so the history stays bisectable. + +## Summary + +| # | Area | Improvement | Type | +|---|----------|--------------------------------------------------------|--------| +| 1 | logger | Persistent per-session log file (`kraken.log`) | feat | +| 2 | recon | AAAA / TXT / CNAME records, prefer `dig` over `host` | feat | +| 3 | web | Parallel directory enumeration with bounded jobs | perf | +| 4 | report | Markdown report alongside the plaintext report | feat | +| 5 | core | Target validation helper, reused across modules | feat | +| 6 | tooling | `.shellcheckrc` so local lint matches CI | chore | + +## Details + +### 1. Persistent session log + +Before, every `log_step` / `log_info` / `log_warn` / `log_error` / +`log_success` call wrote only to the terminal. A pentest leaves no +trace of what was run and when. `lib/logger.sh` now mirrors every log +line (timestamped, without color codes) into +`${KRAKEN_OUTPUT_DIR}/kraken.log` once a session is initialised. The +file is append-only across resumes of the same session, giving a clean +audit trail. + +### 2. Richer reconnaissance + +`lib/modules/recon.sh` previously queried only A / MX / NS records via +`host`. It now also collects AAAA (IPv6), TXT (SPF/DKIM/verification) +and CNAME records, and prefers `dig` when present because its output is +easier to parse and more reliable than `host`. `host` remains the +fallback, then `nslookup`, then `getent` - graceful degradation is +preserved. + +### 3. Parallel web directory enumeration + +`lib/modules/web.sh` probed each candidate path sequentially with a +blocking `curl`. With ~14 paths and a 5s timeout, a dead host could +stall the module for over a minute. Probes now run as bounded +background jobs (default 8 concurrent, override with +`KRAKEN_WEB_JOBS`), results are collected and sorted, then printed and +written deterministically. This matches the project's +parallel-execution convention. + +### 4. Markdown report + +`lib/modules/report.sh` produced a single plaintext report. It now also +writes a Markdown version (`REPORT_*.md`) with proper headings and +tables, suitable for pasting into tickets, wikis or client +deliverables. The plaintext report is unchanged so existing tooling +keeps working. + +### 5. Target validation + +A new `kraken_valid_target` helper in `lib/core.sh` rejects empty or +obviously malformed targets (shell metacharacters, whitespace) before +any external tool is invoked. Recon, scan and vuln modules call it +right after prompting, failing fast with a clear message instead of +feeding junk to `nmap` / `curl` / `host`. + +### 6. `.shellcheckrc` + +The CI workflow disabled SC1091 and SC2034 via `SHELLCHECK_OPTS`, but a +contributor running `shellcheck` locally got different results. A +repo-level `.shellcheckrc` now encodes the same exclusions so local and +CI linting agree. diff --git a/lib/core.sh b/lib/core.sh index 4ceb8c4..7fb538f 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -7,7 +7,7 @@ fi KRAKEN_CORE_LOADED=1 # Project metadata. -KRAKEN_VERSION="1.1.0" +KRAKEN_VERSION="1.2.0" KRAKEN_NAME="Kraken Pentest Framework" # Runtime globals (populated by lib/session.sh). @@ -44,6 +44,20 @@ export RESET BOLD DIM export RED GREEN YELLOW BLUE MAGENTA CYAN export BRIGHT_RED BRIGHT_GREEN BRIGHT_YELLOW BRIGHT_BLUE BRIGHT_MAGENTA BRIGHT_CYAN +# Validate a target (domain, hostname or IP). Rejects empty input and +# anything containing whitespace or shell metacharacters before it is +# handed to an external tool. Returns 0 when the target looks safe. +kraken_valid_target() { + local target="$1" + [[ -n "${target}" ]] || return 1 + # Reject whitespace and shell-dangerous characters. + [[ "${target}" =~ [[:space:]\;\|\&\$\`\(\)\<\>\"\'\\] ]] && return 1 + # Must contain only host-legal characters (alnum, dot, hyphen, colon + # for IPv6, and slash so a URL host can be pre-trimmed by the caller). + [[ "${target}" =~ ^[A-Za-z0-9._:/-]+$ ]] || return 1 + return 0 +} + # Width of the terminal, defaulting to 80 when stdout is not a TTY. kraken_term_width() { if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then diff --git a/lib/logger.sh b/lib/logger.sh index 30bf919..00e8592 100644 --- a/lib/logger.sh +++ b/lib/logger.sh @@ -10,28 +10,53 @@ _kraken_timestamp() { date '+%Y-%m-%d %H:%M:%S' } +# Plain tag (no color) used for the on-disk log mirror. +_kraken_plain_tag() { + case "$1" in + step) printf '[*]' ;; + info) printf '[i]' ;; + success) printf '[+]' ;; + warn) printf '[!]' ;; + error) printf '[x]' ;; + *) printf '[ ]' ;; + esac +} + +# Append a log line to the session log file, if a session is active. +# Color codes are stripped so the file stays grep-friendly. +_kraken_log_to_file() { + local level="$1" + local message="$2" + [[ -n "${KRAKEN_OUTPUT_DIR:-}" && -d "${KRAKEN_OUTPUT_DIR}" ]] || return 0 + printf '%s %s - %s\n' \ + "$(_kraken_plain_tag "${level}")" "$(_kraken_timestamp)" "${message}" \ + >> "${KRAKEN_OUTPUT_DIR}/kraken.log" 2>/dev/null || true +} + _kraken_log() { local tag="$1" local message="$2" + local level="${3:-}" printf '%b %s - %s\n' "${tag}" "$(_kraken_timestamp)" "${message}" + _kraken_log_to_file "${level}" "${message}" } log_step() { - _kraken_log "${BRIGHT_MAGENTA}[*]${RESET}" "$1" + _kraken_log "${BRIGHT_MAGENTA}[*]${RESET}" "$1" step } log_info() { - _kraken_log "${BRIGHT_BLUE}[i]${RESET}" "$1" + _kraken_log "${BRIGHT_BLUE}[i]${RESET}" "$1" info } log_success() { - _kraken_log "${BRIGHT_GREEN}[+]${RESET}" "$1" + _kraken_log "${BRIGHT_GREEN}[+]${RESET}" "$1" success } log_warn() { - _kraken_log "${BRIGHT_YELLOW}[!]${RESET}" "$1" + _kraken_log "${BRIGHT_YELLOW}[!]${RESET}" "$1" warn } log_error() { - _kraken_log "${BRIGHT_RED}[x]${RESET}" "$1" >&2 + _kraken_log "${BRIGHT_RED}[x]${RESET}" "$1" error >&2 } diff --git a/lib/modules/recon.sh b/lib/modules/recon.sh index 088a7f9..14126b7 100644 --- a/lib/modules/recon.sh +++ b/lib/modules/recon.sh @@ -6,21 +6,50 @@ if [[ -n "${KRAKEN_MODULE_RECON_LOADED:-}" ]]; then fi KRAKEN_MODULE_RECON_LOADED=1 +# Query a single record type with dig, printing a header and a fallback +# line when nothing is returned. +_kraken_recon_dig_record() { + local target="$1" rtype="$2" + echo "=== ${rtype} Records ===" + local answer + answer=$(dig +short "${rtype}" "${target}" 2>/dev/null) + if [[ -n "${answer}" ]]; then + printf '%s\n' "${answer}" + else + echo "No ${rtype} records found" + fi + echo +} + _kraken_recon_dns_records() { local target="$1" local out_file="$2" { echo "# DNS Records for ${target} - $(date)" echo - if command_exists host; then + if command_exists dig; then + local rtype + for rtype in A AAAA MX NS TXT CNAME; do + _kraken_recon_dig_record "${target}" "${rtype}" + done + elif command_exists host; then echo "=== A Records ===" host -t A "${target}" 2>/dev/null | grep "has address" || echo "No A records found" echo + echo "=== AAAA Records ===" + host -t AAAA "${target}" 2>/dev/null | grep "IPv6 address" || echo "No AAAA records" + echo echo "=== MX Records ===" host -t MX "${target}" 2>/dev/null | grep "mail is handled" || echo "No MX records" echo echo "=== NS Records ===" host -t NS "${target}" 2>/dev/null | grep "name server" || echo "No NS records" + echo + echo "=== TXT Records ===" + host -t TXT "${target}" 2>/dev/null | grep "descriptive text" || echo "No TXT records" + echo + echo "=== CNAME Records ===" + host -t CNAME "${target}" 2>/dev/null | grep "alias" || echo "No CNAME records" elif command_exists nslookup; then echo "=== DNS Info (nslookup) ===" nslookup "${target}" 2>/dev/null || echo "nslookup failed" @@ -64,14 +93,18 @@ _kraken_recon_whois() { _kraken_recon_reverse_dns() { local target="$1" local out_file="$2" - if ! command_exists host; then - return 0 - fi log_step "Attempting reverse DNS..." local ip - ip=$(host "${target}" 2>/dev/null | grep "has address" | head -1 | awk '{print $NF}') - if [[ -n "${ip}" ]]; then - host "${ip}" > "${out_file}" 2>/dev/null || true + if command_exists dig; then + ip=$(dig +short A "${target}" 2>/dev/null | head -1) + [[ -n "${ip}" ]] && dig +short -x "${ip}" > "${out_file}" 2>/dev/null || true + elif command_exists host; then + ip=$(host "${target}" 2>/dev/null | grep "has address" | head -1 | awk '{print $NF}') + [[ -n "${ip}" ]] && host "${ip}" > "${out_file}" 2>/dev/null || true + else + return 0 + fi + if [[ -n "${ip:-}" ]]; then log_success "Reverse DNS completed" fi } @@ -84,8 +117,8 @@ kraken_recon_run() { local target target=$(prompt_value "Enter target (domain or IP)") - if [[ -z "${target}" ]]; then - log_error "No target specified" + if ! kraken_valid_target "${target}"; then + log_error "Invalid or empty target" press_enter_to_continue return fi diff --git a/lib/modules/report.sh b/lib/modules/report.sh index 539841f..d62265b 100644 --- a/lib/modules/report.sh +++ b/lib/modules/report.sh @@ -33,7 +33,7 @@ _kraken_report_summary() { -exec grep -hc "open" {} + 2>/dev/null | awk '{s+=$1} END {print s+0}') fi targets=$(find "${KRAKEN_OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -type d 2>/dev/null \ - | sed -E 's|.*/(recon|scan|web|vuln)_||' | sort -u | tr '\n' ',' | sed 's/,$//') + | sed -E 's#.*/(recon|scan|web|vuln)_##' | sort -u | tr '\n' ',' | sed 's/,$//') cat </dev/null | grep -q .; then + total_findings=$(find "${KRAKEN_OUTPUT_DIR}" -name "findings.txt" \ + -exec cat {} + 2>/dev/null | grep -cE '^(MISSING_HEADER|HTTP_METHODS|INFO_DISCLOSURE):' || echo 0) + fi + hosts_scanned=$(find "${KRAKEN_OUTPUT_DIR}" -type d \ + \( -name "recon_*" -o -name "scan_*" \) 2>/dev/null | wc -l) + if find "${KRAKEN_OUTPUT_DIR}" -name "nmap_services.txt" -print -quit 2>/dev/null | grep -q .; then + ports_found=$(find "${KRAKEN_OUTPUT_DIR}" -name "nmap_services.txt" \ + -exec grep -hc "open" {} + 2>/dev/null | awk '{s+=$1} END {print s+0}') + fi + targets=$(find "${KRAKEN_OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -type d 2>/dev/null \ + | sed -E 's#.*/(recon|scan|web|vuln)_##' | sort -u | tr '\n' ',' | sed 's/,$//') + + cat </dev/null \ + | grep -q .; then + echo "_No data found._" + echo + return + fi + local dir target f + for dir in "${KRAKEN_OUTPUT_DIR}"/${prefix}*; do + [[ -d "${dir}" ]] || continue + target=$(basename "${dir}" | sed "s/${prefix}//") + echo "### ${target}" + echo + for f in "${dir}"/*.txt; do + [[ -f "${f}" ]] || continue + echo "**$(basename "${f}")**" + echo '```' + grep -vE '^#|^$' "${f}" 2>/dev/null | head -40 + echo '```' + echo + done + done +} + # Entry point for the report generation module. kraken_report_run() { kraken_clear_screen @@ -245,8 +332,10 @@ kraken_report_run() { return fi - local report_file - report_file="${KRAKEN_OUTPUT_DIR}/REPORT_$(date +%Y%m%d_%H%M%S).txt" + local stamp report_file md_file + stamp="$(date +%Y%m%d_%H%M%S)" + report_file="${KRAKEN_OUTPUT_DIR}/REPORT_${stamp}.txt" + md_file="${KRAKEN_OUTPUT_DIR}/REPORT_${stamp}.md" log_step "Collecting scan data..." { @@ -259,10 +348,14 @@ kraken_report_run() { _kraken_report_recommendations } > "${report_file}" + _kraken_report_markdown > "${md_file}" + log_success "Report generated!" echo - printf '%sReport saved to:%s\n %s%s%s\n\n' \ - "${BRIGHT_GREEN}" "${RESET}" "${BRIGHT_BLUE}" "${report_file}" "${RESET}" + printf '%sReports saved to:%s\n %s%s%s\n %s%s%s\n\n' \ + "${BRIGHT_GREEN}" "${RESET}" \ + "${BRIGHT_BLUE}" "${report_file}" "${RESET}" \ + "${BRIGHT_BLUE}" "${md_file}" "${RESET}" if prompt_yesno "View report now?" "no"; then echo diff --git a/lib/modules/scan.sh b/lib/modules/scan.sh index 6558983..a87d035 100644 --- a/lib/modules/scan.sh +++ b/lib/modules/scan.sh @@ -56,8 +56,8 @@ kraken_scan_run() { local target target=$(prompt_value "Enter target IP/domain") - if [[ -z "${target}" ]]; then - log_error "No target specified" + if ! kraken_valid_target "${target}"; then + log_error "Invalid or empty target" press_enter_to_continue return fi diff --git a/lib/modules/vuln.sh b/lib/modules/vuln.sh index 20761cd..71a5250 100644 --- a/lib/modules/vuln.sh +++ b/lib/modules/vuln.sh @@ -70,8 +70,8 @@ kraken_vuln_run() { local target target=$(prompt_value "Enter target (IP/domain)") - if [[ -z "${target}" ]]; then - log_error "No target specified" + if ! kraken_valid_target "${target}"; then + log_error "Invalid or empty target" press_enter_to_continue return fi diff --git a/lib/modules/web.sh b/lib/modules/web.sh index 2736d3c..da0d1db 100644 --- a/lib/modules/web.sh +++ b/lib/modules/web.sh @@ -49,13 +49,26 @@ _kraken_web_headers() { done } +# Number of concurrent probes during directory enumeration. +KRAKEN_WEB_JOBS="${KRAKEN_WEB_JOBS:-8}" + +# Probe one path, emitting a "STATUSpath" line for interesting codes. +_kraken_web_probe_path() { + local url="$1" dir="$2" + local status + status=$(_kraken_web_curl_status "${url}/${dir}") + case "${status}" in + 200|401|403) printf '%s\t%s\n' "${status}" "${dir}" ;; + esac +} + _kraken_web_directories() { local url="$1" local out_file="$2" if ! command_exists curl; then return fi - log_step "Testing common directories..." + log_step "Testing common directories (parallel, ${KRAKEN_WEB_JOBS} jobs)..." { echo "# Directory Scan - $(date)" echo "# Target: ${url}" @@ -65,25 +78,38 @@ _kraken_web_directories() { echo printf '%s═══ Directory Enumeration ═══%s\n' "${BRIGHT_MAGENTA}" "${RESET}" - local dir status test_url - for dir in "${KRAKEN_WEB_PATHS[@]}"; do - test_url="${url}/${dir}" - status=$(_kraken_web_curl_status "${test_url}") + # Fan out probes as bounded background jobs, collect raw results, sort + # them for deterministic output, then render and persist. + local raw dir + raw=$( + for dir in "${KRAKEN_WEB_PATHS[@]}"; do + while (( $(jobs -rp | wc -l) >= KRAKEN_WEB_JOBS )); do + wait -n 2>/dev/null || break + done + _kraken_web_probe_path "${url}" "${dir}" & + done + wait + ) + + [[ -z "${raw}" ]] && return + local status path + while IFS=$'\t' read -r status path; do + [[ -n "${status}" ]] || continue case "${status}" in 200) - printf ' %s[+] /%s%s (HTTP %s)\n' "${BRIGHT_GREEN}" "${dir}" "${RESET}" "${status}" - echo "FOUND: /${dir} (HTTP ${status})" >> "${out_file}" + printf ' %s[+] /%s%s (HTTP %s)\n' "${BRIGHT_GREEN}" "${path}" "${RESET}" "${status}" + echo "FOUND: /${path} (HTTP ${status})" >> "${out_file}" ;; 403) - printf ' %s[!] /%s%s (HTTP %s - Forbidden)\n' "${BRIGHT_YELLOW}" "${dir}" "${RESET}" "${status}" - echo "FORBIDDEN: /${dir} (HTTP ${status})" >> "${out_file}" + printf ' %s[!] /%s%s (HTTP %s - Forbidden)\n' "${BRIGHT_YELLOW}" "${path}" "${RESET}" "${status}" + echo "FORBIDDEN: /${path} (HTTP ${status})" >> "${out_file}" ;; 401) - printf ' %s[!] /%s%s (HTTP %s - Auth Required)\n' "${BRIGHT_YELLOW}" "${dir}" "${RESET}" "${status}" - echo "AUTH: /${dir} (HTTP ${status})" >> "${out_file}" + printf ' %s[!] /%s%s (HTTP %s - Auth Required)\n' "${BRIGHT_YELLOW}" "${path}" "${RESET}" "${status}" + echo "AUTH: /${path} (HTTP ${status})" >> "${out_file}" ;; esac - done + done < <(printf '%s\n' "${raw}" | sort) } _kraken_web_technologies() { @@ -139,6 +165,14 @@ kraken_web_run() { log_info "Assuming http:// protocol" fi + local host + host="${url#*://}"; host="${host%%/*}" + if ! kraken_valid_target "${host}"; then + log_error "Invalid URL host: ${host}" + press_enter_to_continue + return + fi + local slug slug=$(echo "${url}" | sed 's|https\?://||' | tr '/:' '_') local web_dir="${KRAKEN_OUTPUT_DIR}/web_${slug}"