Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d25848cc5f | |||
| f32f4ffaee | |||
| 7379ba7b19 | |||
| 3aeb6d6356 | |||
| 4246a7b16f | |||
| e44fe49c8f | |||
| 52cd5fdfc1 | |||
| 947f1ad2b6 | |||
| cfcc82aaca | |||
| 0794e02b52 | |||
| 975965abed | |||
| a678bed154 | |||
| 4ec6f1eec0 | |||
| d1527376e7 | |||
| b99719ce60 | |||
| dc869852bc | |||
| 5cd769c2f4 | |||
| 977ad5c1a4 | |||
| 57a0ccef38 | |||
| 0e7219b191 | |||
| 1d2c4b134f | |||
| b245c462c9 | |||
| 66d9733da7 | |||
| c55deab3a2 | |||
| edcb697793 | |||
| d314d75db1 | |||
| c89a52caf7 | |||
| 9c18cfe273 | |||
| 779d4c33f4 | |||
| 072c24687b | |||
| b811b2bd47 | |||
| 355bfbd328 | |||
| 7b3632bdad | |||
| cd7c52e4fa | |||
| 86140af50e | |||
| e5cd5e5bfe | |||
| dc89f1cd27 | |||
| 388a1e06d4 | |||
| 61eeeb7876 | |||
| df7da4288c | |||
| 70896f1da4 | |||
| 0a38cad926 | |||
| 1e38fb6f7b | |||
| 9b3dc3e581 | |||
| 37b30602fd | |||
| 7afdee4c58 | |||
| d4d8e01e31 | |||
| e5e0da00fe | |||
| dc61cdc7a4 | |||
| 0fa2e1afc7 | |||
| 7ca90ba728 | |||
| cd6fa5bb30 | |||
| fa05641661 | |||
| 97ba4e8616 | |||
| 9882478fb5 | |||
| 9f5b7e1846 | |||
| 05afac7082 | |||
| ae362b0f02 | |||
| 435540606e | |||
| 96aa12c140 | |||
| 9560355a7c | |||
| b44ac231c1 | |||
| 7ff3924f0b | |||
| 39c3729524 | |||
| faddcbd15f | |||
| 78a2d309d1 | |||
| 35940e7584 | |||
| de81f38622 | |||
| 2016892e64 | |||
| 44ad8f506a | |||
| cfa4097df9 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
### REPOSITORY
|
||||
/.github/CODEOWNERS @sdushantha
|
||||
/.github/CODEOWNERS @sdushantha @ppfeister
|
||||
/.github/FUNDING.yml @sdushantha
|
||||
/LICENSE @sdushantha
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@ jobs:
|
||||
run: |
|
||||
git fetch origin exclusions || true
|
||||
|
||||
if git show origin/exclusions:exclusions.txt >/dev/null 2>&1; then
|
||||
if git show origin/exclusions:false_positive_exclusions.txt >/dev/null 2>&1; then
|
||||
# If the exclusions branch and file exist, compare
|
||||
if git diff --quiet origin/exclusions -- exclusions.txt; then
|
||||
if git diff --quiet origin/exclusions -- false_positive_exclusions.txt; then
|
||||
echo "exclusions_changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exclusions_changed=true" >> "$GITHUB_OUTPUT"
|
||||
@@ -71,10 +71,19 @@ jobs:
|
||||
git config user.name "Paul Pfeister (automation)"
|
||||
git config user.email "code@pfeister.dev"
|
||||
|
||||
mv false_positive_exclusions.txt false_positive_exclusions.txt.tmp
|
||||
|
||||
git add -f false_positive_exclusions.txt.tmp # -f required to override .gitignore
|
||||
git stash push -m "stash false positive exclusion list" -- false_positive_exclusions.txt.tmp
|
||||
|
||||
git fetch origin exclusions || true # Allows creation of branch if deleted
|
||||
git checkout -B exclusions origin/exclusions || (git checkout --orphan exclusions && git rm -rf .)
|
||||
|
||||
git add false_positive_exclusions.txt
|
||||
git stash pop || true
|
||||
|
||||
mv false_positive_exclusions.txt.tmp false_positive_exclusions.txt
|
||||
|
||||
git rm -f false_positive_exclusions.txt.tmp || true
|
||||
git add false_positive_exclusions.txt
|
||||
git commit -m "auto: update exclusions list" || echo "No changes to commit"
|
||||
git push origin exclusions
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
- '**/*.py'
|
||||
- '**/*.ini'
|
||||
- '**/*.toml'
|
||||
- 'Dockerfile'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -21,11 +22,13 @@ on:
|
||||
- '**/*.py'
|
||||
- '**/*.ini'
|
||||
- '**/*.toml'
|
||||
- 'Dockerfile'
|
||||
|
||||
jobs:
|
||||
tox-lint:
|
||||
# Linting is ran through tox to ensure that the same linter is used by local runners
|
||||
runs-on: ubuntu-latest
|
||||
# Linting is ran through tox to ensure that the same linter
|
||||
# is used by local runners
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up linting environment
|
||||
@@ -41,7 +44,8 @@ jobs:
|
||||
tox-matrix:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false # We want to know what specicic versions it fails on
|
||||
# We want to know what specicic versions it fails on
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [
|
||||
ubuntu-latest,
|
||||
@@ -67,3 +71,22 @@ jobs:
|
||||
pip install tox-gh-actions
|
||||
- name: Run tox
|
||||
run: tox
|
||||
docker-build-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Get version from pyproject.toml
|
||||
id: get-version
|
||||
run: |
|
||||
VERSION=$(grep -m1 'version = ' pyproject.toml | cut -d'"' -f2)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg VERSION_TAG=${{ steps.get-version.outputs.version }} \
|
||||
-t sherlock-test:latest .
|
||||
- name: Test Docker image runs
|
||||
run: docker run --rm sherlock-test:latest --version
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
name: Modified Target Validation
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "sherlock_project/resources/data.json"
|
||||
|
||||
jobs:
|
||||
validate-modified-targets:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install Poetry
|
||||
uses: abatilo/actions-poetry@v4
|
||||
with:
|
||||
poetry-version: 'latest'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --no-interaction --with dev
|
||||
|
||||
- name: Drop in place updated manifest from base
|
||||
run: |
|
||||
cp sherlock_project/resources/data.json data.json.base
|
||||
git fetch origin pull/${{ github.event.pull_request.number }}/head:pr --depth=1
|
||||
git show pr:sherlock_project/resources/data.json > sherlock_project/resources/data.json
|
||||
cp sherlock_project/resources/data.json data.json.head
|
||||
|
||||
- name: Discover modified targets
|
||||
id: discover-modified
|
||||
run: |
|
||||
CHANGED=$(
|
||||
python - <<'EOF'
|
||||
import json
|
||||
with open("data.json.base") as f: base = json.load(f)
|
||||
with open("data.json.head") as f: head = json.load(f)
|
||||
|
||||
changed = []
|
||||
for k, v in head.items():
|
||||
if k not in base or base[k] != v:
|
||||
changed.append(k)
|
||||
|
||||
print(",".join(sorted(changed)))
|
||||
EOF
|
||||
)
|
||||
|
||||
# Preserve changelist
|
||||
echo -e ">>> Changed targets: \n$(echo $CHANGED | tr ',' '\n')"
|
||||
echo "changed_targets=$CHANGED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate modified targets
|
||||
if: steps.discover-modified.outputs.changed_targets != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
poetry run pytest -q --tb no -rA -m validate_targets -n 20 \
|
||||
--chunked-sites "${{ steps.discover-modified.outputs.changed_targets }}" \
|
||||
--junitxml=validation_results.xml
|
||||
|
||||
- name: Prepare validation summary
|
||||
if: steps.discover-modified.outputs.changed_targets != ''
|
||||
id: prepare-summary
|
||||
run: |
|
||||
summary=$(
|
||||
poetry run python devel/summarize_site_validation.py validation_results.xml || echo "Failed to generate summary of test results"
|
||||
)
|
||||
echo "$summary" > validation_summary.md
|
||||
|
||||
- name: Announce validation results
|
||||
if: steps.discover-modified.outputs.changed_targets != ''
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const body = fs.readFileSync('validation_summary.md', 'utf8');
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.payload.pull_request.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: body,
|
||||
});
|
||||
|
||||
- name: This step shows as ran when no modifications are found
|
||||
if: steps.discover-modified.outputs.changed_targets == ''
|
||||
run: |
|
||||
echo "No modified targets found"
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# 3. Build image with BOTH latest and version tags
|
||||
# i.e. `docker build -t sherlock/sherlock:0.16.0 -t sherlock/sherlock:latest .`
|
||||
|
||||
FROM python:3.12-slim-bullseye as build
|
||||
FROM python:3.12-slim-bullseye AS build
|
||||
WORKDIR /sherlock
|
||||
|
||||
RUN pip3 install --no-cache-dir --upgrade pip
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
# This module summarizes the results of site validation tests queued by
|
||||
# workflow validate_modified_targets for presentation in Issue comments.
|
||||
|
||||
from defusedxml import ElementTree as ET
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def summarize_junit_xml(xml_path: Path) -> str:
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
suite = root.find('testsuite')
|
||||
|
||||
pass_message: str = ":heavy_check_mark: Pass"
|
||||
fail_message: str = ":x: Fail"
|
||||
|
||||
if suite is None:
|
||||
raise ValueError("Invalid JUnit XML: No testsuite found")
|
||||
|
||||
summary_lines: list[str] = []
|
||||
summary_lines.append("#### Automatic validation of changes\n")
|
||||
summary_lines.append("| Target | F+ Check | F- Check |")
|
||||
summary_lines.append("|---|---|---|")
|
||||
|
||||
failures = int(suite.get('failures', 0))
|
||||
errors_detected: bool = False
|
||||
|
||||
results: dict[str, dict[str, str]] = {}
|
||||
|
||||
for testcase in suite.findall('testcase'):
|
||||
test_name = testcase.get('name').split('[')[0]
|
||||
site_name = testcase.get('name').split('[')[1].rstrip(']')
|
||||
failure = testcase.find('failure')
|
||||
error = testcase.find('error')
|
||||
|
||||
if site_name not in results:
|
||||
results[site_name] = {}
|
||||
|
||||
if test_name == "test_false_neg":
|
||||
results[site_name]['F- Check'] = pass_message if failure is None and error is None else fail_message
|
||||
elif test_name == "test_false_pos":
|
||||
results[site_name]['F+ Check'] = pass_message if failure is None and error is None else fail_message
|
||||
|
||||
if error is not None:
|
||||
errors_detected = True
|
||||
|
||||
for result in results:
|
||||
summary_lines.append(f"| {result} | {results[result].get('F+ Check', 'Error!')} | {results[result].get('F- Check', 'Error!')} |")
|
||||
|
||||
if failures > 0:
|
||||
summary_lines.append("\n___\n" +
|
||||
"\nFailures were detected on at least one updated target. Commits containing accuracy failures" +
|
||||
" will often not be merged (unless a rationale is provided, such as false negatives due to regional differences).")
|
||||
|
||||
if errors_detected:
|
||||
summary_lines.append("\n___\n" +
|
||||
"\n**Errors were detected during validation. Please review the workflow logs.**")
|
||||
|
||||
return "\n".join(summary_lines)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: summarize_site_validation.py <junit-xml-file>")
|
||||
sys.exit(1)
|
||||
|
||||
xml_path: Path = Path(sys.argv[1])
|
||||
if not xml_path.is_file():
|
||||
print(f"Error: File '{xml_path}' does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
summary: str = summarize_junit_xml(xml_path)
|
||||
print(summary)
|
||||
+7
-6
@@ -8,8 +8,7 @@ source = "init"
|
||||
|
||||
[tool.poetry]
|
||||
name = "sherlock-project"
|
||||
# single source of truth for version is __init__.py
|
||||
version = "0"
|
||||
version = "0.16.0"
|
||||
description = "Hunt down social media accounts by username across social networks"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
@@ -47,12 +46,10 @@ PySocks = "^1.7.0"
|
||||
requests = "^2.22.0"
|
||||
requests-futures = "^1.0.0"
|
||||
stem = "^1.8.0"
|
||||
torrequest = "^0.1.0"
|
||||
pandas = "^2.2.1"
|
||||
openpyxl = "^3.0.10"
|
||||
|
||||
[tool.poetry.extras]
|
||||
tor = ["torrequest"]
|
||||
tomli = "^2.2.1"
|
||||
pyyaml = "^6.0.3"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
jsonschema = "^4.0.0"
|
||||
@@ -60,5 +57,9 @@ rstr = "^3.2.2"
|
||||
pytest = "^8.4.2"
|
||||
pytest-xdist = "^3.8.0"
|
||||
|
||||
|
||||
[tool.poetry.group.ci.dependencies]
|
||||
defusedxml = "^0.7.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
sherlock = 'sherlock_project.sherlock:main'
|
||||
|
||||
@@ -5,11 +5,26 @@ networks.
|
||||
|
||||
"""
|
||||
|
||||
from importlib.metadata import version as pkg_version, PackageNotFoundError
|
||||
import pathlib
|
||||
import tomli
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
"""Fetch the version number of the installed package."""
|
||||
try:
|
||||
return pkg_version("sherlock_project")
|
||||
except PackageNotFoundError:
|
||||
pyproject_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent / "pyproject.toml"
|
||||
with pyproject_path.open("rb") as f:
|
||||
pyproject_data = tomli.load(f)
|
||||
return pyproject_data["tool"]["poetry"]["version"]
|
||||
|
||||
# This variable is only used to check for ImportErrors induced by users running as script rather than as module or package
|
||||
import_error_test_var = None
|
||||
|
||||
__shortname__ = "Sherlock"
|
||||
__longname__ = "Sherlock: Find Usernames Across Social Networks"
|
||||
__version__ = "0.16.0"
|
||||
__version__ = get_version()
|
||||
|
||||
forge_api_latest_release = "https://api.github.com/repos/sherlock-project/sherlock/releases/latest"
|
||||
|
||||
@@ -79,13 +79,13 @@
|
||||
"username_claimed": "pink"
|
||||
},
|
||||
"AllMyLinks": {
|
||||
"errorMsg": "Not Found",
|
||||
"errorType": "message",
|
||||
"regexCheck": "^[a-z0-9][a-z0-9-]{2,32}$",
|
||||
"url": "https://allmylinks.com/{}",
|
||||
"urlMain": "https://allmylinks.com/",
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"errorMsg": "Page not found",
|
||||
"errorType": "message",
|
||||
"regexCheck": "^[a-z0-9][a-z0-9-]{2,32}$",
|
||||
"url": "https://allmylinks.com/{}",
|
||||
"urlMain": "https://allmylinks.com/",
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"AniWorld": {
|
||||
"errorMsg": "Dieses Profil ist nicht verf\u00fcgbar",
|
||||
"errorType": "message",
|
||||
@@ -115,12 +115,20 @@
|
||||
"username_claimed": "lio24d"
|
||||
},
|
||||
"Apple Discussions": {
|
||||
"errorMsg": "The page you tried was not found. You may have used an outdated link or may have typed the address (URL) incorrectly.",
|
||||
"errorMsg": "Looking for something in Apple Support Communities?",
|
||||
"errorType": "message",
|
||||
"url": "https://discussions.apple.com/profile/{}",
|
||||
"urlMain": "https://discussions.apple.com",
|
||||
"username_claimed": "jason"
|
||||
},
|
||||
"Aparat": {
|
||||
"errorType": "status_code",
|
||||
"request_method": "GET",
|
||||
"url": "https://www.aparat.com/{}/",
|
||||
"urlMain": "https://www.aparat.com/",
|
||||
"urlProbe": "https://www.aparat.com/api/fa/v1/user/user/information/username/{}",
|
||||
"username_claimed": "jadi"
|
||||
},
|
||||
"Archive of Our Own": {
|
||||
"errorType": "status_code",
|
||||
"regexCheck": "^[^.]*?$",
|
||||
@@ -564,8 +572,7 @@
|
||||
"username_claimed": "brown"
|
||||
},
|
||||
"CyberDefenders": {
|
||||
"errorMsg": "<title>Blue Team Training for SOC analysts and DFIR - CyberDefenders</title>",
|
||||
"errorType": "message",
|
||||
"errorType": "status_code",
|
||||
"regexCheck": "^[^\\/:*?\"<>|@]{3,50}$",
|
||||
"request_method": "GET",
|
||||
"url": "https://cyberdefenders.org/p/{}",
|
||||
@@ -592,6 +599,12 @@
|
||||
"urlMain": "https://www.dailymotion.com/",
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"dcinside": {
|
||||
"errorType": "status_code",
|
||||
"url": "https://gallog.dcinside.com/{}",
|
||||
"urlMain": "https://www.dcinside.com/",
|
||||
"username_claimed": "anrbrb"
|
||||
},
|
||||
"Dealabs": {
|
||||
"errorMsg": "La page que vous essayez",
|
||||
"errorType": "message",
|
||||
@@ -600,13 +613,14 @@
|
||||
"urlMain": "https://www.dealabs.com/",
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"DeviantART": {
|
||||
"errorType": "status_code",
|
||||
"regexCheck": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
||||
"url": "https://{}.deviantart.com",
|
||||
"urlMain": "https://deviantart.com",
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"DeviantArt": {
|
||||
"errorType": "message",
|
||||
"errorMsg": "Llama Not Found",
|
||||
"regexCheck": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
||||
"url": "https://www.deviantart.com/{}",
|
||||
"urlMain": "https://www.deviantart.com/",
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"DigitalSpy": {
|
||||
"errorMsg": "The page you were looking for could not be found.",
|
||||
"errorType": "message",
|
||||
@@ -807,14 +821,6 @@
|
||||
"urlMain": "https://framapiaf.org",
|
||||
"username_claimed": "pylapp"
|
||||
},
|
||||
"Freelance.habr": {
|
||||
"errorMsg": "<div class=\"icon_user_locked\"></div>",
|
||||
"errorType": "message",
|
||||
"regexCheck": "^((?!\\.).)*$",
|
||||
"url": "https://freelance.habr.com/freelancers/{}",
|
||||
"urlMain": "https://freelance.habr.com/",
|
||||
"username_claimed": "adam"
|
||||
},
|
||||
"Freelancer": {
|
||||
"errorMsg": "\"users\":{}",
|
||||
"errorType": "message",
|
||||
@@ -1142,6 +1148,13 @@
|
||||
"urlProbe": "https://imginn.com/{}",
|
||||
"username_claimed": "instagram"
|
||||
},
|
||||
"Instapaper": {
|
||||
"errorType": "status_code",
|
||||
"request_method": "GET",
|
||||
"url": "https://www.instapaper.com/p/{}",
|
||||
"urlMain": "https://www.instapaper.com/",
|
||||
"username_claimed": "john"
|
||||
},
|
||||
"Instructables": {
|
||||
"errorType": "status_code",
|
||||
"url": "https://www.instructables.com/member/{}",
|
||||
@@ -1254,6 +1267,13 @@
|
||||
"urlMain": "https://linux.org.ru/",
|
||||
"username_claimed": "red"
|
||||
},
|
||||
"Laracast": {
|
||||
"errorType":"status_code",
|
||||
"url": "https://laracasts.com/@{}",
|
||||
"urlMain": "https://laracasts.com/",
|
||||
"regexCheck": "^[a-zA-Z0-9_-]{3,}$",
|
||||
"username_claimed": "user1"
|
||||
},
|
||||
"Launchpad": {
|
||||
"errorType": "status_code",
|
||||
"url": "https://launchpad.net/~{}",
|
||||
@@ -1426,12 +1446,12 @@
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"Mydramalist": {
|
||||
"errorMsg": "Sign in - MyDramaList",
|
||||
"errorType": "message",
|
||||
"url": "https://www.mydramalist.com/profile/{}",
|
||||
"urlMain": "https://mydramalist.com",
|
||||
"username_claimed": "elhadidy12398"
|
||||
},
|
||||
"errorMsg": "The requested page was not found",
|
||||
"errorType": "message",
|
||||
"url": "https://www.mydramalist.com/profile/{}",
|
||||
"urlMain": "https://mydramalist.com",
|
||||
"username_claimed": "elhadidy12398"
|
||||
},
|
||||
"Myspace": {
|
||||
"errorType": "status_code",
|
||||
"url": "https://myspace.com/{}",
|
||||
@@ -1445,6 +1465,13 @@
|
||||
"urlMain": "https://www.native-instruments.com/forum/",
|
||||
"username_claimed": "jambert"
|
||||
},
|
||||
"namuwiki": {
|
||||
"__comment__": "This is a Korean site and it's expected to return false negatives in certain other regions.",
|
||||
"errorType": "status_code",
|
||||
"url": "https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:{}",
|
||||
"urlMain": "https://namu.wiki/",
|
||||
"username_claimed": "namu"
|
||||
},
|
||||
"NationStates Nation": {
|
||||
"errorMsg": "Was this your nation? It may have ceased to exist due to inactivity, but can rise again!",
|
||||
"errorType": "message",
|
||||
@@ -1558,7 +1585,7 @@
|
||||
"url": "https://outgress.com/agents/{}",
|
||||
"urlMain": "https://outgress.com/",
|
||||
"username_claimed": "pylapp"
|
||||
},
|
||||
},
|
||||
"PCGamer": {
|
||||
"errorMsg": "The specified member cannot be found. Please enter a member's entire name.",
|
||||
"errorType": "message",
|
||||
@@ -1795,8 +1822,7 @@
|
||||
"username_claimed": "blue"
|
||||
},
|
||||
"Roblox": {
|
||||
"errorMsg": "Page cannot be found or no longer exists",
|
||||
"errorType": "message",
|
||||
"errorType": "status_code",
|
||||
"url": "https://www.roblox.com/user.aspx?username={}",
|
||||
"urlMain": "https://www.roblox.com/",
|
||||
"username_claimed": "bluewolfekiller"
|
||||
@@ -1904,7 +1930,7 @@
|
||||
},
|
||||
"SlideShare": {
|
||||
"errorType": "message",
|
||||
"errorMsg": "<title>Username available</title>",
|
||||
"errorMsg": "<title>Page no longer exists</title>",
|
||||
"url": "https://slideshare.net/{}",
|
||||
"urlMain": "https://slideshare.net/",
|
||||
"username_claimed": "blue"
|
||||
@@ -1938,6 +1964,13 @@
|
||||
"urlMain": "https://www.snapchat.com",
|
||||
"username_claimed": "teamsnapchat"
|
||||
},
|
||||
"SOOP": {
|
||||
"errorType": "status_code",
|
||||
"url": "https://www.sooplive.co.kr/station/{}",
|
||||
"urlMain": "https://www.sooplive.co.kr/",
|
||||
"urlProbe": "https://api-channel.sooplive.co.kr/v1.1/channel/{}/station",
|
||||
"username_claimed": "udkn"
|
||||
},
|
||||
"SoundCloud": {
|
||||
"errorType": "status_code",
|
||||
"url": "https://soundcloud.com/{}",
|
||||
@@ -2105,6 +2138,12 @@
|
||||
"urlMain": "https://themeforest.net/",
|
||||
"username_claimed": "user"
|
||||
},
|
||||
"tistory": {
|
||||
"errorType": "status_code",
|
||||
"url": "https://{}.tistory.com/",
|
||||
"urlMain": "https://www.tistory.com/",
|
||||
"username_claimed": "notice"
|
||||
},
|
||||
"TnAFlix": {
|
||||
"errorType": "status_code",
|
||||
"isNSFW": true,
|
||||
@@ -2779,7 +2818,7 @@
|
||||
"username_claimed": "green"
|
||||
},
|
||||
"threads": {
|
||||
"errorMsg": "<title>Threads</title>",
|
||||
"errorMsg": "<title>Threads • Log in</title>",
|
||||
"errorType": "message",
|
||||
"headers": {
|
||||
"Sec-Fetch-Mode": "navigate"
|
||||
|
||||
@@ -1,80 +1,149 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Sherlock Target Manifest",
|
||||
"description": "Social media targets to probe for the existence of known usernames",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": { "type": "string" }
|
||||
},
|
||||
"patternProperties": {
|
||||
"^(?!\\$).*?$": {
|
||||
"type": "object",
|
||||
"description": "Target name and associated information (key should be human readable name)",
|
||||
"required": [ "url", "urlMain", "errorType", "username_claimed" ],
|
||||
"properties": {
|
||||
"url": { "type": "string" },
|
||||
"urlMain": { "type": "string" },
|
||||
"urlProbe": { "type": "string" },
|
||||
"username_claimed": { "type": "string" },
|
||||
"regexCheck": { "type": "string" },
|
||||
"isNSFW": { "type": "boolean" },
|
||||
"headers": { "type": "object" },
|
||||
"request_payload": { "type": "object" },
|
||||
"__comment__": {
|
||||
"type": "string",
|
||||
"description": "Used to clarify important target information if (and only if) a commit message would not suffice.\nThis key should not be parsed anywhere within Sherlock."
|
||||
},
|
||||
"tags": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/tag" },
|
||||
{ "type": "array", "items": { "$ref": "#/$defs/tag" } }
|
||||
]
|
||||
},
|
||||
"request_method": {
|
||||
"type": "string",
|
||||
"enum": [ "GET", "POST", "HEAD", "PUT" ]
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Sherlock Target Manifest",
|
||||
"description": "Social media targets to probe for the existence of known usernames",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": { "type": "string" }
|
||||
},
|
||||
"patternProperties": {
|
||||
"^(?!\\$).*?$": {
|
||||
"type": "object",
|
||||
"description": "Target name and associated information (key should be human readable name)",
|
||||
"required": ["url", "urlMain", "errorType", "username_claimed"],
|
||||
"properties": {
|
||||
"url": { "type": "string" },
|
||||
"urlMain": { "type": "string" },
|
||||
"urlProbe": { "type": "string" },
|
||||
"username_claimed": { "type": "string" },
|
||||
"regexCheck": { "type": "string" },
|
||||
"isNSFW": { "type": "boolean" },
|
||||
"headers": { "type": "object" },
|
||||
"request_payload": { "type": "object" },
|
||||
"__comment__": {
|
||||
"type": "string",
|
||||
"description": "Used to clarify important target information if (and only if) a commit message would not suffice.\nThis key should not be parsed anywhere within Sherlock."
|
||||
},
|
||||
"tags": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/tag" },
|
||||
{ "type": "array", "items": { "$ref": "#/$defs/tag" } }
|
||||
]
|
||||
},
|
||||
"request_method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST", "HEAD", "PUT"]
|
||||
},
|
||||
"errorType": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["message", "response_url", "status_code"]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["message", "response_url", "status_code"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMsg": {
|
||||
"oneOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "array", "items": { "type": "string" } }
|
||||
]
|
||||
},
|
||||
"errorCode": {
|
||||
"oneOf": [
|
||||
{ "type": "integer" },
|
||||
{ "type": "array", "items": { "type": "integer" } }
|
||||
]
|
||||
},
|
||||
"errorUrl": { "type": "string" },
|
||||
"response_url": { "type": "string" }
|
||||
},
|
||||
"dependencies": {
|
||||
"errorMsg": {
|
||||
"oneOf": [
|
||||
{ "properties": { "errorType": { "const": "message" } } },
|
||||
{
|
||||
"properties": {
|
||||
"errorType": {
|
||||
"type": "string",
|
||||
"enum": [ "message", "response_url", "status_code" ]
|
||||
},
|
||||
"errorMsg": {
|
||||
"oneOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "array", "items": { "type": "string" } }
|
||||
]
|
||||
},
|
||||
"errorCode": {
|
||||
"oneOf": [
|
||||
{ "type": "integer" },
|
||||
{ "type": "array", "items": { "type": "integer" } }
|
||||
]
|
||||
},
|
||||
"errorUrl": { "type": "string" },
|
||||
"response_url": { "type": "string" }
|
||||
},
|
||||
"dependencies": {
|
||||
"errorMsg": {
|
||||
"properties" : { "errorType": { "const": "message" } }
|
||||
},
|
||||
"errorUrl": {
|
||||
"properties": { "errorType": { "const": "response_url" } }
|
||||
},
|
||||
"errorCode": {
|
||||
"properties": { "errorType": { "const": "status_code" } }
|
||||
"type": "array",
|
||||
"contains": { "const": "message" }
|
||||
}
|
||||
},
|
||||
"if": { "properties": { "errorType": { "const": "message" } } },
|
||||
"then": { "required": [ "errorMsg" ] },
|
||||
"else": {
|
||||
"if": { "properties": { "errorType": { "const": "response_url" } } },
|
||||
"then": { "required": [ "errorUrl" ] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorUrl": {
|
||||
"oneOf": [
|
||||
{ "properties": { "errorType": { "const": "response_url" } } },
|
||||
{
|
||||
"properties": {
|
||||
"errorType": {
|
||||
"type": "array",
|
||||
"contains": { "const": "response_url" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorCode": {
|
||||
"oneOf": [
|
||||
{ "properties": { "errorType": { "const": "status_code" } } },
|
||||
{
|
||||
"properties": {
|
||||
"errorType": {
|
||||
"type": "array",
|
||||
"contains": { "const": "status_code" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"tag": { "type": "string", "enum": [ "adult", "gaming" ] }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"anyOf": [
|
||||
{ "properties": { "errorType": { "const": "message" } } },
|
||||
{
|
||||
"properties": {
|
||||
"errorType": {
|
||||
"type": "array",
|
||||
"contains": { "const": "message" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"then": { "required": ["errorMsg"] }
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"anyOf": [
|
||||
{ "properties": { "errorType": { "const": "response_url" } } },
|
||||
{
|
||||
"properties": {
|
||||
"errorType": {
|
||||
"type": "array",
|
||||
"contains": { "const": "response_url" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"then": { "required": ["errorUrl"] }
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"tag": { "type": "string", "enum": ["adult", "gaming"] }
|
||||
}
|
||||
}
|
||||
|
||||
+71
-119
@@ -171,8 +171,6 @@ def sherlock(
|
||||
username: str,
|
||||
site_data: dict[str, dict[str, str]],
|
||||
query_notify: QueryNotify,
|
||||
tor: bool = False,
|
||||
unique_tor: bool = False,
|
||||
dump_response: bool = False,
|
||||
proxy: Optional[str] = None,
|
||||
timeout: int = 60,
|
||||
@@ -188,8 +186,6 @@ def sherlock(
|
||||
query_notify -- Object with base type of QueryNotify().
|
||||
This will be used to notify the caller about
|
||||
query results.
|
||||
tor -- Boolean indicating whether to use a tor circuit for the requests.
|
||||
unique_tor -- Boolean indicating whether to use a new tor circuit for each request.
|
||||
proxy -- String indicating the proxy URL
|
||||
timeout -- Time in seconds to wait before timing out request.
|
||||
Default is 60 seconds.
|
||||
@@ -210,32 +206,9 @@ def sherlock(
|
||||
|
||||
# Notify caller that we are starting the query.
|
||||
query_notify.start(username)
|
||||
# Create session based on request methodology
|
||||
if tor or unique_tor:
|
||||
try:
|
||||
from torrequest import TorRequest # noqa: E402
|
||||
except ImportError:
|
||||
print("Important!")
|
||||
print("> --tor and --unique-tor are now DEPRECATED, and may be removed in a future release of Sherlock.")
|
||||
print("> If you've installed Sherlock via pip, you can include the optional dependency via `pip install 'sherlock-project[tor]'`.")
|
||||
print("> Other packages should refer to their documentation, or install it separately with `pip install torrequest`.\n")
|
||||
sys.exit(query_notify.finish())
|
||||
|
||||
print("Important!")
|
||||
print("> --tor and --unique-tor are now DEPRECATED, and may be removed in a future release of Sherlock.")
|
||||
|
||||
# Requests using Tor obfuscation
|
||||
try:
|
||||
underlying_request = TorRequest()
|
||||
except OSError:
|
||||
print("Tor not found in system path. Unable to continue.\n")
|
||||
sys.exit(query_notify.finish())
|
||||
|
||||
underlying_session = underlying_request.session
|
||||
else:
|
||||
# Normal requests
|
||||
underlying_session = requests.session()
|
||||
underlying_request = requests.Request()
|
||||
# Normal requests
|
||||
underlying_session = requests.session()
|
||||
|
||||
# Limit number of workers to 20.
|
||||
# This is probably vastly overkill.
|
||||
@@ -359,15 +332,10 @@ def sherlock(
|
||||
# Store future in data for access later
|
||||
net_info["request_future"] = future
|
||||
|
||||
# Reset identify for tor (if needed)
|
||||
if unique_tor:
|
||||
underlying_request.reset_identity()
|
||||
|
||||
# Add this site's results into final dictionary with all the other results.
|
||||
results_total[social_network] = results_site
|
||||
|
||||
# Open the file containing account links
|
||||
# Core logic: If tor requests, make them here. If multi-threaded requests, wait for responses
|
||||
for social_network, net_info in site_data.items():
|
||||
# Retrieve results again
|
||||
results_site = results_total.get(social_network)
|
||||
@@ -381,6 +349,8 @@ def sherlock(
|
||||
|
||||
# Get the expected error type
|
||||
error_type = net_info["errorType"]
|
||||
if isinstance(error_type, str):
|
||||
error_type: list[str] = [error_type]
|
||||
|
||||
# Retrieve future and ensure it has finished
|
||||
future = net_info["request_future"]
|
||||
@@ -425,58 +395,60 @@ def sherlock(
|
||||
elif any(hitMsg in r.text for hitMsg in WAFHitMsgs):
|
||||
query_status = QueryStatus.WAF
|
||||
|
||||
elif error_type == "message":
|
||||
# error_flag True denotes no error found in the HTML
|
||||
# error_flag False denotes error found in the HTML
|
||||
error_flag = True
|
||||
errors = net_info.get("errorMsg")
|
||||
# errors will hold the error message
|
||||
# it can be string or list
|
||||
# by isinstance method we can detect that
|
||||
# and handle the case for strings as normal procedure
|
||||
# and if its list we can iterate the errors
|
||||
if isinstance(errors, str):
|
||||
# Checks if the error message is in the HTML
|
||||
# if error is present we will set flag to False
|
||||
if errors in r.text:
|
||||
error_flag = False
|
||||
else:
|
||||
# If it's list, it will iterate all the error message
|
||||
for error in errors:
|
||||
if error in r.text:
|
||||
error_flag = False
|
||||
break
|
||||
if error_flag:
|
||||
query_status = QueryStatus.CLAIMED
|
||||
else:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
elif error_type == "status_code":
|
||||
error_codes = net_info.get("errorCode")
|
||||
query_status = QueryStatus.CLAIMED
|
||||
|
||||
# Type consistency, allowing for both singlets and lists in manifest
|
||||
if isinstance(error_codes, int):
|
||||
error_codes = [error_codes]
|
||||
|
||||
if error_codes is not None and r.status_code in error_codes:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
elif r.status_code >= 300 or r.status_code < 200:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
elif error_type == "response_url":
|
||||
# For this detection method, we have turned off the redirect.
|
||||
# So, there is no need to check the response URL: it will always
|
||||
# match the request. Instead, we will ensure that the response
|
||||
# code indicates that the request was successful (i.e. no 404, or
|
||||
# forward to some odd redirect).
|
||||
if 200 <= r.status_code < 300:
|
||||
query_status = QueryStatus.CLAIMED
|
||||
else:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
else:
|
||||
# It should be impossible to ever get here...
|
||||
raise ValueError(
|
||||
f"Unknown Error Type '{error_type}' for " f"site '{social_network}'"
|
||||
)
|
||||
if any(errtype not in ["message", "status_code", "response_url"] for errtype in error_type):
|
||||
error_context = f"Unknown error type '{error_type}' for {social_network}"
|
||||
query_status = QueryStatus.UNKNOWN
|
||||
else:
|
||||
if "message" in error_type:
|
||||
# error_flag True denotes no error found in the HTML
|
||||
# error_flag False denotes error found in the HTML
|
||||
error_flag = True
|
||||
errors = net_info.get("errorMsg")
|
||||
# errors will hold the error message
|
||||
# it can be string or list
|
||||
# by isinstance method we can detect that
|
||||
# and handle the case for strings as normal procedure
|
||||
# and if its list we can iterate the errors
|
||||
if isinstance(errors, str):
|
||||
# Checks if the error message is in the HTML
|
||||
# if error is present we will set flag to False
|
||||
if errors in r.text:
|
||||
error_flag = False
|
||||
else:
|
||||
# If it's list, it will iterate all the error message
|
||||
for error in errors:
|
||||
if error in r.text:
|
||||
error_flag = False
|
||||
break
|
||||
if error_flag:
|
||||
query_status = QueryStatus.CLAIMED
|
||||
else:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
|
||||
if "status_code" in error_type and query_status is not QueryStatus.AVAILABLE:
|
||||
error_codes = net_info.get("errorCode")
|
||||
query_status = QueryStatus.CLAIMED
|
||||
|
||||
# Type consistency, allowing for both singlets and lists in manifest
|
||||
if isinstance(error_codes, int):
|
||||
error_codes = [error_codes]
|
||||
|
||||
if error_codes is not None and r.status_code in error_codes:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
elif r.status_code >= 300 or r.status_code < 200:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
|
||||
if "response_url" in error_type and query_status is not QueryStatus.AVAILABLE:
|
||||
# For this detection method, we have turned off the redirect.
|
||||
# So, there is no need to check the response URL: it will always
|
||||
# match the request. Instead, we will ensure that the response
|
||||
# code indicates that the request was successful (i.e. no 404, or
|
||||
# forward to some odd redirect).
|
||||
if 200 <= r.status_code < 300:
|
||||
query_status = QueryStatus.CLAIMED
|
||||
else:
|
||||
query_status = QueryStatus.AVAILABLE
|
||||
|
||||
if dump_response:
|
||||
print("+++++++++++++++++++++")
|
||||
@@ -596,22 +568,6 @@ def main():
|
||||
dest="output",
|
||||
help="If using single username, the output of the result will be saved to this file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tor",
|
||||
"-t",
|
||||
action="store_true",
|
||||
dest="tor",
|
||||
default=False,
|
||||
help="Make requests over Tor; increases runtime; requires Tor to be installed and in system path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--unique-tor",
|
||||
"-u",
|
||||
action="store_true",
|
||||
dest="unique_tor",
|
||||
default=False,
|
||||
help="Make requests over Tor with new Tor circuit after each request; increases runtime; requires Tor to be installed and in system path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--csv",
|
||||
action="store_true",
|
||||
@@ -719,12 +675,22 @@ def main():
|
||||
help="Include checking of NSFW sites from default list.",
|
||||
)
|
||||
|
||||
# TODO deprecated in favor of --txt, retained for workflow compatibility, to be removed
|
||||
# in future release
|
||||
parser.add_argument(
|
||||
"--no-txt",
|
||||
action="store_true",
|
||||
dest="no_txt",
|
||||
default=False,
|
||||
help="Disable creation of a txt file",
|
||||
help="Disable creation of a txt file - WILL BE DEPRECATED",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--txt",
|
||||
action="store_true",
|
||||
dest="output_txt",
|
||||
default=False,
|
||||
help="Enable creation of a txt file",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
@@ -742,7 +708,7 @@ def main():
|
||||
|
||||
# Check for newer version of Sherlock. If it exists, let the user know about it
|
||||
try:
|
||||
latest_release_raw = requests.get(forge_api_latest_release).text
|
||||
latest_release_raw = requests.get(forge_api_latest_release, timeout=10).text
|
||||
latest_release_json = json_loads(latest_release_raw)
|
||||
latest_remote_tag = latest_release_json["tag_name"]
|
||||
|
||||
@@ -755,22 +721,10 @@ def main():
|
||||
except Exception as error:
|
||||
print(f"A problem occurred while checking for an update: {error}")
|
||||
|
||||
# Argument check
|
||||
# TODO regex check on args.proxy
|
||||
if args.tor and (args.proxy is not None):
|
||||
raise Exception("Tor and Proxy cannot be set at the same time.")
|
||||
|
||||
# Make prompts
|
||||
if args.proxy is not None:
|
||||
print("Using the proxy: " + args.proxy)
|
||||
|
||||
if args.tor or args.unique_tor:
|
||||
print("Using Tor to make requests")
|
||||
|
||||
print(
|
||||
"Warning: some websites might refuse connecting over Tor, so note that using this option might increase connection errors."
|
||||
)
|
||||
|
||||
if args.no_color:
|
||||
# Disable color output.
|
||||
init(strip=True, convert=False)
|
||||
@@ -802,7 +756,7 @@ def main():
|
||||
if args.json_file.isnumeric():
|
||||
pull_number = args.json_file
|
||||
pull_url = f"https://api.github.com/repos/sherlock-project/sherlock/pulls/{pull_number}"
|
||||
pull_request_raw = requests.get(pull_url).text
|
||||
pull_request_raw = requests.get(pull_url, timeout=10).text
|
||||
pull_request_json = json_loads(pull_request_raw)
|
||||
|
||||
# Check if it's a valid pull request
|
||||
@@ -871,8 +825,6 @@ def main():
|
||||
username,
|
||||
site_data,
|
||||
query_notify,
|
||||
tor=args.tor,
|
||||
unique_tor=args.unique_tor,
|
||||
dump_response=args.dump_response,
|
||||
proxy=args.proxy,
|
||||
timeout=args.timeout,
|
||||
@@ -888,7 +840,7 @@ def main():
|
||||
else:
|
||||
result_file = f"{username}.txt"
|
||||
|
||||
if not args.no_txt:
|
||||
if args.output_txt:
|
||||
with open(result_file, "w", encoding="utf-8") as file:
|
||||
exists_counter = 0
|
||||
for website_name in results:
|
||||
|
||||
@@ -129,7 +129,7 @@ class SitesInformation:
|
||||
if data_file_path.lower().startswith("http"):
|
||||
# Reference is to a URL.
|
||||
try:
|
||||
response = requests.get(url=data_file_path)
|
||||
response = requests.get(url=data_file_path, timeout=30)
|
||||
except Exception as error:
|
||||
raise FileNotFoundError(
|
||||
f"Problem while attempting to access data file URL '{data_file_path}': {error}"
|
||||
@@ -166,7 +166,7 @@ class SitesInformation:
|
||||
|
||||
if honor_exclusions:
|
||||
try:
|
||||
response = requests.get(url=EXCLUSIONS_URL)
|
||||
response = requests.get(url=EXCLUSIONS_URL, timeout=10)
|
||||
if response.status_code == 200:
|
||||
exclusions = response.text.splitlines()
|
||||
exclusions = [exclusion.strip() for exclusion in exclusions]
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
NUCLEI_FINGERPRINT_URL: str = "https://raw.githubusercontent.com/projectdiscovery/nuclei-templates/refs/heads/main/http/global-matchers/global-waf-detect.yaml"
|
||||
|
||||
def _check_nuclei_regex(matcher: dict[str,str|list[str]], response: requests.Response) -> bool:
|
||||
import re
|
||||
|
||||
and_cond: bool = matcher.get('condition', '') == 'and'
|
||||
|
||||
target_text: str
|
||||
if matcher['part'] == 'body':
|
||||
target_text = response.text
|
||||
elif matcher['part'] == 'header':
|
||||
target_text = str(response.headers)
|
||||
else:
|
||||
target_text = response.text + str(response.headers)
|
||||
|
||||
for regex in matcher['regex']:
|
||||
if re.search(regex, target_text):
|
||||
if not and_cond:
|
||||
return True
|
||||
else:
|
||||
break
|
||||
else:
|
||||
# `and` conditions will cycle, resulting in this default return True
|
||||
# unless an early failed detection breaks the loop (resulting in False)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_nuclei_words(matcher: dict[str,str|list[str]], response: requests.Response) -> bool:
|
||||
and_cond: bool = matcher.get('condition', '') == 'and'
|
||||
|
||||
target_text: str
|
||||
if matcher['part'] == 'body':
|
||||
target_text = response.text
|
||||
elif matcher['part'] == 'header':
|
||||
target_text = str(response.headers)
|
||||
else:
|
||||
target_text = response.text + str(response.headers)
|
||||
|
||||
for word in matcher['words']:
|
||||
if word in target_text:
|
||||
if not and_cond:
|
||||
return True
|
||||
else:
|
||||
break
|
||||
else:
|
||||
# `and` conditions will cycle, resulting in this default return True
|
||||
# unless an early failed detection breaks the loop (resulting in False)
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_nuclei_fingerprints() -> list[dict[str,str|list[str]]] | None:
|
||||
"""Fetch the latest Nuclei WAF fingerprints from the official repository."""
|
||||
try:
|
||||
response = requests.get(NUCLEI_FINGERPRINT_URL, timeout=10)
|
||||
response.raise_for_status()
|
||||
raw = yaml.safe_load(response.text)
|
||||
fingerprints: list[dict[str,str|list[str]]] = raw['http'][0]['matchers']
|
||||
return fingerprints
|
||||
except requests.RequestException as e:
|
||||
print(f"Error fetching Nuclei fingerprints: {e}")
|
||||
return None
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing YAML data: {e}")
|
||||
return None
|
||||
|
||||
def nuclei_check(response: requests.Response, fingerprints: list[dict[str,str|list[str]]]) -> bool:
|
||||
"""Check if the response matches any of the WAF fingerprints.
|
||||
|
||||
Keyword arguments:
|
||||
response -- The HTTP response to check.
|
||||
fingerprints -- The list of Nuclei WAF fingerprints to check against.
|
||||
|
||||
Returns True if a WAF is detected, False otherwise.
|
||||
"""
|
||||
for matcher in fingerprints:
|
||||
if matcher['type'] == 'word':
|
||||
return _check_nuclei_words(matcher, response)
|
||||
elif matcher['type'] == 'regex':
|
||||
return _check_nuclei_regex(matcher, response)
|
||||
return False
|
||||
+22
-4
@@ -4,9 +4,9 @@ import urllib
|
||||
import pytest
|
||||
from sherlock_project.sites import SitesInformation
|
||||
|
||||
def fetch_local_manifest() -> dict[str, dict[str, str]]:
|
||||
sites_obj = SitesInformation(data_file_path=os.path.join(os.path.dirname(__file__), "../sherlock_project/resources/data.json"))
|
||||
sites_iterable = {site.name: site.information for site in sites_obj}
|
||||
def fetch_local_manifest(honor_exclusions: bool = True) -> dict[str, dict[str, str]]:
|
||||
sites_obj = SitesInformation(data_file_path=os.path.join(os.path.dirname(__file__), "../sherlock_project/resources/data.json"), honor_exclusions=honor_exclusions)
|
||||
sites_iterable: dict[str, dict[str, str]] = {site.name: site.information for site in sites_obj}
|
||||
return sites_iterable
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -25,9 +25,27 @@ def remote_schema():
|
||||
schemadat = json.load(remoteschema)
|
||||
yield schemadat
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--chunked-sites",
|
||||
action="store",
|
||||
default=None,
|
||||
help="For tests utilizing chunked sites, include only the (comma-separated) site(s) specified.",
|
||||
)
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if "chunked_sites" in metafunc.fixturenames:
|
||||
sites_info = fetch_local_manifest()
|
||||
sites_info = fetch_local_manifest(honor_exclusions=False)
|
||||
|
||||
# Ingest and apply site selections
|
||||
site_filter: str | None = metafunc.config.getoption("--chunked-sites")
|
||||
if site_filter:
|
||||
selected_sites: list[str] = [site.strip() for site in site_filter.split(",")]
|
||||
sites_info = {
|
||||
site: data for site, data in sites_info.items()
|
||||
if site in selected_sites
|
||||
}
|
||||
|
||||
params = [{name: data} for name, data in sites_info.items()]
|
||||
ids = list(sites_info.keys())
|
||||
metafunc.parametrize("chunked_sites", params, ids=ids)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
id: global-waf-detect
|
||||
http:
|
||||
- global-matchers: true
|
||||
matchers-condition: or
|
||||
matchers:
|
||||
- type: regex
|
||||
name: regexSite
|
||||
regex:
|
||||
- '(?i)access.to.this.page.has.been.denied'
|
||||
- '(?i)http(s)?://(www.)?anotheroneblocked.\w+.whywasiblocked'
|
||||
condition: or
|
||||
part: response
|
||||
|
||||
- type: word
|
||||
name: wordSiteBody
|
||||
part: body
|
||||
words:
|
||||
- "bad_text_in_body"
|
||||
|
||||
- type: word
|
||||
name: wordSiteHead
|
||||
part: header
|
||||
condition: or
|
||||
words:
|
||||
- "text_in_head"
|
||||
- "other_in_head"
|
||||
@@ -0,0 +1,107 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch, Mock
|
||||
import requests
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
import yaml
|
||||
|
||||
from sherlock_project import waf_check
|
||||
|
||||
|
||||
TEMPLATE_BODY_PATH: str = os.path.join(os.path.dirname(__file__), 'mocks', 'global_waf_detect.yaml')
|
||||
|
||||
def side_effect(url, **kwargs) -> Mock:
|
||||
if url == waf_check.NUCLEI_FINGERPRINT_URL:
|
||||
with open(TEMPLATE_BODY_PATH, 'r', encoding='utf-8') as file:
|
||||
template_body: str = file.read()
|
||||
mock_response: Mock = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.text = template_body
|
||||
return mock_response
|
||||
raise RuntimeError("Unexpected URL")
|
||||
|
||||
class TestWafCheck(unittest.TestCase):
|
||||
|
||||
@patch('sherlock_project.waf_check.requests.get')
|
||||
def test_fetch_nuclei_fingerprints(self, mock_requests_get): # type: ignore
|
||||
mock_requests_get.side_effect = side_effect
|
||||
|
||||
result = waf_check.fetch_nuclei_fingerprints()
|
||||
|
||||
with open(TEMPLATE_BODY_PATH, 'r', encoding='utf-8') as file:
|
||||
template_body: str = file.read()
|
||||
|
||||
expected: list[dict[str, str | list[str]]] = yaml.safe_load(template_body)['http'][0]['matchers']
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_nuclei_regex_check(self):
|
||||
mock_res: requests.Response = requests.Response()
|
||||
mock_res.status_code = 200
|
||||
mock_res._content = b"This is a test response with Test-Regex in the body."
|
||||
mock_res.headers = CaseInsensitiveDict({
|
||||
'Content-Type': 'text/html',
|
||||
'Server': 'TestServer'
|
||||
})
|
||||
matcher: dict[str, str | list[str]] = {
|
||||
'type': 'regex',
|
||||
'name': 'test-regex',
|
||||
'part': 'body',
|
||||
'regex': [r'(?i)not-present'],
|
||||
'condition': 'or'
|
||||
}
|
||||
self.assertFalse(waf_check._check_nuclei_regex(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['regex'] = [r'(?i)TeSt-REgEx']
|
||||
self.assertTrue(waf_check._check_nuclei_regex(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['regex'] = [r'(?i)TeSt-REgEx', r'(?i)Not-Present']
|
||||
self.assertTrue(waf_check._check_nuclei_regex(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['condition'] = 'and'
|
||||
self.assertFalse(waf_check._check_nuclei_regex(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['part'] = 'header'
|
||||
matcher['regex'] = [r'(?i)testserver']
|
||||
self.assertTrue(waf_check._check_nuclei_regex(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['part'] = 'response'
|
||||
self.assertTrue(waf_check._check_nuclei_regex(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['regex'] = [r'(?i)not-present']
|
||||
self.assertFalse(waf_check._check_nuclei_regex(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
def test_nuclei_words_check(self):
|
||||
mock_res: requests.Response = requests.Response()
|
||||
mock_res.status_code = 200
|
||||
mock_res._content = b"This is a test response with test-words in the body."
|
||||
mock_res.headers = CaseInsensitiveDict({
|
||||
'Content-Type': 'text/html',
|
||||
'Server': 'TestServer'
|
||||
})
|
||||
matcher: dict[str, str | list[str]] = {
|
||||
'type': 'word',
|
||||
'name': 'test-word',
|
||||
'part': 'body',
|
||||
'words': ['not-present'],
|
||||
'condition': 'or'
|
||||
}
|
||||
self.assertFalse(waf_check._check_nuclei_words(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['words'] = ['test-word']
|
||||
self.assertTrue(waf_check._check_nuclei_words(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['words'] = ['test-word', 'Not-Present']
|
||||
self.assertTrue(waf_check._check_nuclei_words(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['condition'] = 'and'
|
||||
self.assertFalse(waf_check._check_nuclei_words(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['part'] = 'header'
|
||||
matcher['words'] = ['testserver']
|
||||
self.assertFalse(waf_check._check_nuclei_words(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['words'] = ['TestServer']
|
||||
self.assertTrue(waf_check._check_nuclei_words(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
matcher['part'] = 'response'
|
||||
self.assertTrue(waf_check._check_nuclei_words(matcher, mock_res)) # pyright: ignore[reportPrivateUsage]
|
||||
Reference in New Issue
Block a user