From 197fa6a03360f533ad85b0780e536004b8aed0f5 Mon Sep 17 00:00:00 2001 From: "tv0924@icloud.com" Date: Thu, 5 Jun 2025 22:19:26 +0900 Subject: [PATCH] =?UTF-8?q?[Update]=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 +- README.md | 23 ++--- main.py | 236 ++++++++++++++++++++++++++++++++------------------- run.ps1 | 1 - run.sh | 44 ++++++++++ 5 files changed, 209 insertions(+), 100 deletions(-) delete mode 100644 run.ps1 create mode 100755 run.sh diff --git a/.env.example b/.env.example index 87039d7..2c67931 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,9 @@ ANONYMIZED_TELEMETRY=false GOOGLE_API_KEY= -GOOGLE_MODEL=gemini-2.5-flash-preview-04-17 # 권장 (다른 모델로 교체 가능) [다른 모델로 교체시 성능 보장 불가] -GOOGLE_PLANNER_MODEL=gemini-2.0-flash-lite # 권장 (다른 모델로 교체 가능) [다른 모델로 교체시 성능 보장 불가] +# 권장 (다른 모델로 교체 가능) [다른 모델로 교체시 성능 보장 불가] +GOOGLE_MODEL=gemini-2.5-flash-preview-04-17 +GOOGLE_PLANNER_MODEL=gemini-2.0-flash-lite # 선택 PROXY_HOST=127.0.0.1 diff --git a/README.md b/README.md index bdbdbe3..480651d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ -# 테스트한 영상 -https://f.imnya.ng/.whs/teamproject/ - # 참고하면 좋을만한 것 + - [ ] 일부 웹사이트는 사용자의 언어에 따라 OAuth 옵션을 바꾸기도 합니다. - [ ] https://docs.browser-use.com/customize/custom-functions @@ -14,6 +12,7 @@ uv 설치 후 다음과 같은 명령어를 입력합니다. ``` uv sync ``` + venv와 패키지가 설치가 됩니다. browser_use가 Playwright에 대한 의존성이 있어 브라우저 설치가 필요합니다 @@ -29,20 +28,24 @@ uv run main.py ``` Environment에는 다음과 같은 값이 들어갑니다. + ``` ANONYMIZED_TELEMETRY=false -OPENAI_API_KEY=your_openai_api_key_here -OPENAI_BASE_URL=https://models.github.ai/inference # 선택 -OPENAI_MODEL=openai/gpt-4o-mini # Github Models가 아닐시 gpt-4.1 +GOOGLE_API_KEY=your_openai_api_key_here +GOOGLE_MODEL=gemini-2.5-flash-preview-04-17 +GOOGLE_PLANNER_MODEL=gemini-2.0-flash-lite # 선택 PROXY_HOST=127.0.0.1 PROXY_PORT=8080 ``` -`OPENAI_BASE_URL`은 GitHub Models가 아닐시 비워둡니다. - -`OPENAI_MODEL`은 GitHub Models가 아닐시 `openai/`를 제거합니다. - `PROXY_HOST`와 `PROXY_PORT`는 만약 Caido를 사용 중일 시 환경에 맞게 설정 후 설정합니다. + +# 실행 + +```sh +# ./run.sh {domains.txt 시작 줄} {domains.txt 끝 줄} +./run.sh 12540 13000 +``` diff --git a/main.py b/main.py index 63607a8..8994a2c 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import asyncio import json import os import csv +import argparse from typing import List from dotenv import load_dotenv from pydantic import BaseModel @@ -37,53 +38,65 @@ def make_controller(): extend_planner_system_message = """ 🎯 Mission: Collect Initial SSO Redirect URLs (For Browser Automation) -1. Locate the Login Page -- Navigate to the **client (non-enterprise)** login page. -- If a **privacy policy / cookie / consent popup** appears, **dismiss** or **close** it before continuing. -- ❌ If the page is blocked (e.g., by a firewall, CAPTCHA challenge, or any access restriction), terminate the process immediately and return: - ```json - [ - { - "provider": "Blocked", - "oauth_uri": "-" - } - ] - ``` +※ **절대로 구글 검색, Bing 검색 등 어떤 외부 검색 기능도 사용하지 말고, 주어진 로그인 페이지 URL을 직접 방문하여 탐색하세요.** -2. On the Login Page -- Look for buttons like: - - "Continue with Google" - - "Sign in with GitHub" - - "Login with Naver" -- ✅ Only proceed if **you clearly see a real SSO (social login) button**. -- ❌ Ignore or exclude: - - Buttons with "Passkey" - - Username/password fields - - Email-based login - - Login via certificate or mobile verification - - Any non-OAuth login options +0. **초기 블록(Block) 체크** + - 브라우저가 로그인 페이지에 접근하려 할 때, **페이지가 차단(blocked)** 되거나 **방화벽, CAPTCHA, 접근 제한** 등으로 인해 정상적으로 로드되지 않으면 즉시 프로세스를 종료하고 아래 JSON만 반환해야 합니다. + ```json + [ + { + "provider": "Blocked", + "oauth_uri": "-" + } + ] + ``` + - 이후 단계로 절대 넘어가지 않도록 합니다. -3. If at least one valid SSO login button is found: -- Try to **open it in a new tab**. If that’s not possible, **click it directly**. -- Capture the **first URL that the browser is redirected to** include query string. This URL should: - ✅ Look like: `https://example.com/auth/google` - ❌ Do NOT collect OAuth provider endpoint like: `https://accounts.google.com/...` -- ❌ If you notice any **repeated action** (for example, opening or clicking the same SSO button more than once, or looping between pages), **terminate the process immediately** and return an empty list: `[]`. -- Return the results in the following format: - [ - { - "provider": "Google", - "oauth_uri": "https://example.com/auth/google?include_all_params=..." - }, - { - "provider": "GitHub", - "oauth_uri": "https://example.com/auth/github?include_all_params=..." - } - ] +1. **로그인 페이지 탐색** + - **클라이언트(비엔터프라이즈) 로그인 페이지**로 직접 이동합니다. (검색 엔진을 사용하여 찾아서는 안 됩니다.) + - 접근 후 **개인정보/쿠키/동의 팝업**이 뜨면, 이를 반드시 **닫거나(Dismiss)** 처리하고 계속 진행합니다. + - (이미 0단계에서 블록 여부를 확인했으므로, 이 단계에서는 페이지가 정상 로드되었다고 가정합니다.) -4. If No SSO Login Buttons Are Found or an Error Occurs: -- ❌ Terminate the process immediately. -- Return an empty list: `[]` +2. **SSO 버튼 식별** + - 로그인 페이지에서 다음과 같은 소셜 로그인 버튼을 찾습니다: + - “Continue with Google” + - “Sign in with GitHub” + - “Login with Naver” + - ✅ **실제 SSO 버튼**임이 명확히 확인되는 경우에만 진행합니다. + - ❌ 제외 대상: + - “Passkey” 관련 버튼 + - 아이디/비밀번호 입력란 + - 이메일 기반 로그인 + - 인증서, 휴대폰 인증 등 비-OAuth 로그인 옵션 + +3. **리디렉션 URL 캡처** + - 유효한 SSO 버튼을 하나 이상 찾았다면, 각각의 버튼을 **새 탭으로 열기**를 시도하거나, 불가능할 경우 **직접 클릭**합니다. + - 클릭 후 첫 번째로 **리디렉션된 URL(쿼리 스트링 포함)**을 캡처합니다. 이 URL은: + - ✅ 예시: `https://example.com/auth/google?include_all_params=...` + - ❌ **OAuth 공급자 자체 엔드포인트** (예: `https://accounts.google.com/...`)는 수집하지 않습니다. + - 만약 **반복 행동(looping)**이 감지될 경우(예: 동일한 버튼을 여러 번 열거나 페이지 간 반복 이동), 즉시 프로세스를 종료하고 **빈 배열**을 반환합니다: + ```json + [] + ``` + - 정상적으로 리디렉션 URL을 획득했다면, 아래 형식으로 결과를 수집합니다: + ```json + [ + { + "provider": "Google", + "oauth_uri": "https://example.com/auth/google?include_all_params=..." + }, + { + "provider": "GitHub", + "oauth_uri": "https://example.com/auth/github?include_all_params=..." + } + ] + ``` + +4. **SSO 버튼 미발견 또는 오류 발생 시** + - 페이지 내부에 유효한 SSO 버튼이 전혀 없거나, 탐색 중 예기치 않은 오류가 발생하면 즉시 프로세스를 종료하고 **빈 배열**을 반환합니다: + ```json + [] + ``` """ # ── URL별로 Browser를 새로 띄우는 함수 ── @@ -119,59 +132,108 @@ async def scan_one_url(url: str): controller=controller, extend_planner_system_message=extend_planner_system_message, ) - - # 4) 실제 스캔 실행 - response = await agent.run() - final_result = response.final_result() - if final_result is None: - raise ValueError("final_result()가 None을 반환했습니다.") - - data = json.loads(final_result) + try: - oauth_entries: List[OAuth] = [OAuth(**entry) for entry in data["oauth_providers"]] - except Exception as e: - raise ValueError(f"결과 파싱 실패: {e}\n원본 결과: {final_result}") - # 5) 결과 출력 - print("-" * 50) - print(f"🔗 Scanned URL: {url}\n") - print("🔐 Detected OAuth Providers and URLs:") - for entry in oauth_entries: - if "<" in entry.oauth_uri or "..." in entry.oauth_uri: - print(f"⚠️ WARNING: {entry.provider} URL may be masked or incomplete:\n{entry.oauth_uri}\n") - else: - print(f"- {entry.provider}: {entry.oauth_uri}") - print("-" * 50) + # 4) 실제 스캔 실행 + response = await agent.run() + final_result = response.final_result() + if final_result is None: + raise ValueError("final_result()가 None을 반환했습니다.") - # 6) CSV에 저장 (append) - csv_file = "./oauth_providers.csv" - file_exists = os.path.isfile(csv_file) - with open(csv_file, "a", newline="", encoding="utf-8") as f: - writer = csv.writer(f) - if not file_exists: - writer.writerow(["issuer", "provider", "oauth_uri"]) + data = json.loads(final_result) + try: + oauth_entries: List[OAuth] = [OAuth(**entry) for entry in data["oauth_providers"]] + except Exception as e: + raise ValueError(f"결과 파싱 실패: {e}\n원본 결과: {final_result}") + + # 5) 결과 출력 + print("-" * 50) + print(f"🔗 Scanned URL: {url}\n") + print("🔐 Detected OAuth Providers and URLs:") for entry in oauth_entries: - writer.writerow([url, entry.provider, entry.oauth_uri]) - print(f"✅ OAuth providers saved to {csv_file}\n") + if "<" in entry.oauth_uri or "..." in entry.oauth_uri: + print(f"⚠️ WARNING: {entry.provider} URL may be masked or incomplete:\n{entry.oauth_uri}\n") + else: + print(f"- {entry.provider}: {entry.oauth_uri}") + print("-" * 50) - # 7) Agent와 Browser 닫기 - await agent.close() # Agent 내부 작업 정리 - await context.close() # 브라우저 컨텍스트 종료 (탭/세션 닫기) - await browser.close() # 실제 브라우저 프로세스 종료 + # 6) CSV에 저장 (append) + csv_file = "./oauth_providers.csv" + file_exists = os.path.isfile(csv_file) + with open(csv_file, "a", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + if not file_exists: + writer.writerow(["issuer", "provider", "oauth_uri"]) + for entry in oauth_entries: + writer.writerow([url, entry.provider, entry.oauth_uri]) + print(f"✅ OAuth providers saved to {csv_file}\n") -# ── 인터랙티브 입력 루프 ── -async def loop(): - + # 7) Agent와 Browser 닫기 + await agent.close() # Agent 내부 작업 정리 + await context.close() # 브라우저 컨텍스트 종료 (탭/세션 닫기) + await browser.close() # 실제 브라우저 프로세스 종료 + + except Exception as e: + print(f"❌ Error scanning {url}: {e}") + # 에러 발생 시에도 Agent와 Browser는 닫아야 합니다. + await agent.close() + await context.close() + await browser.close() + +async def loop(filepath: str, start_line: int, end_line: int): + # 인자값으로 받은 파일 경로와 줄 범위를 통해 도메인 리스트 생성 target_list = read_lines_between( - filepath="./domains.txt", - start_line=12187, - end_line=12200 # 원하는 범위로 조정 가능 + filepath=filepath, + start_line=start_line, + end_line=end_line ) - + + # (필요하다면) 강제 설정이 필요한 경우, 아래 주석을 해제하여 target_list[0] 등을 덮어쓸 수 있습니다. + # target_list[0] = "velog.io" for url in target_list: + # scan_one_url은 외부에 정의된 비동기 함수라고 가정합니다. + # 실제로 scan_one_url이 정의된 위치를 import하거나 + # 모듈 수준에 구현해두셔야 합니다. await scan_one_url(f'https://{url}') -# ── 진입점 ── + +def main(): + parser = argparse.ArgumentParser( + prog="domain_scanner", + description="도메인 목록 파일에서 지정한 줄 범위를 읽어 SSO 스캔을 수행합니다." + ) + + # 커맨드라인 인자로 받을 옵션들 정의 + parser.add_argument( + "-f", "--file", + type=str, + required=True, + help="도메인 목록이 들어 있는 텍스트 파일 경로 (예: ./domains.txt)" + ) + parser.add_argument( + "-s", "--start", + type=int, + required=True, + help="읽기 시작 줄 번호 (1-based)" + ) + parser.add_argument( + "-e", "--end", + type=int, + required=True, + help="읽기 종료 줄 번호 (1-based)" + ) + + args = parser.parse_args() + + # 인자값을 비동기 함수에 전달 + asyncio.run(loop( + filepath=args.file, + start_line=args.start, + end_line=args.end + )) + + if __name__ == "__main__": - asyncio.run(loop()) + main() diff --git a/run.ps1 b/run.ps1 deleted file mode 100644 index 6e8207c..0000000 --- a/run.ps1 +++ /dev/null @@ -1 +0,0 @@ -uv run ./main.py \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..1be40c3 --- /dev/null +++ b/run.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# ── 설정 부분 ── +# 실행할 Python 스크립트 이름 (파일 확장자까지) +PYTHON_SCRIPT="main.py" + +# 도메인 목록 파일 경로 (Python 스크립트 실행 시 -f 옵션에 전달) +DOMAIN_FILE="./domains.txt" + +# 몇 줄씩(chunk) 나눠서 실행할지 +CHUNK_SIZE=10 +# ───────────── + +# 인자 개수 확인 +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "예시) $0 10000 11000" + exit 1 +fi + +START_LINE=$1 +END_LINE=$2 + +# START_LINE부터 END_LINE까지 CHUNK_SIZE 만큼씩 반복 +current=$START_LINE +while [ "$current" -le "$END_LINE" ]; do + # 각 청크 구간의 마지막 줄 계산 + chunk_end=$(( current + CHUNK_SIZE - 1 )) + if [ "$chunk_end" -gt "$END_LINE" ]; then + chunk_end=$END_LINE + fi + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Processing lines ${current} to ${chunk_end}..." + # Python 스크립트 실행 + # -f DOMAIN_FILE: 도메인 목록 파일 경로 + # -s current : 읽기 시작 줄 + # -e chunk_end: 읽기 끝 줄 + uv run "$PYTHON_SCRIPT" -f "$DOMAIN_FILE" -s "$current" -e "$chunk_end" + + # 다음 청크의 시작 값 설정 + current=$(( chunk_end + 1 )) +done + +echo "모든 청크 처리 완료."