Install
$ agentstack add skill-thapr0digy-skills-enum-js ✓ 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
@pentest-core/skills/shared/engagement-resolver.md @pentest-core/skills/shared/scope-validator.md @pentest-core/skills/shared/activity-log.md
/enum-js — JavaScript Analysis
You are performing deep analysis of JavaScript files to extract hidden endpoints, hardcoded secrets, API keys, and internal URLs. You use jsluice for AST-based analysis that finds things simple regex misses.
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")
TESTER=$(whoami)
TESTER_MATCH=$(jq -r --arg h "$TESTER" '.testers[] | select(.handle == $h) | .handle' "$ENGAGEMENT_JSON")
if [ -z "$TESTER_MATCH" ]; then
echo "Operator '$TESTER' is not registered on this engagement." >&2
exit 1
fi
# --- End Engagement Resolver ---
After this block, $ENGAGEMENT_ID, $OUTPUT_DIR, and $TESTER 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 Input
Accept any of the following input forms:
- Target URL — e.g.,
https://app.acme.com— skill crawls the target and collects JS files - File path — a file containing JS URLs, one per line
- Local directory — a directory of previously downloaded JS files
- No argument — check for existing katana output in
{OUTPUT_DIR}/enum/katana-*.jsonand extract JS URLs from it; if no katana output exists, check for gau/waybackurls output in{OUTPUT_DIR}/recon/and filter for.jsURLs
ARG="${1:-}"
INPUT_MODE=""
JS_URL_FILE=""
JS_DIR=""
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")
if [ -n "$ARG" ]; then
if [[ "$ARG" =~ ^https?:// ]]; then
INPUT_MODE="url"
TARGET_URL="$ARG"
elif [ -d "$ARG" ]; then
INPUT_MODE="directory"
JS_DIR="$ARG"
elif [ -f "$ARG" ]; then
INPUT_MODE="file"
JS_URL_FILE="$ARG"
else
echo "Argument '$ARG' is not a valid URL, file, or directory." >&2
exit 1
fi
else
# No argument — check for existing katana output
KATANA_FILES=$(ls "${OUTPUT_DIR}/enum/katana-"*.json 2>/dev/null | sort -t- -k3 -r | head -1)
if [ -n "$KATANA_FILES" ]; then
INPUT_MODE="katana"
KATANA_SOURCE="$KATANA_FILES"
echo "No argument provided — using katana output: $KATANA_SOURCE"
else
# Fall back to gau/waybackurls output
RECON_JS=$(ls "${OUTPUT_DIR}/recon/"*.txt "${OUTPUT_DIR}/recon/"*.json 2>/dev/null | head -1)
if [ -n "$RECON_JS" ]; then
INPUT_MODE="recon"
RECON_SOURCE="$RECON_JS"
echo "No katana output found — using recon data: $RECON_SOURCE"
else
echo "No argument provided and no existing katana or recon data found." >&2
echo "Provide a target: /enum-js " >&2
exit 1
fi
fi
fi
Scope validation (URL input only)
If a target URL was provided, validate it against the engagement scope before crawling.
if [ "$INPUT_MODE" = "url" ]; then
TARGET_HOST=$(python3 -c "from urllib.parse import urlparse; print(urlparse('$TARGET_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)
for OOS_ENTRY in $OUT_OF_SCOPE; do
if [[ "$TARGET_HOST" == *"$OOS_ENTRY"* ]] || echo "$TARGET_IPS" | grep -q "$OOS_ENTRY"; then
echo "[!] Target '$TARGET_URL' matches out-of-scope entry '$OOS_ENTRY'. Aborting." >&2
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg tester "$TESTER" \
--arg action "scope_check" \
--arg target "$TARGET_URL" \
--arg status "blocked" \
--arg result "out_of_scope: $OOS_ENTRY" \
'{ts:$ts, tester:$tester, action:$action, target:$target, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
exit 1
fi
done
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg tester "$TESTER" \
--arg action "scope_check" \
--arg target "$TARGET_URL" \
--arg status "completed" \
--arg result "in_scope" \
'{ts:$ts, tester:$tester, action:$action, target:$target, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
fi
Step 3: Check Tool Availability
Check each tool with which 2>/dev/null. Log every check with action: "tool_check".
| Tool | Required | Purpose | |---|---|---| | jsluice | Required | AST-based endpoint and secret extraction from JS | | katana | Optional | JS crawling when a target URL is provided |
If jsluice is not installed, abort with a clear install instruction:
> jsluice is required for this skill. Install with: go install github.com/BishopFox/jsluice/cmd/jsluice@latest
declare -A TOOL_AVAILABLE
for TOOL in jsluice katana; 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 tester "$TESTER" \
--arg action "tool_check" \
--arg command "TOOL_AVAILABLE[$TOOL]=false" \
--arg status "completed" \
--arg result "$STATUS" \
'{ts:$ts, tester:$tester, action:$action, command:$command, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
done
if [ "${TOOL_AVAILABLE[jsluice]}" = "false" ]; then
echo "jsluice is required for this skill. Install with: go install github.com/BishopFox/jsluice/cmd/jsluice@latest" >&2
exit 1
fi
if [ "$INPUT_MODE" = "url" ] && [ "${TOOL_AVAILABLE[katana]}" = "false" ]; then
echo "[!] katana is not available — cannot crawl target URL for JS files."
echo " Install katana: go install github.com/projectdiscovery/katana/cmd/katana@latest"
echo " Alternatively, provide a file of JS URLs or a local JS directory."
exit 1
fi
Step 4: Collect JavaScript Files
If target URL provided — crawl with katana
if [ "$INPUT_MODE" = "url" ]; then
JS_URL_FILE="$TMPDIR/js-urls.txt"
katana -u "$TARGET_URL" -d 3 -jc -ef css,png,jpg,gif,svg,woff -json \
| jq -r 'select(.endpoint | test("\\.(js|mjs)$")) | .endpoint' \
| sort -u > "$JS_URL_FILE"
JS_URL_COUNT=$(wc -l \"$JS_URL_FILE\"" \
--arg status "completed" \
--arg result "$JS_URL_COUNT JS URLs collected" \
'{ts:$ts, tester:$tester, action:$action, target:$target, command:$command, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
fi
# If using katana output from a previous run — extract JS URLs from it
if [ "$INPUT_MODE" = "katana" ]; then
JS_URL_FILE="$TMPDIR/js-urls.txt"
jq -r 'select(.endpoint | test("\\.(js|mjs)$")) | .endpoint' "$KATANA_SOURCE" \
| sort -u > "$JS_URL_FILE"
JS_URL_COUNT=$(wc -l "$JS_URL_FILE"
JS_URL_COUNT=$(wc -l "$JS_URL_MAP"
while read -r url; do
FILENAME=$(echo "$url" | md5sum | cut -d' ' -f1).js
curl -sL "$url" -o "$JS_DIR/$FILENAME"
echo "$url -> $FILENAME" >> "$JS_URL_MAP"
done > "${OUTPUT_DIR}/activity.log"
else
JS_FILE_COUNT=$(find "$JS_DIR" -name "*.js" | wc -l)
echo "Using local directory: $JS_DIR ($JS_FILE_COUNT JS files)"
fi
Log the collection to activity.log:
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg tester "$TESTER" \
--arg action "enum" \
--arg target "${TARGET_URL:-$JS_DIR}" \
--arg command "find $JS_DIR -name '*.js' | jsluice (urls|secrets)" \
--arg status "starting" \
--arg result "Analyzing $JS_FILE_COUNT JS files" \
'{ts:$ts, tester:$tester, action:$action, target:$target, command:$command, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
Step 5: Extract Endpoints
Run jsluice URL extraction across all JS files and deduplicate by URL:
JSLUICE_URLS_OUT="${ENUM_DIR}/jsluice-urls-${TESTER}-$(date +%s).json"
find "$JS_DIR" -name "*.js" -exec jsluice urls {} \; \
| jq -s 'unique_by(.url)' > "$JSLUICE_URLS_OUT"
TOTAL_URLS=$(jq 'length' "$JSLUICE_URLS_OUT")
echo "jsluice extracted $TOTAL_URLS unique URLs."
Parse and categorize the extracted URLs:
# API endpoints — paths containing /api/, /v1/, /v2/, /v3/, /graphql, /rest, etc.
API_ENDPOINTS=$(jq '[.[] | select(.url | test("/api/|/v[0-9]+/|/graphql|/rest/|/service/|/gql"))]' "$JSLUICE_URLS_OUT")
API_COUNT=$(echo "$API_ENDPOINTS" | jq 'length')
# Internal URLs — non-public hostnames, private IPs, .internal/.local domains
INTERNAL_URLS=$(jq '[.[] | select(.url | test("10\\.[0-9]+\\.[0-9]+\\.[0-9]+|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|localhost|127\\.0\\.0\\.1|\\.internal\\b|\\.local\\b|\\.corp\\b|\\.lan\\b"))]' "$JSLUICE_URLS_OUT")
INTERNAL_COUNT=$(echo "$INTERNAL_URLS" | jq 'length')
# Third-party integrations — known external APIs and services
THIRD_PARTY=$(jq '[.[] | select(.url | test("stripe\\.com|amazonaws\\.com|googleapis\\.com|twilio\\.com|sendgrid\\.com|segment\\.com|mixpanel\\.com|amplitude\\.com|datadog\\.com|sentry\\.io|auth0\\.com|okta\\.com|launchdarkly\\.com"))]' "$JSLUICE_URLS_OUT")
THIRD_PARTY_COUNT=$(echo "$THIRD_PARTY" | jq 'length')
# Path patterns — route templates with parameters
PATH_PATTERNS=$(jq '[.[] | select(.url | test(":[a-zA-Z_]+|\\{[a-zA-Z_]+\\}|\\[[a-zA-Z_]+\\]")) | .url]' "$JSLUICE_URLS_OUT")
PATTERN_COUNT=$(echo "$PATH_PATTERNS" | jq 'length')
echo "Categorized: $API_COUNT API endpoints, $INTERNAL_COUNT internal URLs, $THIRD_PARTY_COUNT third-party, $PATTERN_COUNT path patterns."
Log the extraction:
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg tester "$TESTER" \
--arg action "enum" \
--arg target "${TARGET_URL:-$JS_DIR}" \
--arg command "find $JS_DIR -name '*.js' -exec jsluice urls {} \; | jq -s 'unique_by(.url)' > $JSLUICE_URLS_OUT" \
--arg status "completed" \
--arg output_file "$JSLUICE_URLS_OUT" \
--arg result "URLs: total=$TOTAL_URLS api=$API_COUNT internal=$INTERNAL_COUNT third_party=$THIRD_PARTY_COUNT patterns=$PATTERN_COUNT" \
'{ts:$ts, tester:$tester, action:$action, target:$target, command:$command, status:$status, output_file:$output_file, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
Step 6: Extract Secrets
Run jsluice secrets extraction across all JS files:
JSLUICE_SECRETS_OUT="${ENUM_DIR}/jsluice-secrets-${TESTER}-$(date +%s).json"
find "$JS_DIR" -name "*.js" -exec jsluice secrets {} \; \
| jq -s '.' > "$JSLUICE_SECRETS_OUT"
TOTAL_SECRETS=$(jq 'length' "$JSLUICE_SECRETS_OUT")
echo "jsluice found $TOTAL_SECRETS secrets."
For each secret, generate a finding with all required fields. Map secret types to severity:
- AWS credentials (
AKIA...keys oraws_secret_access_key) — critical, CWE-798, CVSS 9.8 - API keys (generic, Google API key, Stripe secret/publishable, Twilio, etc.) — high, CWE-798, CVSS 8.2
- Tokens (JWT, Bearer, OAuth access/refresh tokens) — high, CWE-798, CVSS 8.2
- Passwords / connection strings (database URLs, DSNs, hardcoded passwords) — critical, CWE-798, CVSS 9.8
- Private keys (RSA/EC/PEM private keys) — critical, CWE-321, CVSS 9.1
FINDINGS_COUNT=0
# Resolve source file URL for a downloaded JS file using the url map
resolve_source_url() {
local filename="$1"
if [ -f "$TMPDIR/js-url-map.txt" ]; then
grep " -> $(basename "$filename")$" "$TMPDIR/js-url-map.txt" | awk '{print $1}' | head -1
else
echo "$filename"
fi
}
while IFS= read -r SECRET_JSON; do
SECRET_TYPE=$(echo "$SECRET_JSON" | jq -r '.kind // "unknown"')
SECRET_FILE=$(echo "$SECRET_JSON" | jq -r '.filename // "unknown"')
SECRET_CONTEXT=$(echo "$SECRET_JSON" | jq -r '.value // ""' | head -c 200)
SECRET_LINE=$(echo "$SECRET_JSON" | jq -r '.line // 0')
SOURCE_URL=$(resolve_source_url "$SECRET_FILE")
# Determine severity, CVSS, and CWE by type
case "$SECRET_TYPE" in
aws_access_key_id|aws_secret_access_key|AWSAccessKeyId)
SEVERITY="critical"
CVSS_VECTOR="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"
CVSS_SCORE=9.8
CWE="CWE-798"
TITLE="AWS Credential Exposed in JavaScript"
;;
private_key|rsa_private_key|ec_private_key|pem_private_key)
SEVERITY="critical"
CVSS_VECTOR="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N"
CVSS_SCORE=9.1
CWE="CWE-321"
TITLE="Private Key Exposed in JavaScript"
;;
password|db_password|database_url|connection_string)
SEVERITY="critical"
CVSS_VECTOR="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"
CVSS_SCORE=9.8
CWE="CWE-798"
TITLE="Hardcoded Password or Connection String in JavaScript"
;;
jwt|bearer_token|oauth_token|access_token|refresh_token)
SEVERITY="high"
CVSS_VECTOR="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
CVSS_SCORE=8.2
CWE="CWE-798"
TITLE="Authentication Token Exposed in JavaScript"
;;
*)
SEVERITY="high"
CVSS_VECTOR="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
CVSS_SCORE=7.5
CWE="CWE-798"
TITLE="API Key or Secret Exposed in JavaScript"
;;
esac
FINDING_ID="FINDING-JS-$(date +%s)-$FINDINGS_COUNT"
FINDING_FILE="${FINDINGS_DIR}/${FINDING_ID}-js-secret.json"
jq -nc \
--arg id "$FINDING_ID" \
--arg title "$TITLE" \
--arg severity "$SEVERITY" \
--arg cvss_vector "$CVSS_VECTOR" \
--argjson cvss_score "$CVSS_SCORE" \
--arg cwe "$CWE" \
--arg secret_type "$SECRET_TYPE" \
--arg source_url "$SOURCE_URL" \
--arg secret_file "$SECRET_FILE" \
--arg context "$SECRET_CONTEXT" \
--argjson line "$SECRET_LINE" \
--arg tester "$TESTER" \
--arg date "$(date -u +"%Y-%m-%d")" \
--arg engagement_id "$ENGAGEMENT_ID" \
'{
id: $id,
title: $title,
severity: $severity,
cvss: {vector: $cvss_vector, score: $cvss_score},
cwe: $cwe,
description: ("A \($secret_type) was found hardcoded in a JavaScript file served from \($source_url). Hardcoded credentials expose the application to unauthorized access if the JavaScript is publicly accessible."),
affected_assets: [$source_url],
evidence: {
steps: [
("Retrieve JavaScript file: \($source_url)"),
("Locate secret at line \($line | tostring) in \($secret_file)"),
("Identified secret type: \($secret_type)"),
("Context (truncated): \($context)")
]
},
impact: "An attacker with access to the JavaScript file can extract the \($secret_type) and use it to access protected resources, escalate privileges, or pivot to backend infrastructure.",
remediation: "Remove hardcoded credentials. Use environment variables or a secrets management solution (e.g., AWS Secrets Manager, HashiCorp Vault, Doppler). Rotate any exposed credentials immediately.",
references: [
"https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure",
"https://cwe.mitre.org/data/definitions/798.html",
"https://github.com/BishopFox/jsluice"
],
compliance_tags: ["OWASP A02:2021", "CWE-798"],
status: "confirmed",
found_by: $tester,
found_date: $date,
source_skill: "enum-js",
source_tool: "jsluice",
engagement_id: $engagement_id
}' > "$FINDING_FILE"
FINDINGS_COUNT=$((FINDINGS_COUNT + 1))
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg tester "$TESTER" \
--arg action "finding" \
…
## 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.