AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Enum Js

skill-thapr0digy-skills-enum-js · by thapr0digy

Standalone JavaScript analysis — extract endpoints, secrets, API keys, and internal URLs from JS files using jsluice. Invoke via /enum-js [target-url-or-file].

— No reviews yet
0 installs
32 views
0.0% view→install

Install

$ agentstack add skill-thapr0digy-skills-enum-js

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-thapr0digy-skills-enum-js)

Reliability & compatibility

✓ Security review passed
0 installs to date
— no reviews yet
○ 4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Enum Js? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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-*.json and extract JS URLs from it; if no katana output exists, check for gau/waybackurls output in {OUTPUT_DIR}/recon/ and filter for .js URLs
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 or aws_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.

Versions

  • v0.1.0 Imported from the upstream source.