Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9564474
chore(submodule): add rest.mdm-sn-container-runner as git submodule
abdibekbolot Jun 16, 2026
b4d3e8f
fix(package): update pyproject.toml for bot-virus-challenge project
abdibekbolot Jun 16, 2026
5282187
fix(scripts): update version scripts for Python module format
abdibekbolot Jun 16, 2026
3219798
feat(bot): add WebUI automation bot module
abdibekbolot Jun 16, 2026
2971662
feat(infra): add bot-runner service with hardened networking
abdibekbolot Jun 16, 2026
48354c0
chore: pin bot-virus runner adaptation
abdibekbolot Jun 17, 2026
c44277c
feat(api): port humanize challenge API to bot-virus
abdibekbolot Jun 17, 2026
b912872
chore(gitignore): ignore private challenge artifacts
abdibekbolot Jun 20, 2026
889a814
build(scoring): add private detector dependency and score policy
abdibekbolot Jun 20, 2026
88955d9
feat(scoring): add run-scoped eval attribution
abdibekbolot Jun 20, 2026
78d2f71
fix(api): return explicit errors for broken challenge endpoints
abdibekbolot Jun 20, 2026
b6313f2
build(scoring): install bundled private scoring wheels
abdibekbolot Jun 22, 2026
64af1e2
feat(scoring): add public payload shape validation
abdibekbolot Jun 22, 2026
558b4a3
feat(web): add browser behavior collection SDK
abdibekbolot Jun 22, 2026
021f505
chore(bot): replace multi-file boilerplate with two-file contract
abdibekbolot Jun 24, 2026
f079c21
chore(infra): add CHALLENGE_VM_HOST and CHALLENGE_VM_PORT env vars
abdibekbolot Jun 24, 2026
9c5d4fa
feat(challenge): HBv6 non-behavioral runtime integrity backend
abdibekbolot Jun 24, 2026
29b9b6b
feat(sdk): HBv6 non-behavioral runtime integrity collector + encryption
abdibekbolot Jun 24, 2026
71943fb
chore(deps): update scoring wheel for HBv6 non-behavioral scoring
abdibekbolot Jun 24, 2026
82f7c25
test: update tests for HBv6 non-behavioral flow
abdibekbolot Jun 24, 2026
7608d6d
chore(submodule): update rest.mdm-sn-container-runner for HBv6 changes
abdibekbolot Jun 24, 2026
0a72bba
Merge pull request #11 from RedTeamSubnet/dev
BaratovSokhibjon Jul 2, 2026
944a285
build(deps-dev): update pyright requirement
dependabot[bot] Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,16 @@ volumes/configs/**/*.DS_Store
volumes/configs/**/*Thumbs.db
volumes/configs/**/*~
volumes/configs/**/*._*

# Internal design docs (kept local, not published)
docs/superpowers/

# Internal benchmark/test bot fixtures (reveal scoring expectations — keep local)
benchmarks/
tests/fixtures/bots/

