
악성 봇이 서버에 접근하는 것을 자동으로 차단하세요
CDN 뒤에 숨지 않고도 악성 봇을 차단하도록 서버를 구성하는 데 도움을 주는 TUI, 웹 UI, CLI입니다.
NGINX 및 기존 방화벽(iptables 또는 nftables)과 함께 두 개의 개별 계층에서 작동합니다:
이 앱은 서버에서 잠기는 일이 없도록 최선을 다하지만, 사용에 따른 책임은 본인에게 있습니다. 또한 AGPL 라이선스이므로 상업적으로 사용하는 경우 라이선스 조항을 준수해야 합니다.
crates.io에서:``` cargo install stop-bots
또는 체크아웃에서 빌드:```
cargo install --path .
GitHub releases에서 미리 빌드된 x86_64 Linux 바이너리를 사용할 수 있습니다.
빌드하려면 Rust 1.88 이상이 필요합니다. 실질적으로 Linux 전용입니다: systemctl, nginx -t, nft/iptables를 셸을 통해 호출하므로, 다른 곳에서도 컴파일은 되지만 거기서는 별로 쓸모가 없습니다.
인자 없이 바이너리를 실행하면 TUI가 시작되고, stop-bots web은 동일한 화면을 브라우저에서 보여주며(웹 UI 참조), stop-bots --help는 CLI 서브커맨드의 전체 목록을 보여줍니다. TUI와 CLI는 함께 사용할 수 있습니다. TUI에서 모든 것을 설정한 다음, crontab에서 CLI를 사용해 규칙을 최신 상태로 유지할 수 있습니다.
앱을 종료하거나 팝업/서브메뉴에서 빠져나오려면 'q' 또는 Escape를 누르면 됩니다.
't'로 다크 테마와 라이트 테마를 전환할 수 있습니다. 앱이 테마를 자동 감지하려고 시도하지만, 일부 터미널과 멀티플렉서 조합에서는 올바른 선택을 하는 데 필요한 정보가 충분하지 않습니다.
1–4(또는 d/b/s/p)는 해당 화면으로 바로 이동하고, Left/Right 또는 vim의 h/l 별칭은 화면을 차례로 넘깁니다. ?는 언제든 전체 키 바인딩 참조를 토글하고, :는 모든 동작을 이름별로 나열하는 커맨드 팔레트를 엽니다. Tab / Shift+Tab은 항상 현재 화면의 패널 사이를 이동하며, 화면 사이를 이동하지는 않습니다.
Dashboard는 방화벽 스크립트에 들어가는 모든 것을 관리하고, Site settings는 NGINX 설정에 들어가는 모든 것을 관리합니다. 이 구분이 각 설정이 어디에 위치하는지를 결정합니다.
Up/Down은 세 목록 사이를 이동하고, m은 지역 모드를 전환합니다. F를 누르면 현재 방화벽 규칙을 스크립트로 렌더링합니다 — 팝업에는 나중에 수동으로 적용하는 대신 즉시 적용하기 위한 "apply after writing" 토글(Space)도 있습니다. 호스트 전체에 작용하는 키가 세 개 더 있습니다: u는 모든 목록을 다운로드하고, a는 두 영역(NGINX, 그다음 방화벽)을 모두 적용하며, w는 이 콘솔을 NGINX 뒤에 둡니다 — 브라우저에서 버튼과 패널로 제공되는 것과 동일한 세 가지입니다.Tab으로 포커스)이 생성되는 설정을 형성하는 호스트 전역 선택 사항 — 차단된 요청이 무엇을 돌려받는지(아래 참조), 생성된 robots.txt를 제공할지 여부, 속도 제한 — 을 담고 있으며, 그 아래에 디스크에서 발견된 모든 NGINX 사이트가 각각 실시간 "up to date / stale / not found" 상태와 현재 정책을 한 사이트 또는 모든 사이트에 적용하는 동작과 함께 표시됩니다. 이러한 설정 중 하나라도 변경하면 적용된 모든 사이트가 STALE로 바뀌며, 이것이 다시 적용하라는 신호입니다. 사이트를 열면 해당 사이트의 카테고리/봇 정책을 재정의하고, 여섯 가지 요청 형태 규칙 중 원하는 것을 켜고, 차단에서 제외할 경로를 나열할 수 있습니다.Bot settings, 모든 목록 소스와 모든 개별 봇이 있는 곳:
Site settings, 호스트 전역 NGINX 선택 사항이 디스크에서 발견된 모든 사이트 위에 있는 곳:
Dynamic Protection, 지금 서버에 들어오는 트래픽을 실시간으로 보여주는 화면:
알려진 봇, 카테고리별(스캐너 / 검색 엔진 / AI 크롤러)로,
ArcJet의 Well-Known Bots,
ai.robots.txt, 그리고
NGINX Ultimate Bad Bot Blocker
목록에서 가져옵니다. 카테고리를 차단하면 각 사이트의 NGINX 설정에 if ($http_user_agent ...) 규칙이 주입됩니다(apply-blocks / Site settings의 a/A).
과도한 요청, NGINX 자체의 속도 제한을 통해. 여기 있는 다른 모든 것과 달리 이것은 사후에 로그를 분석하는 것이 아니라 요청 시점에 NGINX가 적용합니다. 기본적으로 꺼져 있습니다: 잘못된 사이트에 맞춰진 제한은 실제 방문자를 돌려보냅니다.
먼저 정중하게 — 차단 중인 모든 봇을 나열하는 선택적 생성 robots.txt로, 이를 존중하는 크롤러를 위한 것이며, 아래의 허니팟 경로도 포함합니다. 기본적으로 꺼져 있습니다. 현재 사이트가 /robots.txt에서 제공하는 것을 대체하기 때문입니다.
단, 예외를 지정한 곳은 제외 — 사이트별 경로 예외로, /blog를 제외한 모든 곳에서 AI 크롤러를 차단할 수 있습니다.
브라우저처럼 보이지 않는 요청, 사이트별로. 여섯 가지 독립 규칙이 있으며, 각각 자체 토글이고 기본적으로 모두 꺼져 있습니다 — 규칙마다 스위치가 하나씩이라, 여러분의 무언가가 작동을 멈추면 어느 규칙 때문인지 알 수 있습니다:
Site settings의 호스트 전역 선택 하나입니다. 이것들은 서로 바꿔 쓸 수 있는 상태 코드가 아닙니다 — 각각 다른 의미를 전하며, 그 차이는 여러분이 잡을 의도가 없었던 클라이언트에게 가장 중요합니다:
Tarpit은 오탐에 대해 가장 온건한 옵션이며 — 잘못 잡힌 클라이언트는 거부되지 않고 느려질 뿐입니다 — 봇에게는 비용 면에서 가장 가혹합니다. 연결이 유휴 상태로 앉아 있기 때문입니다. 선택하기 전에 알아야 할 두 가지: 여러분의 워커 연결 중 하나를 그동안 점유하므로, tarpit에 걸린 클라이언트가 쏟아지면 실제 방문자와 worker_connections를 두고 경쟁합니다. 그리고 실제로 얼마나 오래 지속되는지는 NGINX가 작은 오류 본문을 어떻게 작성하기로 선택하는지에 달려 있으며, 이는 실제 서버에 대해 확인이 필요하다고 TODO.md에 기록되어 있습니다.
이들 각각은 Dashboard의 "Automatic blocking" 패널에 있는 독립 스위치이며, 각각 스스로 만료되고 그 행동이 계속되면 다시 추가되는 임시 방화벽 차단을 추가합니다.
이들은 내부 타이머로 실행되어 매분 SSH 및 NGINX 액세스 로그를 다시 읽습니다 — 단, TUI나 웹 UI가 실행 중일 때만입니다. 둘 중 하나면 동일한 일정, 동일한 데이터베이스로 유지되므로, 웹 UI를 띄워 두는 것만으로 충분합니다. 둘 다 실행 중이 아니면 아무것도 탐지되지 않습니다. stop-bots 프로세스가 전혀 없는 서버의 경우, 아래의 Unattended, from cron을 참조하세요.
/.env, /.git/config, /wp-config.php 등에 대한 단일 요청은 그 자체로 결정적이므로 임계값이 필요 없습니다. 내장 목록은 어딘가에서는 합법적인 경로 — /wp-login.php, /wp-admin/, /xmlrpc.php, /phpmyadmin — 를 의도적으로 제외합니다. 자신의 관리자를 잠그는 것이 어차피 404 탐지기가 잡을 스캐너를 놓치는 것보다 나쁘기 때문입니다. set-probe-paths로 직접 추가할 수 있습니다.robots.txt에 Disallow:로만 게시되고 어디에도 링크되지 않은 경로. 여기에 도달한다는 것은 robots.txt를 무시한다는 뜻이며, 합법적인 것은 실수로 그렇게 하지 않습니다 — 여기서 가장 강력한 신호이고 가장 긴 차단입니다. 작동하려면 robots.txt 생성이 켜져 있어야 합니다.세 가지 더 있는데, 이들은 클라이언트가 무엇을 요청하는지가 아니라 어떻게 행동하는지를 봅니다. 셋 다 기본적으로 꺼져 있습니다. 각각 스스로 배제할 수 없는 오탐이 하나씩 있기 때문입니다 — 그리고 셋 다 검증된 검색 엔진 크롤러는 예외로 두는데, 그렇지 않으면 그 모두에 해당할 것이기 때문입니다:
304는 가져온 자산으로 계산됨). 자산을 전혀 제공하지 않는 사이트 — 순수 JSON API — 에서는 도움이 되지 않습니다.Referer가 전혀 없음. Referrer-Policy: no-referrer와 프라이버시 도구에 의해 약화됩니다. 서로 다른 경로 임계값이 있어야 겨우 쓸 만해집니다./24 안에 여러 주소가 같은 회차에 플래그되면 /24를 차단합니다. 기본적으로 꺼져 있습니다 — 세 개가 잘못 행동했다고 256개 주소를 차단하는 것은 설계상의 부수 피해입니다. (IPv6는 다르며 스위치가 필요 없습니다: 탐지는 항상 /64를 차단합니다. /64는 하나의 LAN이며, 이는 단일 IPv4 주소가 나타내는 것과 같은 것입니다. IPv6 공격자가 우연히 사용한 단일 주소를 차단하는 것은 아무것도 막지 못합니다 — 그들은 2^64개를 더 가지고 있습니다.)위의 모든 것은 생성된 것입니다. 그중 무엇이든 실제로 적용되고 있는지는 별개의 문제이며, stop-bots status가 이에 답하는 명령입니다:```
stop-bots status
일곱 가지 검사가 있으며, 그중 첫 번째가 가장 가치 있는 검사입니다. 생성된 규칙이 실제로 커널에 있는가, 아니면 디스크에만 있는가? 실제 호스트 하나가 3주 동안 `/etc/stop-bots/firewall.nft`에 48,860개의 drop 규칙을 두고도 빈 ruleset으로 운영되었습니다. 스크립트를 작성하는 것과 로드하는 것은 별개의 단계인데, 아무도 두 번째 단계를 들여다보지 않았기 때문입니다.
나머지는 다음과 같습니다. ruleset이 재부팅 후에도 살아남는가(`nftables.service`가 활성화되어 있는가?), 스크립트가 여전히 규칙과 일치하는가, NGINX 블록이 적용되었는가, 콘솔 서비스가 이름에 명시된 바이너리를 실행하고 있는가, 데이터베이스를 위한 공간이 있는가, 그리고 탐지기들이 자신의 로그를 읽을 수 있는가.
무언가가 **CRITICAL**이면 비영(非零) 코드로 종료하므로 모니터링 검사로 동작합니다. `--quiet`는 주의가 필요한 것만 출력하며, 이는 cron에 적합한 형태입니다:```
0 * * * * /usr/local/bin/stop-bots status --quiet
실행할 수 없었던 검사 — nft list는 root 권한이 필요하다 — 는 UNKNOWN을 보고하며, 결코
OK가 아니다. 살펴볼 수 없었다는 이유로 모든 것이 괜찮다고 말하는 상태 검사는 아무것도 하지 않는 것보다
더 나쁘다. 왜냐하면 그것이 믿어지기 때문이다.
동일한 보고서가 콘솔과 TUI 양쪽의 Dashboard에 있으며, 렌더링할 때마다가 아니라 내부 cron에 의해
매시간 수집된다: 대규모 ruleset에서의 nft list는 수 메가바이트의 텍스트이다.
위의 모든 방화벽 결정은 생성될 뿐, 자동으로 적용되지 않는다: render-firewall
(또는 Dashboard의 f 키)은 검토하고 직접 적용할 iptables 또는 nftables 스크립트를 작성하며,
현재 연결된 SSH 세션을 잠글 수 있는 스크립트는 작성을 거부한다.
세 가지가 당신 대신 적용할 수 있으며, 세 가지 모두 당신이 요청해야 한다: TUI의 렌더 팝업
("작성 후 적용") 또는 그 a 키, 웹 콘솔의 방화벽 패널("작성 후 실행") 또는 그 "Apply everything" 버튼,
그리고 당신이 작성한 crontab에서의 batch --apply — Unattended, from cron 참조.
그중 어느 것도 자동적인 것의 부작용이 아니다: 내부 cron은 스크립트를 렌더링하고 결코 실행하지 않는다.
NGINX 쪽에도 동일하게 적용된다: 설정을 변경하는 것은 작성될 내용만 변경한다.
Site settings는 적용하기 전까지 각 사이트를 STALE로 표시한다.
탐지기를 끄는 것은 이미 추가된 차단을 결코 제거하지 않는다 — 그것들은 스스로 만료된다.
"탐지 중지"와 "탐지된 것 되돌리기"는 의도적으로 분리되어 있다; 두 번째는
Dynamic Protection 화면 또는 remove-firewall-rule이다.
차단과 무관한 단순한 액세스 로그 집계도 있다: record-access-stats /
list-access-stats는 각 user agent가 성공적인(오류가 아닌) 요청에 얼마나 자주 나타나는지 세어,
누가 차단되고 있는지 위에 누가 실제로 방문하고 있는지 볼 수 있게 한다.
stop-bots batch는 TUI가 수동으로 하는 모든 것에 대한 한 번의 패스이다: 모든 목록을 새로 고치고,
로그를 스캔하고, NGINX 차단 규칙과 방화벽 스크립트를 작성한다.```
0 4 * * * root /usr/local/bin/stop-bots batch --apply --ssh-log /var/log/auth.log
*/10 * * * * root /usr/local/bin/stop-bots batch --apply --no-fetch --ssh-log /var/log/auth.log
It says nothing when everything worked, so a healthy nightly run doesn't mail you. A failed
step prints to stderr and sets a non-zero exit status, which is what makes cron tell you
about it. Run it once by hand with `--verbose` first — that prints a line per step, and is
the easiest way to see what it is actually doing.
**`--apply` is what makes it enforce anything.** Without it, `batch` writes the NGINX config
and the firewall script and stops: config does nothing until a reload, a script does nothing
until it is run. That is this project's default everywhere, and it stays the default here.
`batch` and a long-running front-end coexist safely. The TUI, the web UI and `batch` all
record what they did through the same keys in the same database, so whichever gets to a job
first does it and the others find it no longer due — you don't get two detection passes, and
the Dashboard's "Scheduled tasks" panel shows what actually happened rather than claiming
everything is overdue. If you already leave the web UI running, the nightly `batch` entry is
belt and braces rather than a requirement; if you don't, it is the only thing keeping
detection current.
**With `--apply`, the SSH lockout guard can refuse — and refusing means nothing is applied.**
It refuses if the rules would block a client that is connected right now, *and* if no SSH log
could be read at all, because then the check could not run. The interactive
`render-firewall` only prints a note in that second case, on the reasoning that a human is
watching the terminal; from cron nobody is. **Pass `--ssh-log` explicitly**: cron runs as
root so `/var/log/auth.log` usually reads fine, but on a journald-only host `journalctl`
under cron can come back empty, which is exactly the case it refuses on. `--force` overrides
the guard if you mean it.
One step failing never stops the others, and the NGINX and firewall halves are independent —
a failed NGINX reload still leaves the firewall applied, and the other way round.
`batch` records each step against the same schedule the TUI's internal cron uses, so the two
agree about what has already run instead of both doing it, and the Dashboard's "Scheduled
tasks" panel shows what your real cron did.
# The web UI
`stop-bots web` serves the same five screens in a browser.```
stop-bots web
127.0.0.1:8787에 바인딩되며, 해당 머신에서만 접근할 수 있고, 첫 실행 시 생성된 비밀번호를 한 번 출력합니다. SSH 터널을 통해 노트북에서 접근하세요:``` ssh -L 8787:127.0.0.1:8787 your-server
그런 다음 <http://127.0.0.1:8787/>을 여십시오.

콘솔은 운영 체제의 라이트 또는 다크 설정을 따르며, 헤더에 토글이 있습니다. 위의 TUI 스크린샷은 다크 테마이고, 이 스크린샷들은 라이트 테마입니다. TUI에서 사용하는 키가 여기에서도 작동합니다: `1`–`4`는 화면 전환, `/`는 검색 상자 포커스, `?`는 도움말 열기입니다.

세 가지 호스트 전체 작업이 헤더에 있으며, 브라우저에서는 버튼으로, TUI에서는 단일 키로 제공됩니다:
- **모두 업데이트** (`u`)는 모든 봇 목록, 모든 크롤러 IP 범위, 모든 *활성화된* 평판 피드 및 모든 *선택된* 국가를 다운로드합니다 — `stop-bots batch`가 가져오는 것과 동일한 세트를, 동일한 계획에서 가져옵니다. 하나의 소스가 실패해도 나머지는 중단되지 않으며, 무언가가 적용하기 전까지는 아무것도 강제되지 않습니다.
- **모두 적용** (`a`)은 NGINX 설정을 작성하고 다시 로드한 다음, 방화벽 스크립트를 작성하고 실행합니다. 두 플레인은 독립적입니다: 어느 쪽이 실패하더라도 다른 쪽은 여전히 자기 차례를 갖습니다. 왜냐하면 절반만 적용된 호스트가 NGINX 구문 오류로 방화벽까지 오래된 상태가 된 호스트보다 낫기 때문입니다.
- **웹 액세스** (`w`)는 NGINX가 콘솔 자체를 제공하도록 설정합니다 — [NGINX 뒤에서](#behind-nginx-a-subdomain-or-a-path-prefix)를 참조하십시오.
## 서비스로 실행 (Debian)```
sudo stop-bots install web
/etc/systemd/system/stop-bots-web.service를 작성하고, /var/lib/stop-bots(0700 — 콘솔의 비밀번호 해시를 보관)와 /etc/stop-bots를 생성하며, 비밀번호가 없으면 생성하고, 유닛을 활성화하고 시작합니다.
--dry-run은 전체 계획을 출력하고 아무것도 변경하지 않습니다. 이 프로젝트에서 데몬을 시작하는 유일한 명령이므로 여기서부터 시작하세요. --prefix <dir>은 동일한 트리를 root 없이 읽을 수 있는 곳에 작성합니다. 유닛이 이미 존재하고 이를 편집한 경우, 설치 프로그램은 편집 내용을 덮어쓰지 않고 중단하고 그 사실을 알립니다. 의도한 것이라면 --force를 사용하세요.
서비스는 root로 실행됩니다. 콘솔이 /etc/nginx를 다시 작성하고, 방화벽 스크립트를 작성하며, nginx -t와 systemctl reload nginx를 실행하기 때문입니다. 기능 집합을 그대로 유지하면서 권한을 분리할 수 있는 방법은 없습니다. 유닛에는 해당 요구 사항에서도 유지되는 하드닝과 어떤 하드닝을 왜 제외했는지에 대한 주석이 포함되어 있습니다.
바인드 주소, 호스트 허용 목록 및 경로 접두사는 의도적으로 유닛에 포함되지 않습니다 — 실행 중인 서버가 데이터베이스에서 이를 다시 읽으므로, ExecStart에 넣으면 진실의 출처가 두 개가 됩니다. stop-bots web --save ...로 변경하고 재시작하세요.
이것이 root로 실행되면 한 가지가 달라집니다: 내부 cron의 일일 RenderFirewall 작업이 이제 /etc/stop-bots/firewall.nft를 작성할 수 있게 되는데, 이는 콘솔을 직접 사용자 권한으로 실행했을 때는 불가능했습니다. 그 스크립트를 적용하는 것은 아무것도 없습니다 — 실행하는 것은 여전히 직접 해야 할 일입니다.
Debian만 확인하는데, 테스트된 것이 그것이기 때문입니다. 이 유닛은 모든 systemd 배포판에서 올바를 가능성이 매우 높지만, 가정하는 SSH 로그 경로는 Debian의 것입니다.
루프백 이외의 주소에 바인딩하려면 두 번째의 의도적인 플래그가 필요합니다. 이 콘솔은 실행 중인 호스트의 방화벽과 NGINX 설정을 다시 작성할 수 있기 때문입니다:``` stop-bots web --bind 0.0.0.0:8787 --expose --allowed-hosts admin.example.com --save
`--allowed-hosts`는 실제로 선택 사항이 아닙니다. 목록에 없는 호스트 이름을 가진 요청은 거부됩니다. 이것이 콘솔에 대한 DNS 리바인딩을 실패하게 만드는 이유이며, 이름으로 접근하는 노출된 서버에는 그 이름을 명시해야 하는 이유입니다.
이 도구가 보호하는 바로 그 NGINX 뒤에 TLS와 함께 배치하십시오. 그렇게 하고 프록시가 `X-Forwarded-For`를 설정한다면, 콘솔이 해당 헤더를 신뢰해도 된다고 알려주십시오. 그렇지 않으면 요청이 실제로 어디서 왔는지 구분할 수 없습니다:```
stop-bots web --bind 127.0.0.1:8787 # and set web:trust_forwarded_for
TLS 뒤에서는 web:secure_cookie도 설정하십시오. 이것이 없으면 브라우저는 같은 호스트의 http:// URL에도 세션 쿠키를 전송합니다.
web:trust_forwarded_for는 겉보기보다 더 중요합니다. 이것이 없으면 프록시 뒤의 모든 요청이 127.0.0.1에서 오는 것으로 도착하므로, 콘솔은 한 클라이언트와 다른 클라이언트를 구분할 수 없습니다 — 즉, 쏟아지는 로그인 시도가 여러분과 같은 스로틀 버킷을 공유하게 되고, 자신의 주소를 차단하는 것을 막아주는 가드가 비교할 대상이 없게 됩니다. 이것이 있으면 둘 다 클라이언트별로 작동합니다.
콘솔이 이 설정을 대신 해줄 수 있으며, TUI(Dashboard에서 w)도 마찬가지입니다. 둘 다 NGINX 설정을 작성하고, 경로 접두사를 기록하며, 호스트 이름을 허용 목록에 추가합니다 — 서로 일치해야 하는 세 가지인데, 접두사가 없으면 모든 링크가 location 블록을 벗어나고 호스트 이름이 없으면 모든 요청이 403이 되기 때문입니다. 둘 다 설정이 적용되기 전에 nginx -t로 검증하고, 실패하면 롤백하며, 검증된 후에만 새 주소를 기록합니다.
두 가지 모드가 있으며, path가 기본값인 데에는 이유가 있습니다: 이미 가지고 있는 사이트에 location 블록을 추가하므로 콘솔이 그 사이트의 인증서를 상속받습니다. 서브도메인은 자체 인증서가 필요하며, certbot --nginx -d <host>가 실행되기 전까지 이 콘솔의 비밀번호 양식과 세션 쿠키는 네트워크를 평문으로 통과합니다.
이 섹션의 나머지는 같은 내용을 수동으로 하는 방법이며, 패널을 사용하더라도 한 번쯤 읽어볼 가치가 있습니다 — 아래의 후행 슬래시 함정이 바로 이를 방지하기 위해 존재하는 실수입니다.
서브도메인이 더 간단한 배포 방식이며, 가능하다면 이쪽을 선택하십시오:```nginx server { server_name stopbots.example.com; location / { proxy_pass http://127.0.0.1:8787; proxy_set_header Host $host; } }
## 주요 기능
- **다중 소스 수집**: GitHub, GitLab, Bitbucket, 로컬 디렉터리, ZIP 아카이브, Docker 이미지, 웹 URL
- **지능형 스캐닝**: 시그니처 기반 탐지, 엔트로피 분석, AST 파싱
- **다중 언어 지원**: Python, JavaScript, TypeScript, Java, Go, Rust, C/C++, PHP, Ruby 등
- **유연한 보고**: JSON, SARIF, HTML, Markdown, CSV, 커스텀 템플릿
- **CI/CD 통합**: GitHub Actions, GitLab CI, Jenkins, CircleCI
- **확장 가능한 아키텍처**: 플러그인 시스템, 커스텀 규칙, API 액세스
## 설치
### 사전 요구 사항
- Python 3.9 이상
- pip 또는 poetry
- (선택 사항) Docker 20.10 이상
### pip 사용
```bash
pip install secscan
git clone https://github.com/example/secscan.git
cd secscan
pip install -e .
docker pull example/secscan:latest
docker run --rm -v $(pwd):/scan example/secscan:latest scan /scan
# 로컬 디렉터리 스캔
secscan scan ./my-project
# GitHub 저장소 스캔
secscan scan https://github.com/user/repo
# 특정 브랜치 스캔
secscan scan https://github.com/user/repo --branch develop
프로젝트 루트에 secscan.yaml 파일을 생성하세요:
version: "1.0"
scan:
exclude:
- "**/node_modules/**"
- "**/.git/**"
- "**/vendor/**"
include:
- "**/*.py"
- "**/*.js"
- "**/*.ts"
rules:
severity_threshold: medium
custom_rules_dir: ./rules
output:
format: sarif
path: ./reports/results.sarif
name: Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run SecScan
uses: example/secscan-action@v1
with:
args: scan . --format sarif --output results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
security-scan:
image: example/secscan:latest
script:
- secscan scan . --format json --output report.json
artifacts:
reports:
sast: report.json
secscan [OPTIONS] COMMAND [ARGS]...
Commands:
scan 대상에서 보안 문제 스캔
rules 사용 가능한 규칙 나열 및 관리
report 기존 결과에서 보고서 생성
config 구성 관리
version 버전 정보 표시
rules/ 디렉터리에 YAML 파일을 생성하여 커스텀 규칙을 정의하세요:
id: CUSTOM-001
name: Hardcoded API Key
description: Detects hardcoded API keys in source code
severity: high
category: secrets
patterns:
- type: regex
pattern: 'api[_-]?key\s*=\s*["\'][A-Za-z0-9]{32,}["\']'
languages: [python, javascript, go]
remediation: |
Move API keys to environment variables or a secrets manager.
Never commit credentials to version control.
┌─────────────────────────────────────────────────┐
│ CLI Layer │
├─────────────────────────────────────────────────┤
│ Orchestration Engine │
├──────────┬──────────┬──────────┬────────────────┤
│ Collectors│ Analyzers│ Rules │ Reporters │
├──────────┼──────────┼──────────┼────────────────┤
│ GitHub │ AST │ Built-in │ JSON │
│ GitLab │ Entropy │ Custom │ SARIF │
│ Local │ Pattern │ Plugin │ HTML │
│ Docker │ Taint │ │ CSV │
└──────────┴──────────┴──────────┴────────────────┘
from secscan import Scanner, Config
config = Config(
severity_threshold="medium",
exclude_patterns=["**/test/**"],
)
scanner = Scanner(config=config)
results = scanner.scan("./my-project")
for finding in results.findings:
print(f"[{finding.severity}] {finding.rule_id}: {finding.message}")
print(f" Location: {finding.file}:{finding.line}")
# 서버 시작
secscan serve --port 8080
# 스캔 시작
curl -X POST http://localhost:8080/api/v1/scan \
-H "Content-Type: application/json" \
-d '{"target": "./my-project", "format": "json"}'
# 결과 조회
curl http://localhost:8080/api/v1/scans/{scan_id}
기여를 환영합니다! 자세한 내용은 CONTRIBUTING.md를 참조하세요.
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다. 자세한 내용은 LICENSE 파일을 참조하세요.
**경로 접두사도 작동합니다**, 하지만 콘솔에 이를 알려야 합니다 — 모든 링크, 폼 액션, 리다이렉트, 쿠키 경로를 접두사가 이미 포함된 상태로 생성해야 하며, 이를 추측할 수는 없습니다:```
stop-bots web --base-path /stop-bots --allowed-hosts example.com --save
이 프로젝트에 기여하고 개선하는 데 도움을 주신 모든 분들께 감사드립니다.
이 도구는 교육 및 방어적 보안 목적으로만 제작되었습니다. 다음을 준수할 책임은 전적으로 사용자에게 있습니다:
허가받지 않은 시스템에 대해 이 도구를 사용하는 것은 불법이며 윤리적이지 않습니다. 항상 적절한 허가를 받고, 자신이 소유하거나 테스트 권한을 부여받은 시스템에서만 테스트하십시오.
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다 — 자세한 내용은 LICENSE 파일을 참조하십시오.
외부에서 이를 강제하는 것은 없지만, 실패는 미묘하기보다는 요란하다. 접두사가 설정된 상태에서 접두사 없는 요청은 절반만 작동하는 페이지가 아니라 평범한 404가 된다.
두 가지가 의도적으로 빠져 있으며, Help 화면에 그 이유와 함께 명시되어 있다:
stop-bots web --set-password를
사용하라.또한 연결된 주소를 차단하는 것을 거부하는데, 그렇게 하면 되돌리는 데 사용할 콘솔을 빼앗기게 되기 때문이다.
예전에는 세 가지였다. 방화벽 스크립트 적용이 세 번째였는데, 그것을 실행하는 것이
호스트를 네트워크에서 떼어낼 수 있는 유일한 작업이라는 이유에서였다. 이제는
사용 가능하다 — Dashboard의 "Apply everything"(TUI에서는 a), 또는 방화벽 패널의
"run it after writing" 체크박스 — 왜냐하면 cron으로부터 안전하게 만드는 가드가
버튼으로부터도 안전하게 만들기 때문이다. 규칙은 현재 SSH로 로그인한 클라이언트에
대해, 스크립트 자체가 평가할 순서대로 검사되며, 그중 하나를 차단하게 될 규칙은
경고가 아니라 거부이다. 예전의 쓰기 전용 동작을 되찾으려면 콘솔을 --no-apply로
시작하라.
로그인 시도는 제한된다. 비밀번호를 추측할 수 있어서가 아니라 — 생성된 144비트이므로 — 하나를 검증하는 데 Argon2id가 실행되고, 인증되지 않은 호출자가 게시할 수 있는 만큼 빠르게 그것을 구동하도록 놔두는 것은 이 도구가 보호해야 할 호스트에 대한 서비스 거부 공격이기 때문이다. 열 번의 잘못된 시도는 무료이며, 그 이후에는 클라이언트가 지수적으로 백오프하고, 전역 상한이 시도가 어느 주소에서 오든 관계없이 CPU를 제한한다.
NGINX가 Docker 안에 있고 그 설정이 bind mount에 있다면, systemctl reload nginx는
아무것도 리로드하지 않는다. 대신 두 명령을 컨테이너로 향하게 하라 — 이는 CLI와
TUI에도 적용된다:```
stop-bots set-nginx-commands
--test "docker exec web nginx -t"
--reload "docker exec web nginx -s reload"
명령은 단어로 분리되어 직접 실행됩니다. 셸을 거치지 않으므로 `;`,
`|`, `$VAR`는 구문이 아니라 일반 문자입니다.
# 기여
코드가 어떻게 구성되어 있는지, 어떻게 테스트되는지, 어떤 규칙에 따라 작성되는지는
[CONTRIBUTING.md](https://github.com/ivankovic/stop-bots/blob/main/CONTRIBUTING.md)에 있습니다. 릴리스 과정은
[RELEASING.md](https://github.com/ivankovic/stop-bots/blob/main/RELEASING.md)에 있습니다.
# 연락처
[[email protected]](mailto:[email protected])로 연락하실 수 있습니다.
# 라이선스
Copyright (C) 2026 Marko Ivankovic
이 프로그램은 자유 소프트웨어입니다: 자유 소프트웨어 재단이 발표한 GNU Affero 일반 공중 사용 허가서
(라이선스 버전 3 또는 (선택에 따라) 이후 버전)의 조건에 따라
재배포 및/또는 수정할 수 있습니다.
라이선스의 전체 내용은 [LICENSE](https://github.com/ivankovic/stop-bots/blob/main/LICENSE) 파일을
참조하십시오.
## AGPL 소프트웨어를 사용할 수 없습니까?
대체 라이선싱은 **제공되지 않습니다**.
NOT BLOCKED/BLOCKED(빨간색으로 표시) 태그가 붙습니다. Tab/Shift+Tab은 Up/Down이 적용될 두 패널을 전환하고, f는 공유 필터(전체 / 차단되지 않은 것만 / 차단된 것만)를 순환하며, Enter는 선택된 NOT BLOCKED 행을 차단하거나, 이미 BLOCKED라면 차단을 해제합니다. i는 선택된 주소를 검사합니다: 평판 피드 중 어느 것이 이를 나열하는지, 게시된 크롤러 범위 안에 있는지(이것이 진짜 Googlebot과 그저 그렇다고 주장하는 user agent를 구분합니다), 어느 국가에 속하는지, 그리고 어떤 계정으로 로그인을 시도했는지. 이 모든 것은 이 호스트가 이미 다운로드한 목록에서 나옵니다 — 여기에는 역방향 DNS나 whois 조회가 없습니다. PTR 레코드는 해당 주소를 보유한 누구든 작성하는 것이며, 권위 있는 것처럼 읽히는 공격자 제공 텍스트가 되기 때문입니다.| 규칙 | 봇 외에 차단하는 것 |
|---|
| HTTP/1.0 및 HTTP/1.1 | HTTP/2를 사용하지 않는 크롤러와 API 클라이언트 |
Accept 헤더 없음 | 일부 API 클라이언트는 아무것도 보내지 않음 |
Accept-Language 없음 | 프라이버시 도구가 제거함 |
비어 있거나 없는 User-Agent | 스크립트와 상태 검사가 종종 생략함 |
Host가 맨 IP | IP로 사이트에 접근하는 것을 차단 |
| TLS 1.0 / 1.1 | 아주 오래된 클라이언트만 |
모든 규칙에 두 가지 안전장치가 적용되며, 여러분에게 맡기지 않고 강제됩니다:
server 블록에만 작성됩니다. 브라우저는 TLS 없이 HTTP/2를 사용하지 않으므로, 평범한 listen 80 블록에서는 모든 요청이 HTTP/1.1입니다 — 브라우저가 HTTPS로 가는 도중에 만드는 리다이렉트까지 포함해서. 포트 80과 포트 443 블록은 보통 server_name을 공유하므로 설정이 둘 다에 도달합니다. TLS 규칙만 그 규칙들을 받습니다. 헤더 형태 규칙은 평범한 HTTP에서도 작동하며 둘 다에 작성됩니다./.well-known/은 항상 예외입니다. Let's Encrypt가 HTTP-01 챌린지를 가져오는 곳이 바로 여기이며, HTTP/1.1로 Accept 없이, 종종 User-Agent도 없이 요청합니다 — 예외가 없으면 몇 주 뒤 인증서 갱신이 멈춥니다.| 옵션 | 용도 |
|---|
403 Forbidden(기본값) | 차단이 의도적이었음을 알림; 잘못 잡힌 사람이 대응할 수 있는 유일한 옵션 |
404 Not Found | 무언가 차단되었다는 사실 자체를 숨김 |
410 Gone | 예의 바른 크롤러에게 URL을 영구히 버리도록 요청 — 공격자가 아니라 크롤러를 돌려보낼 때는 403보다 이것을 선호 |
429 Too Many Requests | 예의 바른 클라이언트에게 물러나 재시도하라고 알림 |
418 I'm a teapot | RFC 2324의 농담. 작동은 하지만 IANA에 등록되지 않았고, NGINX는 빈 본문으로 보냄 |
444 close connection | 아무 응답도 하지 않음; 가장 저렴하지만 서버가 다운된 것과 구별 불가 |
Tarpit | 403으로 응답하되 본문을 초당 1바이트로 흘려보내, 클라이언트가 넘어가지 않고 기다리게 함 |
| 옵션 | 설명 | 기본값 |
|---|
--format | 출력 형식 (json, sarif, html, md, csv) | json |
--output | 출력 파일 경로 | stdout |
--severity | 최소 심각도 (low, medium, high, critical) | low |
--exclude | 제외할 패턴 | 없음 |
--threads | 병렬 워커 수 | 4 |
--timeout | 스캔당 타임아웃(초) | 300 |
--verbose | 상세 출력 활성화 | false |