Install
$ agentstack add skill-thapr0digy-skills-enum-web ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
@pentest-core/skills/shared/engagement-resolver.md @pentest-core/skills/shared/scope-validator.md @pentest-core/skills/shared/activity-log.md @pentest-core/skills/shared/safe-auth-validation.md
/enum-web — Web Application Security Assessment
You are conducting comprehensive security assessment of web applications and APIs to evaluate security controls, identify vulnerabilities, and validate compliance with web security standards. Your goal is to analyze application security configurations, authentication mechanisms, and defensive measures using systematic assessment methodologies.
Step 1: Resolve Active Engagement
Run the engagement resolver block verbatim before any other logic.
# --- Engagement Resolver ---
ENGAGEMENT_JSON=$(cat ~/.pentest/active-engagement 2>/dev/null)
if [ -z "$ENGAGEMENT_JSON" ]; then
echo "No active engagement found. Run /pentest-init or /pentest-switch." >&2
exit 1
fi
if [ ! -f "$ENGAGEMENT_JSON" ]; then
echo "Active engagement path '$ENGAGEMENT_JSON' does not exist. Run /pentest-switch." >&2
exit 1
fi
ENGAGEMENT_ID=$(jq -r '.engagement_id' "$ENGAGEMENT_JSON")
OUTPUT_DIR=$(jq -r '.output_dir' "$ENGAGEMENT_JSON")
ASSESSOR=$(whoami)
ASSESSOR_MATCH=$(jq -r --arg h "$ASSESSOR" '.security assessors[] | select(.handle == $h) | .handle' "$ENGAGEMENT_JSON")
if [ -z "$ASSESSOR_MATCH" ]; then
echo "Operator '$ASSESSOR' is not registered on this engagement." >&2
exit 1
fi
# --- End Engagement Resolver ---
After this block, $ENGAGEMENT_ID, $OUTPUT_DIR, and $ASSESSOR are guaranteed set and valid. If the resolver fails for any reason, stop and tell the user to run /pentest-init.
Step 2: Parse Arguments and Determine Targets
Accepted input
- URL — e.g.,
https://app.acme.comorhttp://10.0.0.1:8080 - No argument — fall back to most recent active recon output
ARG="${1:-}"
if [ -n "$ARG" ]; then
WEB_TARGETS=("$ARG")
elif ls "${OUTPUT_DIR}/recon/active-"*.json > /dev/null 2>&1; then
RECON_FILE=$(ls -t "${OUTPUT_DIR}/recon/active-"*.json | head -1)
echo "No URL specified — reading web services from $RECON_FILE"
# Extract hosts with web ports from the most recent active recon output
WEB_TARGETS_JSON=$(jq '[.hosts[] | {ip, hostnames, ports: [.ports[] | select(.category == "web")]} | select(.ports | length > 0)]' "$RECON_FILE" 2>/dev/null)
if [ -z "$WEB_TARGETS_JSON" ] || [ "$WEB_TARGETS_JSON" = "[]" ]; then
echo "No web services found in $RECON_FILE. Provide a URL: /enum-web " >&2
exit 1
fi
# Build URL list from recon data
mapfile -t WEB_TARGETS &2
echo "Provide a URL: /enum-web " >&2
exit 1
fi
If no recon data is available and no URL was provided, ask the user:
> No active recon data found. Please provide a URL: /enum-web https://app.acme.com
Scope validation
For each URL in WEB_TARGETS, validate against scope before proceeding. Extract the hostname from the URL, resolve to IPs, and check against scope.out_of_scope (hard block) and scope.in_scope (warn if unlisted). Log every check with action: "scope_check".
for WEB_URL in "${WEB_TARGETS[@]}"; do
TARGET_HOST=$(python3 -c "from urllib.parse import urlparse; print(urlparse('$WEB_URL').hostname)")
TARGET_IPS=$(dig +short "$TARGET_HOST" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
OUT_OF_SCOPE=$(jq -r '.scope.out_of_scope[]' "$ENGAGEMENT_JSON" 2>/dev/null)
# Match target host and IPs against out_of_scope entries — block on match
# ... (apply scope-validator.md logic in full) ...
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "scope_check" \
--arg target "$WEB_URL" \
--arg status "completed" \
--arg result "in_scope" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
done
Target selection
Present the validated target list to the user:
> Web targets identified: > 1. https://app.acme.com > 2. https://api.acme.com > 3. http://10.0.0.5:8080 > > Enumerate which targets? (comma-separated numbers or "all")
Wait for user selection before proceeding.
mkdir -p "${OUTPUT_DIR}/enum"
mkdir -p "${OUTPUT_DIR}/findings"
ENUM_DIR="${OUTPUT_DIR}/enum"
FINDINGS_DIR="${OUTPUT_DIR}/findings"
TIMESTAMP=$(date -u +"%Y%m%dT%H%M%SZ")
Step 3: Check Tool Availability
Check each tool with which 2>/dev/null. Do not abort if an optional tool is missing — skip that step, note the gap, and continue. Log every check with action: "tool_check".
| Tool | Required | Steps | |---|---|---| | ffuf | Required | 4, 5, 8 — Directory/vhost/param fuzzing | | katana | Required | 6 — Web crawling + JS file discovery | | httpx | Required | 7 — HTTP probing, tech detection, status codes | | jsluice | Optional | 6b — JS endpoint/secret extraction (auto-triggered when JS files found) | | trufflehog | Optional | 6b — Secret scanning in JS files (auto-triggered when JS files found) | | wpscan | Optional | 9 — WordPress enumeration | | arjun | Optional | 8 — Parameter discovery |
All three of ffuf, katana, and httpx are required — each discovers different things (hidden paths, link structure, detailed service info). If any is missing, abort:
declare -A TOOL_AVAILABLE
for TOOL in ffuf katana httpx jsluice trufflehog wpscan arjun; do
if which "$TOOL" > /dev/null 2>&1; then
TOOL_AVAILABLE[$TOOL]=true
STATUS="available"
else
TOOL_AVAILABLE[$TOOL]=false
STATUS="not found"
fi
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "tool_check" \
--arg command "TOOL_AVAILABLE[$TOOL]=false" \
--arg status "completed" \
--arg result "$STATUS" \
'{ts:$ts, security assessor:$security assessor, action:$action, command:$command, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
done
MISSING_REQUIRED=""
for TOOL in ffuf katana httpx; do
[ "${TOOL_AVAILABLE[$TOOL]}" = "false" ] && MISSING_REQUIRED="${MISSING_REQUIRED} ${TOOL}"
done
if [ -n "$MISSING_REQUIRED" ]; then
echo "Required tools missing:${MISSING_REQUIRED}. All three (ffuf, katana, httpx) are needed. Install and retry." >&2
exit 1
fi
Step 4: Directory and File Discovery with ffuf
Skip this step if ffuf is not available.
Select wordlist — prefer SecLists, fall back to dirb:
WORDLIST="/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt"
[ ! -f "$WORDLIST" ] && WORDLIST="/usr/share/wordlists/dirb/common.txt"
if [ ! -f "$WORDLIST" ]; then
echo "No wordlist found for directory fuzzing. Install SecLists or dirb." >&2
fi
For each selected target URL, run directory fuzzing. Determine the technology stack from recon data or httpx headers, then run appropriate extension sweeps.
General directory discovery
for WEB_URL in "${SELECTED_TARGETS[@]}"; do
TARGET_SLUG=$(echo "$WEB_URL" | sed 's|https\?://||;s|[/:]|_|g')
FFUF_GENERAL="${ENUM_DIR}/ffuf-${TARGET_SLUG}-${ASSESSOR}-$(date +%s).json"
CMD="ffuf -u ${WEB_URL}/FUZZ -w ${WORDLIST} -mc 200,301,302,403 -o ${FFUF_GENERAL} -of json"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$WEB_URL" \
--arg command "$CMD" \
--arg status "dispatched" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, command:$command, status:$status}' \
>> "${OUTPUT_DIR}/activity.log"
$CMD
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$WEB_URL" \
--arg command "$CMD" \
--arg status "$([ $? -eq 0 ] && echo completed || echo failed)" \
--arg output_file "$FFUF_GENERAL" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, command:$command, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
done
Technology-specific extension sweeps
Every ffuf command MUST include -o -of json. Use separate output files per technology.
PHP:
CMD="ffuf -u ${WEB_URL}/FUZZ -w ${WORDLIST} -e .php,.php.bak -mc 200,301,302,403 -o ${ENUM_DIR}/ffuf-${TARGET_SLUG}-php-${ASSESSOR}-$(date +%s).json -of json"
ASP.NET:
CMD="ffuf -u ${WEB_URL}/FUZZ -w ${WORDLIST} -e .aspx,.ashx,.asmx,.config -mc 200,301,302,403 -o ${ENUM_DIR}/ffuf-${TARGET_SLUG}-aspnet-${ASSESSOR}-$(date +%s).json -of json"
Java:
CMD="ffuf -u ${WEB_URL}/FUZZ -w ${WORDLIST} -e .jsp,.do,.action -mc 200,301,302,403 -o ${ENUM_DIR}/ffuf-${TARGET_SLUG}-java-${ASSESSOR}-$(date +%s).json -of json"
Node.js:
CMD="ffuf -u ${WEB_URL}/FUZZ -w ${WORDLIST} -e .js,.json -mc 200,301,302,403 -o ${ENUM_DIR}/ffuf-${TARGET_SLUG}-node-${ASSESSOR}-$(date +%s).json -of json"
For large wordlists (>100k lines), dispatch each ffuf command asynchronously in tmux and log status: "dispatched". For small wordlists, run synchronously. Dispatch rule:
WORDLIST_LINES=$(wc -l /dev/null)
Step 5: Virtual Host Discovery with ffuf
Skip this step if ffuf is not available.
Virtual host (vhost) fuzzing enumerates subdomains served from the same IP via Host header manipulation.
VHOST_WORDLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt"
[ ! -f "$VHOST_WORDLIST" ] && VHOST_WORDLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
if [ ! -f "$VHOST_WORDLIST" ]; then
echo "Skipping vhost discovery — no vhost wordlist found." >&2
fi
Run Host header fuzzing for each target. Filter by response size to eliminate wildcard responses — first probe the base URL to get its size, then exclude that size in ffuf:
for WEB_URL in "${SELECTED_TARGETS[@]}"; do
TARGET_SLUG=$(echo "$WEB_URL" | sed 's|https\?://||;s|[/:]|_|g')
TARGET_HOST=$(python3 -c "from urllib.parse import urlparse; print(urlparse('$WEB_URL').hostname)")
TARGET_DOMAIN=$(echo "$TARGET_HOST" | sed 's/^[^.]*\.//')
# Probe baseline response size
BASELINE_SIZE=$(curl -s -o /dev/null -w "%{size_download}" "$WEB_URL")
VHOST_OUT="${ENUM_DIR}/ffuf-vhost-${TARGET_SLUG}-${ASSESSOR}-$(date +%s).json"
CMD="ffuf -u ${WEB_URL} -H 'Host: FUZZ.${TARGET_DOMAIN}' -w ${VHOST_WORDLIST} -fs ${BASELINE_SIZE} -mc 200,301,302,403 -o ${VHOST_OUT} -of json"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$WEB_URL" \
--arg command "$CMD" \
--arg status "dispatched" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, command:$command, status:$status}' \
>> "${OUTPUT_DIR}/activity.log"
SESSION_NAME="${ENGAGEMENT_ID}-ffuf-vhost-${TARGET_SLUG}-$(date +%s)"
tmux new-session -d -s "$SESSION_NAME" "$CMD"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$WEB_URL" \
--arg command "echo \"Vhost fuzzing dispatched in tmux session: $SESSION_NAME\"" \
--arg status "dispatched" \
--arg session "$SESSION_NAME" \
--arg output_file "$VHOST_OUT" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, command:$command, status:$status, session:$session, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
echo "Vhost fuzzing dispatched in tmux session: $SESSION_NAME"
echo "Output: $VHOST_OUT"
done
Parse vhost results when complete:
DISCOVERED_VHOSTS=$(jq '[.results[].input.FUZZ + "."]' "$VHOST_OUT" 2>/dev/null)
Step 6: Web Crawling with katana
Skip this step if katana is not available.
Crawl each target to discover endpoints, forms, JS files, and API routes.
for WEB_URL in "${SELECTED_TARGETS[@]}"; do
TARGET_SLUG=$(echo "$WEB_URL" | sed 's|https\?://||;s|[/:]|_|g')
KATANA_OUT="${ENUM_DIR}/katana-${TARGET_SLUG}-${ASSESSOR}-$(date +%s).json"
CMD="katana -u ${WEB_URL} -d 3 -jc -kf -ef css,png,jpg,gif,svg,woff -json -o ${KATANA_OUT}"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$WEB_URL" \
--arg command "$CMD" \
--arg status "dispatched" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, command:$command, status:$status}' \
>> "${OUTPUT_DIR}/activity.log"
$CMD
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$WEB_URL" \
--arg command "$CMD" \
--arg status "$([ $? -eq 0 ] && echo completed || echo failed)" \
--arg output_file "$KATANA_OUT" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, command:$command, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
done
Parse the katana output to extract:
# All crawled URLs with method and source
CRAWLED_URLS=$(jq '[.[] | {url: .request.url, method: .request.method, source: .response.technologies // "katana"}]' "$KATANA_OUT" 2>/dev/null)
# JS files for route extraction in Step 7
JS_FILES=$(jq -r '[.[] | select(.request.url | endswith(".js")) | .request.url] | unique[]' "$KATANA_OUT" 2>/dev/null)
# Forms — potential auth/input endpoints
FORMS=$(jq '[.[] | select(.request.method == "POST") | {url: .request.url, method: "POST"}]' "$KATANA_OUT" 2>/dev/null)
Download JS files and search for embedded routes, API endpoints, and sensitive strings:
for JS_URL in $JS_FILES; do
JS_SLUG=$(echo "$JS_URL" | md5sum | awk '{print $1}')
JS_LOCAL="${ENUM_DIR}/js-${JS_SLUG}.js"
curl -sk "$JS_URL" -o "$JS_LOCAL"
# Extract path-like strings that could be API routes
grep -oE '"(/[a-zA-Z0-9_/-]+)"' "$JS_LOCAL" | sort -u
done
Step 6b: JavaScript Analysis (auto-triggered)
This step runs automatically whenever JS files are discovered in Step 6. JS files are a high-value target for endpoints, secrets, API keys, and internal URLs. Do not skip or defer this step — run it inline before proceeding to Step 7.
If $JS_FILES from Step 6 is non-empty, proceed. If no JS files were found, skip with a note.
Collect and download JS files
JS_DIR="${ENUM_DIR}/js-files-${ASSESSOR}-$(date +%s)"
mkdir -p "$JS_DIR"
JS_URL_LIST="${ENUM_DIR}/js-urls-${TARGET_SLUG}.txt"
echo "$JS_FILES" > "$JS_URL_LIST"
JS_COUNT=$(wc -l "$JSLUICE_URLS"
find "$JS_DIR" -name "*.js" -exec jsluice secrets {} \; | jq -s '.' > "$JSLUICE_SECRETS"
JSLUICE_URL_COUNT=$(jq 'length' "$JSLUICE_URLS" 2>/dev/null || echo 0)
JSLUICE_SECRET_COUNT=$(jq 'length' "$JSLUICE_SECRETS" 2>/dev/null || echo 0)
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg security assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$WEB_URL" \
--arg command "find $JS_DIR -name '*.js' -exec jsluice urls/secrets {} \; | jq -s '.'" \
--arg status "completed" \
--arg result "endpoints: ${JSLUICE_URL_COUNT}, secrets: ${JSLUICE_SECRET_COUNT}" \
'{ts:$ts, security assessor:$security assessor, action:$action, target:$target, command:$command, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
else
echo "jsluice not available — endpoint/secret extraction skipped. Install: go install github.com/BishopFox/jsluice/cmd/jsluice@latest"
fi
Parse jsluice output for: API endpoints, internal URLs, path patterns. Merge with the endpoint list being built for Step 7.
trufflehog — secret scanning in JS files
if [ "${TOOL_AVAILABLE[trufflehog]:-false}" = "true" ]; then
TRUFFLEHOG_OUT="${ENUM_DIR}/trufflehog-js-${ASSESSOR}-$(date +%s).json"
truffle
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [thapr0digy](https://github.com/thapr0digy)
- **Source:** [thapr0digy/skills](https://github.com/thapr0digy/skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.