# Private detector source (compiled to the rt_bv_score wheel — never publish source)
private/
# Rust build artifacts
target/
Cargo.lock
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "src/modules/rest.mdm-sn-container-runner"]
path = src/modules/rest.mdm-sn-container-runner
url = git@github.com:RedTeamSubnet/rest.hb-bot-executer.git
59 changes: 59 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@ services:
image: redteamsubnet61/rest-bv-challenge:0.0.0
build:
context: ./src/bv_challenge/challenge
platforms:
- linux/amd64
restart: unless-stopped
networks:
- bot-virus-challenge-net
- bot-executor-net
platform: linux/amd64
privileged: true
ulimits:
nofile: 32768
environment:
TERM: ${TERM:-xterm}
TZ: ${TZ:-Asia/Seoul}
BV_CHALLENGE_API_PORT: ${BV_CHALLENGE_API_PORT:-10001}
BV_CHALLENGE_API_BOT_RUNNER__URL: ${BV_CHALLENGE_API_BOT_RUNNER__URL:-http://bot-runner:8000}
BV_CHALLENGE_API_BOT_RUNNER__SESSION_COUNT: ${BV_CHALLENGE_API_BOT_RUNNER__SESSION_COUNT:-2}
BV_CHALLENGE_API_BOT_RUNNER__REQUEST_TIMEOUT_SEC: ${BV_CHALLENGE_API_BOT_RUNNER__REQUEST_TIMEOUT_SEC:-900}
env_file:
- path: .env
required: false
Expand All @@ -18,3 +30,50 @@ services:
ports:
- "${BV_CHALLENGE_API_PORT:-10001}:${BV_CHALLENGE_API_PORT:-10001}"
tty: true

bot-runner:
image: redteamsubnet61/bot_virus_bot_runner:latest
build:
context: ./src/modules/rest.mdm-sn-container-runner
platforms:
- linux/amd64
restart: unless-stopped
networks:
- bot-virus-challenge-net
- bot-executor-net
privileged: true
user: "0:0"
environment:
TERM: ${TERM:-xterm}
TZ: ${TZ:-UTC}
VM_RUNNER_API_PORT: ${VM_RUNNER_API_PORT:-8000}
# Hostname the spawned bot container uses to reach the challenge API on
# bot-executor-net. Must match the challenge-api service name/alias on
# that network (default "challenger-api" does not resolve here).
CHALLENGE_VM_HOST: ${CHALLENGE_VM_HOST:-challenge-api}
CHALLENGE_VM_PORT: ${CHALLENGE_VM_PORT:-10001}
env_file:
- path: .env
required: false
volumes:
- "bot-runner-logs:${VM_RUNNER_API_LOGS_DIR:-/var/log/rest.vm-runner}"
- "bot-runner-data:${VM_RUNNER_API_DATA_DIR:-/var/lib/rest.vm-runner}"
- "/var/run/docker.sock:/var/run/docker.sock"
ports:
- "${VM_RUNNER_API_PORT:-8000}:${VM_RUNNER_API_PORT:-8000}"
tty: true

networks:
bot-virus-challenge-net:
name: bot-virus-challenge-net
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 1400
bot-executor-net:
name: bot-executor-net
driver: bridge
internal: true

volumes:
bot-runner-logs:
bot-runner-data:
22 changes: 12 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ classifiers = [
]
dynamic = ["version", "dependencies", "optional-dependencies"]

# [tool.setuptools.packages.find]
# where = ["src"]
# include = ["bv_challenge*"]
# namespaces = false
[tool.setuptools]
include-package-data = true

[tool.setuptools.packages.find]
where = ["src"]
include = ["bv_challenge*"]

[tool.setuptools.dynamic]
version = { attr = "bv_challenge.__version__.__version__" }
dependencies = { file = "./requirements.txt" }
version = { attr = "bv_challenge.__version__" }
dependencies = { file = ["requirements.txt"] }

[tool.setuptools.dynamic.optional-dependencies]
# Options dependencies for DEVELOPMENT
Expand All @@ -62,8 +64,8 @@ dev = { file = [
# venv = ".venv"

[project.urls]
Homepage = "https://github.com/RedTeamSubnet/challenge-template"
Homepage = "https://github.com/RedTeamSubnet/bot-virus-challenge"
Documentation = "https://docs.theredteam.io"
Repository = "https://github.com/RedTeamSubnet/challenge-template.git"
Issues = "https://github.com/RedTeamSubnet/challenge-template/issues"
Changelog = "https://github.com/RedTeamSubnet/challenge-template/blob/main/CHANGELOG.md"
Repository = "https://github.com/RedTeamSubnet/bot-virus-challenge.git"
Issues = "https://github.com/RedTeamSubnet/bot-virus-challenge/issues"
Changelog = "https://github.com/RedTeamSubnet/bot-virus-challenge/blob/main/CHANGELOG.md"
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ python-dotenv>=1.0.1,<2.0.0
pydantic[email,timezone]>=2.0.3,<3.0.0
pydantic-settings>=2.2.1,<3.0.0
# redteam_core @ git+https://github.com/RedTeamSubnet/RedTeam.git@v4.2.2
./requirements/rt_bv_score-0.1.0-cp310-abi3-manylinux_2_34_x86_64.whl
./requirements/vault_unlock-0.1.0-cp310-abi3-manylinux_2_34_x86_64.whl
2 changes: 1 addition & 1 deletion requirements/requirements.dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -e .
# -r ./requirements.test.txt
# -r ./requirements.build.txt
pyright>=1.1.392,<2.0.0
pyright>=1.1.411,<2.0.0
pre-commit>=4.0.1,<5.0.0
4 changes: 2 additions & 2 deletions scripts/bump-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fi

## --- Variables --- ##
# Load from environment variables:
VERSION_FILE_PATH="${VERSION_FILE_PATH:-./VERSION.txt}"
VERSION_FILE_PATH="${VERSION_FILE_PATH:-./src/bv_challenge/__version__.py}"


_BUMP_TYPE=""
Expand Down Expand Up @@ -130,7 +130,7 @@ main()

echo "[INFO]: Bumping version to '${_new_version}'..."
# Update the version file with the new version:
echo "${_new_version}" > "${VERSION_FILE_PATH}" || exit 2
echo -e "__version__ = \"${_new_version}\"" > "${VERSION_FILE_PATH}" || exit 2
echo "[OK]: New version: '${_new_version}'"

./scripts/sync-versions.sh -a || exit 2
Expand Down
4 changes: 2 additions & 2 deletions scripts/get-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ cd "${_PROJECT_DIR}" || exit 2

## --- Variables --- ##
# Load from environment variables:
VERSION_FILE_PATH="${VERSION_FILE_PATH:-./VERSION.txt}"
VERSION_FILE_PATH="${VERSION_FILE_PATH:-./src/bv_challenge/__version__.py}"
## --- Variables --- ##


if [ -n "${VERSION_FILE_PATH}" ] && [ -f "${VERSION_FILE_PATH}" ]; then
_current_version=$(cat "${VERSION_FILE_PATH}") || exit 2
_current_version=$(< "${VERSION_FILE_PATH}" grep "__version__ = " | awk -F' = ' '{print $2}' | tr -d '"') || exit 2
else
_current_version="0.0.0"
fi
Expand Down
18 changes: 18 additions & 0 deletions src/bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# syntax=docker/dockerfile:1
# Reference bot image — two-file contract: this Dockerfile + bot.py.
FROM redteamsubnet61/bv-bot-base:latest

WORKDIR /app

# The base image ships Chrome and a Python venv (/opt/venv) on PATH, but not the
# Selenium Python client. Install it into that venv as root (the venv's
# site-packages is not writable by the default seluser).
USER root
RUN /opt/venv/bin/python3 -m pip install --no-cache-dir selenium

COPY bot.py /app/bot.py

# Drop back to the unprivileged default user to run the bot.
USER seluser

ENTRYPOINT ["python3", "/app/bot.py"]
171 changes: 171 additions & 0 deletions src/bot/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Reference bot for the Bot Virus challenge — single-file, two-file contract.

The miner submission is exactly two files: this ``bot.py`` and a ``Dockerfile``.
This reference bot opens the challenge web page (``/_web``) with a headless
Chrome and waits for the browser-side SDK to collect the integrity signals and
POST the encrypted payload to ``/_eval``. It does NOT fill forms, move the
mouse, scroll, or submit anything from Python — submission must happen from the
browser context (``window.BV_SUBMITTED === true``).

Endpoint configuration is read from the environment provided by the runner:

CHALLENGE_WEB_URL e.g. http://challenge-api:10001/_web (preferred)
CHALLENGE_BASE_URL e.g. http://challenge-api:10001 (web url derived)
BV_SESSION_COUNT number of sessions to run (default: 1)

Only the Selenium Python client is required on top of the base image.
"""

import os
import sys
import logging
import subprocess
import tempfile
from urllib.parse import urlparse

from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


logger = logging.getLogger(__name__)

_VIEWPORT_WIDTH = 1440
_VIEWPORT_HEIGHT = 900
_DEFAULT_PORT = "10001"


def resolve_web_url() -> str:
"""Resolve the challenge ``/_web`` URL from the environment.

Order of preference: CHALLENGE_WEB_URL, then CHALLENGE_BASE_URL + "/_web",
then the container's default gateway host, then a sane default.
"""

_web_url = os.getenv("CHALLENGE_WEB_URL")
if _web_url:
return _web_url

_base_url = os.getenv("CHALLENGE_BASE_URL")
if _base_url:
return f"{_base_url.rstrip('/')}/_web"

# Fallback: try to reach the host via the default gateway.
try:
_host = subprocess.check_output(
"ip route | awk '/default/ { print $3 }'", shell=True, text=True
).strip()
except Exception:
_host = "challenge-api"

_web_url = f"http://{_host}:{_DEFAULT_PORT}/_web"
logger.warning(f"CHALLENGE_WEB_URL not set, using fallback: {_web_url}")
return _web_url


def setup_driver(web_url: str) -> WebDriver:
"""Initialize headless Chrome and load the challenge page."""

_options = webdriver.ChromeOptions()
_options.add_argument("--headless")
_options.add_argument("--no-sandbox")
_options.add_argument("--disable-gpu")
_options.add_argument("--ignore-certificate-errors")

# Treat the (HTTP) challenge origin as secure so the SDK gets a secure
# context and WebCrypto SubtleCrypto is available for payload encryption.
# The flag takes an *origin* and only applies with a dedicated user-data-dir.
_parsed = urlparse(web_url)
_origin = f"{_parsed.scheme}://{_parsed.netloc}"
_options.add_argument(f"--unsafely-treat-insecure-origin-as-secure={_origin}")
_options.add_argument(f"--user-data-dir={tempfile.mkdtemp(prefix='bv-chrome-')}")
_options.add_argument(f"--window-size={_VIEWPORT_WIDTH},{_VIEWPORT_HEIGHT}")

driver = webdriver.Chrome(options=_options)
driver.get(web_url)

# Ensure the minimal verification page has loaded.
WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.ID, "status")))
return driver


def run_session(driver: WebDriver) -> bool:
"""Wait for the browser-side SDK to submit the payload to /_eval."""

try:
WebDriverWait(driver, 30).until(
lambda d: d.execute_script("return window.BV_SUBMITTED === true;")
)
logger.info("Browser-side SDK submitted the payload to /_eval.")

# Surface browser console logs for debugging (best effort).
try:
for entry in driver.get_log("browser"):
logger.info(f"[console][{entry.get('level')}] {entry.get('message')}")
except Exception as err:
logger.warning(f"Could not retrieve browser console logs: {err}")

return True
except Exception as err:
logger.error(f"Browser-side submission did not complete: {err}")
return False


def automate(web_url: str) -> bool:
"""Run a single automation session against the challenge web page."""

driver = None
try:
driver = setup_driver(web_url)
return run_session(driver)
except WebDriverException as err:
logger.error(f"WebDriver setup failed: {err}")
return False
except Exception as err:
logger.error(f"Automation failed: {err}")
return False
finally:
if driver is not None:
try:
driver.delete_all_cookies()
driver.execute_script("window.localStorage.clear();")
except Exception:
pass
driver.quit()


def main() -> None:
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S %z",
format="[%(asctime)s | %(levelname)s | %(filename)s:%(lineno)d]: %(message)s",
)

logger.info("Starting WebUI automation bot...")

web_url = resolve_web_url()
logger.info(f"Challenge web URL: {web_url}")

try:
session_count = int(os.getenv("BV_SESSION_COUNT", "1"))
except (TypeError, ValueError):
session_count = 1
session_count = max(1, session_count)

logger.info(f"Running {session_count} session(s)")

for index in range(session_count):
logger.info(f"Session {index + 1}/{session_count}")
automate(web_url)

logger.info("Done!\n")


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions src/bv_challenge/challenge/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ RUN --mount=type=cache,target=/root/.cache,sharing=locked \
# COPY ./requirements* ./
RUN --mount=type=cache,target=/root/.cache,sharing=locked \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
--mount=type=bind,source=requirements,target=requirements \
python3 -m uv pip install --prefix=/install -r ./requirements.txt


Expand Down Expand Up @@ -140,6 +141,7 @@ FROM base AS app

WORKDIR "${BV_CHALLENGE_API_DIR}"
COPY --chown=${UID}:${GID} ./api ${BV_CHALLENGE_API_DIR}/api
COPY --chown=${UID}:${GID} ./templates ${BV_CHALLENGE_API_DIR}/templates
COPY --chown=${UID}:${GID} --chmod=770 ./scripts/*.sh /usr/local/bin/

# VOLUME ["${BV_CHALLENGE_API_DATA_DIR}"]
Expand Down
3 changes: 3 additions & 0 deletions src/bv_challenge/challenge/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-

from api.__version__ import __version__


__all__ = ["__version__"]
Loading