adding validated services? patching forcad_local.py

This commit is contained in:
Your Name
2026-08-13 08:32:35 +07:00
parent d485f61169
commit e795614e45
1459 changed files with 420036 additions and 436 deletions

View File

@@ -0,0 +1,9 @@
tasks:
- name: bashist
checker: bashist/checker.py
default_score: ???
checker_timeout: 30
puts: 2
gets: 2
places: 2
checker_type: pfr

View File

@@ -0,0 +1,224 @@
#!/usr/bin/env python3
import json
import random
import secrets
import sys
from typing import List, NamedTuple
import checklib
import requests
class PrivatePost(NamedTuple):
content: str
private: bool = True
class PublicPost(NamedTuple):
username: str
content: str
def random_username() -> str:
if random.random() < 0.8:
return checklib.rnd_username()
return checklib.rnd_string(5 + secrets.randbelow(20))
def random_password() -> str:
if random.random() < 0.8:
return checklib.rnd_password()
return checklib.rnd_string(5 + secrets.randbelow(20))
def minidumps(data) -> str:
return json.dumps(data, separators=(",", ":"), sort_keys=True)
class Checker(checklib.BaseChecker):
def __init__(self, host: str):
super().__init__(host)
port = 1599
self.host_with_port = f"{self.host}:{port}"
self.url = f"http://{self.host_with_port}"
def action(self, action, *args, **kwargs):
try:
super().action(action, *args, **kwargs)
except self.get_check_finished_exception():
raise
except requests.RequestException as e:
self.cquit(
checklib.Status.DOWN, "Connection error", f"Requests error {repr(e)}"
)
def random_new_user(self) -> tuple[str, str, requests.Session]:
sess = self.get_initialized_session()
username = random_username()
password = random_password()
r = sess.post(
f"{self.url}/api/user/register",
json={"username": username, "password": password},
)
self.assert_(r.ok, f"Could not register: {r.status_code=} {r.text=}")
return username, password, sess
def check_if_user_exists(self, username: str):
r = requests.get(f"{self.url}/api/users")
self.assert_(r.ok, f"Could not list users: {r.status_code=} {r.text=}")
users = self.get_json(r, f"Invalid GET /api/users response json: {r.text=}")
self.assert_in(
{"username": username},
users,
f"Expected {username=} in GET /api/users response: {r.text=}",
)
def login(self, username: str, password: str) -> requests.Session:
sess = self.get_initialized_session()
r = sess.post(
f"{self.url}/api/user/login",
json={"username": username, "password": password},
)
self.assert_(r.ok, f"Could not login: {r.status_code=} {r.text=}")
return sess
def create_post(
self,
sess: requests.Session,
content: str,
is_private: bool = True,
):
r = sess.post(
f"{self.url}/api/post/new",
json={"content": content, "private": is_private},
)
self.assert_(
r.ok,
f"Could not create post with {content=} and {is_private=}: {r.status_code=} {r.text=}",
)
def list_user_posts(self, sess: requests.Session) -> List[PrivatePost]:
r = sess.get(f"{self.url}/api/user/posts")
self.assert_(
r.ok,
f"Could not list user's posts: {r.status_code=} {r.text=}",
)
posts = []
data = self.get_json(r, f"Invalid GET /api/user/posts json: {r.text=}")
for value in data:
post = PrivatePost(value["content"], value["private"])
posts.append(post)
return posts
def list_all_posts(self) -> List[PublicPost]:
sess = self.get_initialized_session()
r = sess.get(f"{self.url}/api/posts")
self.assert_(
r.ok,
f"Could not list all posts: {r.status_code=} {r.text=}",
)
posts = []
data = self.get_json(r, f"Invalid GET /api/user/posts json: {r.text=}")
for value in data:
post = PublicPost(value["username"], value["content"])
posts.append(post)
return posts
def check(self):
username, password, sess = self.random_new_user()
self.check_if_user_exists(username)
sess = self.login(username, password)
content = checklib.rnd_string(5 + secrets.randbelow(40))
is_private = True
self.create_post(sess, content, is_private)
posts = self.list_user_posts(sess)
post = PrivatePost(content=content)
self.assert_in(
post,
posts,
f"Expected {post=} in private posts {posts=}",
checklib.Status.CORRUPT,
)
content = checklib.rnd_string(5 + secrets.randbelow(40))
is_private = False
self.create_post(sess, content, is_private)
posts = self.list_all_posts()
post = PublicPost(username=username, content=content)
self.assert_in(
post,
posts,
f"Expected {post=} in public posts {posts=}",
checklib.Status.CORRUPT,
)
static_file = random.choice(
[
"index.html",
"login.html",
"newpost.html",
"posts.html",
"register.html",
"userposts.html",
]
)
r = sess.get(f"{self.url}/{static_file}")
self.assert_(
r.ok,
f"Could not get static file - {static_file}: {r.status_code=} {r.text=}",
)
self.cquit(checklib.Status.OK)
def put(self, flag_id: str, flag: str, vuln: str):
username, password, sess = self.random_new_user()
self.create_post(sess, flag, True)
self.cquit(checklib.Status.OK, private=minidumps((username, password)))
def get(self, flag_id: str, flag: str, _: str):
username, password = json.loads(flag_id)
sess = self.login(username, password)
posts = self.list_user_posts(sess)
post = PrivatePost(content=flag)
self.assert_in(
post,
posts,
f"Expected {post=} in private posts {posts=}",
checklib.Status.CORRUPT,
)
self.cquit(checklib.Status.OK)
if __name__ == "__main__":
host = sys.argv[2]
checker = Checker(host)
try:
action = sys.argv[1]
arguments = sys.argv[3:]
checker.action(action, *arguments)
except checker.get_check_finished_exception():
checklib.cquit(checklib.Status(checker.status), checker.public, checker.private)

View File

@@ -0,0 +1,9 @@
tasks:
- name: block-game
checker: block-game/checker.py
default_score: ???
checker_timeout: 30
puts: 2
gets: 2
places: 2
checker_type: pfr

View File

@@ -0,0 +1,498 @@
#!/usr/bin/env python3
import hashlib
import sys
import secrets
import json
import checklib
import requests
import random
import websockets
import websockets.sync.client
from requests.cookies import get_cookie_header
import level_gen
def random_str(prefix="") -> str:
if random.random() < 0.1:
return prefix + "Hello world"
elif random.random() < 0.1:
return prefix + "How are you?"
elif random.random() < 0.1:
return prefix + "I'm fine"
elif random.random() < 0.1:
return prefix + "Super secret communication"
elif random.random() < 0.1:
return prefix + "There are flags I want to send you"
else:
return prefix + checklib.rnd_string(5 + secrets.randbelow(40))
def random_username() -> str:
if random.random() < 0.8:
return checklib.rnd_username()
return checklib.rnd_string(5 + secrets.randbelow(20))
def random_password() -> str:
if random.random() < 0.8:
return checklib.rnd_password()
return checklib.rnd_string(5 + secrets.randbelow(20))
def random_level_name(hint = "") -> str:
x = random.random()
if x < 0.33:
return checklib.rnd_string(5 + secrets.randbelow(20)) + "_" + hint
elif x < 0.66:
return checklib.rnd_string(5 + secrets.randbelow(20)) + "_" + hint + "_" + checklib.rnd_string(5 + secrets.randbelow(20))
else:
return hint + "_" + checklib.rnd_string(5 + secrets.randbelow(20))
def random_description(hint = "") -> str:
strings = [checklib.rnd_string(5 + secrets.randbelow(20)) for _ in range(random.randrange(1, 3))]
strings.insert(random.randrange(len(strings)), hint)
return "\n".join(strings)
def random_prize(hint = "") -> str:
x = random.random()
if x < 0.33:
return "youre " + checklib.rnd_string(5 + secrets.randbelow(20)) + " cool"
if x < 0.66:
return "secret secret " +checklib.rnd_string(5 + secrets.randbelow(20))
return "my promocode: " + checklib.rnd_string(5 + secrets.randbelow(20))
def minidumps(data) -> str:
return json.dumps(data, separators=(",", ":"), sort_keys=True)
def jsonhash(data: dict) -> str:
return hashlib.sha256(minidumps(data).encode()).hexdigest()
class Checker(checklib.BaseChecker):
vulns = 2
timeout = 60
uses_attack_data: bool = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
port = 5874
self.host_with_port = f"{self.host}:{port}"
self.url = f"http://{self.host_with_port}"
def action(self, action, *args, **kwargs):
try:
super().action(action, *args, **kwargs)
except self.get_check_finished_exception():
raise
except requests.RequestException as e:
self.cquit(
checklib.Status.DOWN, "Connection error", f"Requests error {repr(e)}"
)
def check_whoami(self, sess: requests.Session, username: str):
r = sess.get(f"{self.url}/api/user")
self.assert_(
r.ok, f"Could not get whoami after registering: {r.status_code=} {r.text=}"
)
data = self.get_json(r, f"Invalid json in whoami response: {r.text=}")
self.assert_("id" in data, f"Invalid whoami response: no id, {r.text=}")
self.assert_(
"username" in data, f"Invalid whoami response: no username, {r.text=}"
)
self.assert_(
data["username"] == username,
f"Invalid whoami response: username mismatch, {r.text=}",
)
def random_new_user(self) -> tuple[str, str, requests.Session]:
sess = self.get_initialized_session()
username = random_username()
password = random_password()
r = sess.post(
f"{self.url}/api/auth/register",
json={
"username": username,
"password": password,
},
)
self.assert_(r.ok, f"Could not register: {r.status_code=} {r.text=}")
self.check_whoami(sess, username)
return username, password, sess
def login(
self,
username: str,
password: str,
check_whoami: bool = True,
error_status: checklib.Status = checklib.Status.MUMBLE,
) -> requests.Session:
sess = self.get_initialized_session()
r = sess.post(
f"{self.url}/api/auth/login",
json={
"username": username,
"password": password,
},
)
self.assert_(r.ok, f"Could not login: {r.status_code=} {r.text=}", error_status)
if check_whoami:
self.check_whoami(sess, username)
return sess
def get_level_list(
self,
sess: requests.Session,
checker_status: checklib.Status = checklib.Status.MUMBLE,
) -> list:
r = sess.get(f"{self.url}/api/user/levels?page=0")
self.assert_(
r.ok,
f"Could not get level list: {r.status_code=} {r.text=}",
checker_status,
)
data = self.get_json(
r, f"Invalid json in level list: {r.text=}", checker_status
)
self.assert_(
isinstance(data, list),
f"Invalid level list: not a list, {r.text=}",
checker_status,
)
return data
def verify_level(self, level: dict):
self.assert_(
isinstance(level, dict), f"Level data {level} is invalid: expected dict"
)
self.assert_(
"id" in level and isinstance(level["id"], int),
f"Level data {level} is invalid: expected id of type int",
)
self.assert_(
"name" in level and isinstance(level["name"], str),
f"Level data {level} is invalid: expected name of type str",
)
self.assert_(
"description" in level and isinstance(level["description"], str),
f"Level data {level} is invalid: expected description of type str",
)
self.assert_(
"visibility" in level and level["visibility"] in ("private", "public"),
f"Level data {level} is invalid: expected visibility 'private' or 'public'",
)
return True
def get_level_by_name(
self,
sess: requests.Session,
name: str,
not_found_status: checklib.Status = checklib.Status.MUMBLE,
) -> dict:
r = sess.get(f"{self.url}/api/user/level", params={"name": name})
if r.status_code == 404:
self.assert_(
False,
f"Level {name=} not found via search",
not_found_status,
)
self.assert_(
r.ok,
f"Could not search level by name: {r.status_code=} {r.text=}",
not_found_status,
)
level_data = self.get_json(
r, f"Invalid json in level search response: {r.text=}", not_found_status
)
self.verify_level(level_data)
self.assert_eq(level_data["name"], name, f"{level_data['name']=} mismatch")
return level_data
def check_level_saved(
self,
sess: requests.Session,
name: str,
description: str,
visibility: str,
data: dict,
prize: str = None,
):
level_list_data = self.get_level_by_name(sess, name)
level_id = level_list_data["id"]
self.assert_eq(
level_list_data["name"], name, f"{level_list_data['name']=} mismatch"
)
self.assert_eq(
level_list_data["description"],
description,
f"{level_list_data['description']=} mismatch",
)
self.assert_eq(
level_list_data["visibility"],
visibility,
f"{level_list_data['visibility']=} mismatch",
)
r = sess.get(f"{self.url}/api/user/level/{level_id}")
level_data = self.get_json(r, f"Invalid json in level data: {r.text}")
self.verify_level(level_data)
self.assert_(
"data" in level_data,
f"Level data {level_data} is invalid: expected level data",
)
self.assert_eq(level_data["name"], name, f"{level_data['name']=} mismatch")
self.assert_eq(
level_data["description"],
description,
f"{level_data['description']=} mismatch",
)
self.assert_eq(
level_data["visibility"],
visibility,
f"{level_data['visibility']=} mismatch",
)
self.assert_eq(level_data["data"], data, f"{level_data['data']=} mismatch")
if prize is not None:
self.assert_(
"prize" in level_data and isinstance(level_data["prize"], str),
f"Level data {level_data} is invalid: expected prize of type string",
)
self.assert_eq(
level_data["prize"], prize, f"{level_data['data']=} mismatch"
)
def put(self, flag_id: str, flag: str, vuln: str):
username, password, sess = self.random_new_user()
try:
vuln = int(vuln)
except ValueError as e:
raise ValueError(f"Invalid {vuln=}, expected a number") from e
if vuln == 1:
tiles_data = level_gen.generate_unbeatable()
level_name = f"unbeatable_FLAG_by_{username}_{secrets.token_hex(4)}"
description = "Win to get the FLAG. This level is impossible to beat!!"
visibility = "public"
elif vuln == 2:
tiles_data, _ = level_gen.generate_easy()
level_name = f"private_FLAG_by_{username}_{secrets.token_hex(4)}"
description = "Win to get the FLAG"
visibility = "private"
else:
raise ValueError(f"Invalid {vuln=}, expected 1 or 2")
r = sess.post(
f"{self.url}/api/user/level",
json={
"name": level_name,
"description": description,
"visibility": visibility,
"data": tiles_data,
"prize": flag,
},
)
self.assert_(r.ok, f"Could not create the level: {r.status_code=} {r.text=}")
self.check_level_saved(
sess, level_name, description, visibility, tiles_data, flag
)
tiles_data_hash = jsonhash(tiles_data)
self.cquit(
checklib.Status.OK,
public=minidumps({"level_name": level_name}),
private=minidumps((username, password, level_name, visibility, tiles_data_hash)),
)
def get(self, flag_id: str, flag: str, vuln: str):
username, password, level_name, visibility, tiles_data_hash = json.loads(flag_id)
sess = self.login(
username, password, check_whoami=False, error_status=checklib.Status.CORRUPT
)
level_list_data = self.get_level_by_name(
sess, level_name, not_found_status=checklib.Status.CORRUPT
)
r = sess.get(f"{self.url}/api/user/level/{level_list_data['id']}")
level_data = self.get_json(r, f"Invalid json in level data: {r.text}")
self.verify_level(level_data)
self.assert_(
"prize" in level_data and isinstance(level_data["prize"], str),
f"Level data {level_data} is invalid: expected prize of type string",
)
self.assert_eq(
level_data["prize"], flag, "Flag mismatch", checklib.Status.CORRUPT
)
self.assert_(
"data" in level_data,
f"Level data {level_data} is invalid: expected tiles data",
)
actual_tiles_data_hash = jsonhash(level_data["data"])
self.assert_eq(tiles_data_hash, actual_tiles_data_hash, "Flag level contents changed", checklib.Status.CORRUPT)
self.assert_eq(visibility, level_data["visibility"], "Flag level visibility changed", checklib.Status.CORRUPT)
self.cquit(checklib.Status.OK)
def check_user_search(self, sess, username):
r = sess.post(f"{self.url}/api/user", json={"username": username})
data = self.get_json(r, f"Invalid POST /api/user response json: {r.text}")
self.assert_in("username", data, f"Expected username in POST /api/user response: {r.text}")
self.assert_eq(data["username"], username, f"Expected {username=}: {r.text}")
self.assert_in("id", data, f"Expected id in POST /api/user response: {r.text}")
id = data["id"]
r = sess.post(f"{self.url}/api/user", json={"id": id})
data = self.get_json(r, f"Invalid POST /api/user response json: {r.text}")
self.assert_in("username", data, f"Expected username in POST /api/user response: {r.text}")
self.assert_eq(data["username"], username, f"Expected {username=}: {r.text}")
self.assert_in("id", data, f"Expected id in POST /api/user response: {r.text}")
self.assert_eq(data["id"], id, f"Expected {id=}: {r.text}")
def get_level_data(self, sess, id):
r = sess.get(f"{self.url}/api/user/level/{id}")
level_data = self.get_json(r, f"Invalid json in level data: {r.text}")
self.verify_level(level_data)
return level_data
def connect_with_auth(self, sess: requests.Session, url):
cookie_value = get_cookie_header(sess.cookies, requests.Request("GET", url.replace("ws://", "http://")))
return websockets.sync.client.connect(url, additional_headers={"Cookie": cookie_value})
def recv_message(self, sock, timeout):
try:
m = sock.recv(timeout)
except TimeoutError:
return False
except websockets.exceptions.ConnectionClosedError:
return False
try:
m = json.loads(m)
except json.JSONDecodeError:
self.assert_(False, f"Invalid websocket message: {m}")
if "type" in m and m["type"] == "level_complete":
self.assert_in("option", m, f"Invalid level_complete: {m}")
self.assert_in("prize", m["option"], f"Invalid level_complete: {m}")
return m["option"]["prize"]
return True
def play_level(self, sess, id, moves = [], expect_prize = False, expect_updates = False):
got_updates = False
with self.connect_with_auth(sess, f"ws://{self.host_with_port}/api/user/level/{id}/play") as sock:
for move in moves:
sock.send(json.dumps({"type": "move", "option": {"direction": move}}))
while True:
timeout = 0.5 if expect_updates and not got_updates else 0.01
m = self.recv_message(sock, timeout)
if m == True: # noqa: E712
got_updates = True
elif m == False: # noqa: E712
break
else:
return m
while True:
timeout = 0.5 if expect_prize else 0.05
m = self.recv_message(sock, timeout)
if m == True: # noqa: E712
got_updates = True
elif m == False: # noqa: E712
break
else:
return m
self.assert_(not expect_prize, "Didn't get the prize")
self.assert_(got_updates or not expect_updates, "Didn't get any updates from the server")
def check(self):
username, password, sess = self.random_new_user()
self.check_user_search(sess, username)
r = sess.post(f"{self.url}/api/auth/logout")
self.assert_(r.ok, f"Couldn't logout: {r.text}")
r = sess.post(f"{self.url}/api/user/whoami")
self.assert_(not r.ok, f"Can access whoami after logout: {r.text}")
sess = self.login(username, password)
levels = self.get_level_list(sess)
if len(levels) > 0:
level = random.choice(levels)
data = self.get_level_data(sess, level["id"])
self.play_level(sess, data["id"])
if random.random() < 0.5:
tiles_data, moves = level_gen.generate_easy()
level_name = random_level_name("ez")
description = random_description("Easy level")
else:
tiles_data, moves = level_gen.generate_box_test()
level_name = random_level_name("box")
description = random_description("boxes boxes")
prize = random_prize()
r = sess.post(
f"{self.url}/api/user/level",
json={
"name": level_name,
"description": description,
"visibility": "public",
"data": tiles_data,
"prize": prize,
},
)
data = self.get_json(r, f"Invalid level creation response: {r.text}")
self.assert_in("id", data, f"Expected id after level creation: {r.text}")
id = data["id"]
got_prize = self.play_level(sess, id, moves, True, True)
self.assert_eq(prize, got_prize, f"Got wrong prize {got_prize}")
self.check_level_saved(sess, level_name, description, "public", tiles_data, prize)
username, password, sess = self.random_new_user()
got_prize = self.play_level(sess, id, moves, True, True)
self.assert_eq(prize, got_prize, f"Got wrong prize {got_prize}")
self.check_level_saved(sess, level_name, description, "public", tiles_data)
tiles_data, moves = level_gen.generate_box_test()
level_name = random_level_name("boxes_priv")
description = random_description("only mine level")
prize = random_prize()
r = sess.post(
f"{self.url}/api/user/level",
json={
"name": level_name,
"description": description,
"visibility": "private",
"data": tiles_data,
"prize": prize,
},
)
data = self.get_json(r, f"Invalid level creation response: {r.text}")
self.assert_in("id", data, f"Expected id after level creation: {r.text}")
id = data["id"]
self.check_level_saved(sess, level_name, description, "private", tiles_data, prize)
username, password, sess = self.random_new_user()
r = sess.get(f"{self.url}/api/user/level/{id}")
self.assert_(not r.ok, f"Successfully got private level data: {id} by {username}")
r = sess.get(f"{self.url}/api/user/level/{id}/play")
self.assert_(not r.ok, f"Successfully started playing private level: {id} played by {username}")
self.cquit(checklib.Status.OK)
if __name__ == "__main__":
host = sys.argv[2]
checker = Checker(host)
try:
action = sys.argv[1]
arguments = sys.argv[3:]
checker.action(action, *arguments)
except checker.get_check_finished_exception():
checklib.cquit(
checklib.Status(checker.status),
checker.public,
checker.private,
)

View File

@@ -0,0 +1,272 @@
import random
def is_good(walls, size, x, y) -> bool:
return 0 <= x < size and 0 <= y < size and ((x == 0 or walls[x - 1][y]) + (y == 0 or walls[x][y - 1]) + (x == size - 1 or walls[x + 1][y]) + (y == size - 1 or walls[x][y + 1]) >= 3)
def move_str2coord(move, x, y) -> tuple[int, int]:
if move == "left":
return x - 1, y
elif move == "up":
return x, y - 1
elif move == "right":
return x + 1, y
elif move == "down":
return x, y + 1
else:
raise ValueError()
def invert_move(move):
if move == "left":
return "right"
elif move == "up":
return "down"
elif move == "right":
return "left"
elif move == "down":
return "up"
else:
raise ValueError()
def generate_box_test() -> dict:
size = random.randint(5, 15)
tiles = []
winning_moves = []
walls = [[True] * size for _ in range(size)]
x, y = random.randrange(size), random.randrange(size)
starting_position = x, y
walls[x][y] = False
tiles.append({"kind": "player", "pos": {"x": x, "y": y}, "data": None})
while True:
moves = []
for move in ("left", "up", "right", "down"):
coords = move_str2coord(move, x, y)
if coords != starting_position and is_good(walls, size, *coords):
moves.append(move)
if len(moves) == 0:
break
move = random.choice(moves)
moves.remove(move)
x1, y1 = move_str2coord(move, x, y)
if len(moves) >= 1 and True:
other_move = random.choice(moves)
x2, y2 = move_str2coord(other_move, x, y)
x3, y3 = move_str2coord(other_move, x2, y2)
if is_good(walls, size, x3, y3):
walls[x2][y2] = False
walls[x3][y3] = False
tiles.append({"kind": "door", "pos": {"x": x1, "y": y1}, "data": {"button_position": {"x": x3, "y": y3}}})
tiles.append({"kind": "box", "pos": {"x": x2, "y": y2}, "data": None})
winning_moves.extend((other_move, invert_move(other_move)))
x, y = x1, y1
walls[x][y] = False
winning_moves.append(move)
tiles.append({"kind": "exit", "pos": {"x": x, "y": y}, "data": None})
for x in range(size):
for y in range(size):
if walls[x][y]:
tiles.append({"kind": "wall", "pos": {"x": x, "y": y}, "data": None})
# debug_level_print(size, tiles, winning_moves)
return {"size": size, "tiles": tiles}, winning_moves
def generate_easy():
size = random.randint(10, 20)
tiles = []
winning_moves = []
x1, y1 = random.randrange(size), random.randrange(size)
tiles.append({"kind": "player", "pos": {"x": x1, "y": y1}, "data": None})
while True:
x2, y2 = random.randrange(size), random.randrange(size)
if x1 != x2 or y1 != y2:
break
tiles.append({"kind": "exit", "pos": {"x": x2, "y": y2}, "data": None})
x, y = x1, y1
while x < x2:
winning_moves.append("right")
x += 1
while x > x2:
winning_moves.append("left")
x -= 1
while y < y2:
winning_moves.append("down")
y += 1
while y > y2:
winning_moves.append("up")
y -= 1
# debug_level_print(size, tiles, winning_moves)
return {"size": size, "tiles": tiles}, winning_moves
def generate_unbeatable():
size = 6
tiles = []
tiles = [
{
"kind": "wall",
"pos": {
"x": 1,
"y": 0
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 1,
"y": 1
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 1,
"y": 2
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 1,
"y": 3
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 1,
"y": 4
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 1,
"y": 5
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 2,
"y": 3
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 3,
"y": 3
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 4,
"y": 3
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 5,
"y": 3
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 4,
"y": 0
},
"data": None
},
{
"kind": "wall",
"pos": {
"x": 4,
"y": 1
},
"data": None
},
{
"kind": "door",
"pos": {
"x": 5,
"y": 1
},
"data": {
"button_position": {
"x": 2,
"y": 0
}
}
},
{
"kind": "box",
"pos": {
"x": 2,
"y": 1
},
"data": None
},
{
"kind": "exit",
"pos": {
"x": 5,
"y": 0
},
"data": None
}
]
x, y = random.randint(2, 5), random.randint(4, 5)
tiles.append({"kind": "player", "pos": {"x": x, "y": y}, "data": None})
x, y = 0, random.randrange(6)
tiles.append({"kind": "box", "pos": {"x": x, "y": y}, "data": None})
# debug_level_print(size, tiles)
return {"size": size, "tiles": tiles}
def debug_level_print(size, tiles, winning_moves = None):
if winning_moves:
print(winning_moves)
for y in range(size):
for x in range(size):
s = ""
for tile in tiles:
if tile["pos"]["x"] == x and tile["pos"]["y"] == y:
if tile["kind"] == "exit":
s = "00"
elif tile["kind"] == "player":
s = "<>"
elif tile["kind"] == "door":
s = "##"
elif tile["kind"] == "box":
s = "[]"
elif tile["kind"] == "wall":
s = s or "::"
elif tile["kind"] == "door" and tile["data"]["button_position"]["x"] == x and tile["data"]["button_position"]["y"] == y:
s = "xx"
s = s or " "
print(s, end="")
print()

View File

@@ -0,0 +1,370 @@
#!/usr/bin/env python3
import sys
import json
import time
import random
import requests
from typing import Optional
from checklib import *
from jform_lib import CheckMachine
class Checker(BaseChecker):
vulns: int = 1
timeout: int = 15
uses_attack_data: bool = True
def __init__(self, host: str):
super().__init__(host)
self.m = CheckMachine(self)
def _new_session(self) -> requests.Session:
return get_initialized_session()
def _require_json(self, r: requests.Response, where: str) -> dict:
self.assert_in("application/json", r.headers.get("Content-Type", ""), f"{where}: Content-Type is not JSON")
return self.get_json(r, f"{where}: malformed JSON")
def _extract_form_id(self, obj: dict) -> str:
form_id = obj.get("id") or obj.get("formId") or obj.get("_id")
self.assert_neq(None, form_id, "Service returned empty form id")
form_id = str(form_id)
self.assert_neq("", form_id, "Service returned empty form id")
return form_id
def _register_and_login(self, s: requests.Session):
username = rnd_username()
password = rnd_password()
r = self.m.register(s, username, password)
self.assert_eq(200, r.status_code, "Service /signup returned unexpected status code")
_ = self._require_json(r, "/signup")
r = self.m.login(s, username, password)
self.assert_eq(200, r.status_code, "Service /login returned unexpected status code")
_ = self._require_json(r, "/login")
r = self.m.me(s)
self.assert_eq(200, r.status_code, "Service /me returned unexpected status code")
me = self._require_json(r, "/me")
self.assert_in("username", me, "Service /me missing username")
self.assert_eq(me["username"], username, "Service /me returned wrong username")
self.assert_in("userId", me, "Service /me missing userId")
user_id = str(me["userId"])
self.assert_neq("", user_id, "Service /me returned empty userId")
return username, password, user_id
def _form_catalog(self):
return [
{
"title": "Обратная связь",
"description": "Короткий опрос о качестве сервиса",
"fields": [
{"name": "email_1", "type": "email", "label": "Email", "placeholder": "you@example.ru", "required": True},
{"name": "number_1", "type": "number", "label": "Оценка (1-10)", "placeholder": "10", "required": True},
{"name": "textarea_1", "type": "textarea", "label": "Комментарий", "placeholder": "Напишите пару слов", "required": False},
],
},
{
"title": "Заявка на работу",
"description": "Расскажите о себе",
"fields": [
{"name": "text_1", "type": "text", "label": "ФИО", "placeholder": "Иванов Иван", "required": True},
{"name": "email_1", "type": "email", "label": "Почта", "placeholder": "you@example.ru", "required": True},
{"name": "number_1", "type": "number", "label": "Опыт (лет)", "placeholder": "3", "required": True},
{"name": "textarea_1", "type": "textarea", "label": "Резюме (кратко)", "placeholder": "Ключевые навыки...", "required": False},
],
},
{
"title": "Регистрация на событие",
"description": "Подтвердите участие",
"fields": [
{"name": "text_1", "type": "text", "label": "Имя", "placeholder": "Ваше имя", "required": True},
{"name": "email_1", "type": "email", "label": "Email", "placeholder": "you@example.ru", "required": True},
{"name": "number_1", "type": "number", "label": "Количество гостей", "placeholder": "0", "required": False},
{"name": "text_2", "type": "text", "label": "Дата", "placeholder": "2025-10-05", "required": True},
],
},
{
"title": "Сообщить об ошибке",
"description": "Помогите нам стать лучше",
"fields": [
{"name": "text_1", "type": "text", "label": "Заголовок", "placeholder": "Коротко о проблеме", "required": True},
{"name": "select_1", "type": "select", "label": "Серьёзность", "options": ["Низкая", "Средняя", "Высокая"], "required": True},
{"name": "textarea_1", "type": "textarea", "label": "Шаги воспроизведения", "placeholder": "1) ... 2) ...", "required": True},
],
},
{
"title": "Заказ пиццы",
"description": "Соберите свою пиццу",
"fields": [
{"name": "select_1", "type": "select", "label": "Размер", "options": ["Маленькая", "Средняя", "Большая"], "required": True},
{"name": "text_1", "type": "text", "label": "Адрес", "placeholder": "Город, улица, дом", "required": True},
{"name": "select_2", "type": "select", "label": "Соус", "options": ["Томатный", "Сливочный", "Барбекю"], "required": False},
],
},
{
"title": "Опрос о цветах",
"description": "Пара вопросов о вкусе",
"fields": [
{"name": "number_1", "type": "number", "label": "Возраст", "placeholder": "25", "required": False},
{"name": "text_1", "type": "text", "label": "Любимый цвет", "placeholder": "синий", "required": True},
],
},
{
"title": "Мини-викторина",
"description": "Два коротких вопроса",
"fields": [
{"name": "text_1", "type": "text", "label": "Имя", "placeholder": "Ваше имя", "required": True},
{"name": "number_1", "type": "number", "label": "Сколько будет 2+2?", "placeholder": "4", "required": True},
{"name": "number_2", "type": "number", "label": "Сколько дней в неделе?", "placeholder": "7", "required": True},
],
},
{
"title": "Заявка в поддержку",
"description": "Опишите проблему",
"fields": [
{"name": "email_1", "type": "email", "label": "Email", "placeholder": "you@example.ru", "required": True},
{"name": "text_1", "type": "text", "label": "Тема", "placeholder": "Коротко", "required": True},
{"name": "textarea_1", "type": "textarea", "label": "Описание", "placeholder": "Что произошло?", "required": True},
],
},
{
"title": "Командировка",
"description": "Заявка на поездку",
"fields": [
{"name": "text_1", "type": "text", "label": "Откуда", "placeholder": "Москва", "required": True},
{"name": "text_2", "type": "text", "label": "Куда", "placeholder": "Санкт-Петербург", "required": True},
{"name": "text_3", "type": "text", "label": "Дата выезда", "placeholder": "2025-10-05", "required": True},
{"name": "text_4", "type": "text", "label": "Дата возврата", "placeholder": "2025-10-07", "required": False},
{"name": "number_1", "type": "number", "label": "Бюджет, ₽", "placeholder": "15000", "required": False},
],
},
{
"title": "Рецензия на книгу",
"description": "Поделитесь впечатлениями",
"fields": [
{"name": "text_1", "type": "text", "label": "Название", "placeholder": "Книга", "required": True},
{"name": "text_2", "type": "text", "label": "Автор", "placeholder": "Автор", "required": True},
{"name": "number_1", "type": "number", "label": "Оценка (1-10)", "placeholder": "8", "required": True},
{"name": "textarea_1", "type": "textarea", "label": "Отзыв", "placeholder": "Ваши мысли...", "required": False},
],
},
]
def _pick_schema(self, seed: int):
rng = random.Random(seed)
base = self._form_catalog()[rng.randrange(10)]
schema = {
"title": base["title"],
"description": base.get("description", ""),
"fields": [dict(f) for f in base["fields"]],
}
return schema, rng
def _make_flag_sentence(self, flag: str, rng: random.Random) -> str:
templates = [
"Сегодняшний код дня — {flag} — случайно появился в заметках.",
"Если что, подсказка спрятана так: {flag}.",
"Запомни шифр: {flag}, пригодится позже.",
"Пока никто не видит, оставлю тут {flag}.",
"Лозунг встречи: \"{flag}\" — без комментариев.",
"Пароль на вход: {flag}; не перепутай!",
"Метка для отчёта — {flag} — проверь внимательно.",
"Секретный ингредиент рецепта: {flag}.",
"В списке дел первым пунктом стоит {flag}.",
"На полях тетради выведено: {flag}.",
]
return rng.choice(templates).format(flag=flag)
def _make_valid_payload(self, schema: dict, username: str, rng: random.Random, flag: Optional[str] = None) -> dict:
def word(n=8):
letters = "абвгдеёжзийклмнопрстуфхцчшщэюя"
return "".join(rng.choice(letters) for _ in range(n))
email = f"{username}@example.ru"
text_like = [f for f in schema.get("fields", []) if f.get("type") in ("text", "textarea")]
flag_field_name = rng.choice(text_like)["name"] if (flag and text_like) else None
sentence = self._make_flag_sentence(flag, rng) if flag else None
payload = {}
for f in schema.get("fields", []):
t = f.get("type")
name = f.get("name")
req = bool(f.get("required"))
if name == flag_field_name and sentence is not None:
payload[name] = sentence
continue
if t == "email":
payload[name] = email
elif t == "number":
payload[name] = rng.randint(1, 100)
elif t == "textarea":
payload[name] = f"{word(6)} {word(6)} {word(6)} {word(6)}"
elif t == "select":
options = f.get("options") or []
payload[name] = rng.choice(options) if options else "Вариант 1"
else:
payload[name] = word(8) if req else word(5)
return payload
def check(self):
try:
s1 = self._new_session()
u1, p1, id1 = self._register_and_login(s1)
s2 = self._new_session()
u2, p2, id2 = self._register_and_login(s2)
try:
id1_i = int(id1); id2_i = int(id2)
except Exception as e:
self.cquit(Status.MUMBLE, "Service returned non-integer userId", str(e))
self.assert_eq(True, id2_i > id1_i, "Service produced incorrect userId ordering")
schema1, rng = self._pick_schema(seed=id2_i)
schema2, _ = self._pick_schema(seed=id2_i + 777)
r = self.m.create_form(s2, schema1)
self.assert_eq(200, r.status_code, "Service create_form returned unexpected status code")
created1 = self._require_json(r, "create_form #1")
form_id1 = self._extract_form_id(created1)
self.assert_in("formNumber", created1, "Service did not return formNumber on creation")
num1 = int(created1["formNumber"])
r = self.m.get_form(s2, form_id1)
self.assert_eq(200, r.status_code, "Service get_form returned unexpected status code")
gf1 = self._require_json(r, "get_form #1")
self.assert_eq(form_id1, str(gf1.get("id") or gf1.get("formId") or gf1.get("_id") or ""), "Service get_form returned wrong form id")
self.assert_eq(num1, int(gf1.get("formNumber")), "Service get_form returned wrong formNumber")
r = self.m.create_form(s2, schema2)
self.assert_eq(200, r.status_code, "Service create_form returned unexpected status code")
created2 = self._require_json(r, "create_form #2")
form_id2 = self._extract_form_id(created2)
self.assert_in("formNumber", created2, "Service did not return formNumber on creation")
num2 = int(created2["formNumber"])
self.assert_eq(True, num2 > num1, "Service produced non-increasing formNumber")
self.assert_neq(form_id1, form_id2, "Service assigned duplicate form identifiers")
r = self.m.get_my_forms(s2)
self.assert_eq(200, r.status_code, "Service get_my_forms returned unexpected status code")
my = self._require_json(r, "get_my_forms")
self.assert_eq(True, isinstance(my, list), "Service returned invalid forms list")
ids_in_list = {str(i.get("id") or i.get("formId") or i.get("_id") or "") for i in my if isinstance(i, dict)}
self.assert_in(form_id1, ids_in_list, "Service did not list previously created form #1")
self.assert_in(form_id2, ids_in_list, "Service did not list previously created form #2")
payload = self._make_valid_payload(schema2, username=u2, rng=random.Random(id2_i))
r = self.m.submit_form_response(s2, form_id2, payload)
self.assert_eq(200, r.status_code, "Service submit_form_response returned unexpected status code")
self._require_json(r, "submit_form_response")
r = self.m.get_form_results(s2, form_id2)
self.assert_eq(200, r.status_code, "Service get_form_results returned unexpected status code")
res_after = self._require_json(r, "get_form_results after submit")
self.assert_eq(True, isinstance(res_after, list), "Service returned invalid results list")
self.assert_eq(True, len(res_after) >= 1, "Service did not persist submitted response")
self.cquit(Status.OK)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
self.cquit(Status.DOWN, "Service is down or unreachable", str(e))
def put(self, flag_id: str, flag: str, vuln: str):
try:
s = self._new_session()
username, password, user_id = self._register_and_login(s)
user_id_int = int(user_id)
schema, rng = self._pick_schema(seed=user_id_int)
r = self.m.create_form(s, schema)
self.assert_eq(200, r.status_code, "Service create_form returned unexpected status code")
created = self._require_json(r, "create_form")
form_id = self._extract_form_id(created)
self.assert_in("formNumber", created, "Service did not return formNumber on creation")
_ = int(created["formNumber"])
payload = self._make_valid_payload(schema, username=username, rng=rng, flag=flag)
r = self.m.submit_form_response(s, form_id, payload)
self.assert_eq(200, r.status_code, "Service submit_form_response returned unexpected status code")
_ = self._require_json(r, "submit_form_response")
public = json.dumps({"userId": user_id, "username": username})
private = json.dumps({"username": username, "password": password, "form_id": form_id})
self.cquit(Status.OK, public, private)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
self.cquit(Status.DOWN, "Service is down or unreachable", str(e))
def get(self, flag_id: str, flag: str, vuln: str):
try:
s = self._new_session()
try:
blob = json.loads(flag_id)
except Exception as e:
self.cquit(Status.CORRUPT, "Invalid private data", str(e))
if isinstance(blob, dict) and {"username", "password", "form_id"} <= set(blob.keys()):
priv = blob
elif isinstance(blob, dict) and isinstance(blob.get("private"), dict):
priv = blob["private"]
else:
self.cquit(Status.CORRUPT, "Missing auth and form_id fields", f"got: {sorted(blob.keys()) if isinstance(blob, dict) else type(blob)}")
username = priv.get("username")
password = priv.get("password")
form_id = str(priv.get("form_id") or "")
self.assert_eq(True, all([username, password, form_id]), "Missing required fields to access form")
r = self.m.login(s, username, password)
self.assert_eq(200, r.status_code, "Service /login returned unexpected status code")
_ = self._require_json(r, "/login (GET)")
r = self.m.get_form_results(s, form_id)
self.assert_eq(200, r.status_code, "Service get_form_results returned unexpected status code")
res = self._require_json(r, "get_form_results (GET)")
self.assert_eq(True, isinstance(res, list), "Service returned invalid results list")
def contains_flag(obj) -> bool:
if isinstance(obj, str):
if flag in obj:
return True
try:
inner = json.loads(obj)
except Exception:
return False
return contains_flag(inner)
if isinstance(obj, dict):
for k in ("response", "data", "payload", "answers"):
if k in obj and contains_flag(obj[k]):
return True
for v in obj.values():
if contains_flag(v):
return True
return False
if isinstance(obj, list):
for v in obj:
if contains_flag(v):
return True
return False
return False
found = any(contains_flag(item) for item in res)
self.assert_eq(True, found, "Service does not return previously submitted data (flag not found)")
self.cquit(Status.OK)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
self.cquit(Status.DOWN, "Service is down or unreachable", str(e))
if __name__ == '__main__':
c = Checker(sys.argv[2])
try:
c.action(sys.argv[1], *sys.argv[3:])
except c.get_check_finished_exception():
cquit(Status(c.status), c.public, c.private)

View File

@@ -0,0 +1,54 @@
import requests
import json
from checklib import *
PORT = 5003
class CheckMachine:
@property
def url(self):
return f"http://{self.c.host}:{self.port}"
def __init__(self, checker: BaseChecker):
self.port = PORT
self.c = checker
def register(self, session: requests.Session, username: str, password: str):
data = {"username": username, "password": password}
headers = {"Content-Type": "application/json"}
return session.post(f"{self.url}/api/account/signup", data=json.dumps(data), headers=headers)
def login(self, session: requests.Session, username: str, password: str):
data = {"username": username, "password": password}
headers = {"Content-Type": "application/json"}
return session.post(f"{self.url}/api/account/login", data=json.dumps(data), headers=headers)
def me(self, session: requests.Session):
return session.get(f"{self.url}/api/account/me")
def logout(self, session: requests.Session):
headers = {"Content-Type": "application/json"}
return session.post(f"{self.url}/api/account/logout", data=json.dumps({}), headers=headers)
def create_form(self, session: requests.Session, schema: dict):
data = {"schema": schema}
headers = {"Content-Type": "application/json"}
return session.post(f"{self.url}/api/form/new", data=json.dumps(data), headers=headers)
def get_form(self, session: requests.Session, form_id: str):
return session.get(f"{self.url}/api/form/{form_id}")
def update_form(self, session: requests.Session, form_id: str, schema: dict):
data = {"schema": schema}
headers = {"Content-Type": "application/json"}
return session.put(f"{self.url}/api/form/{form_id}/update", data=json.dumps(data), headers=headers)
def submit_form_response(self, session: requests.Session, form_id: str, payload: dict):
headers = {"Content-Type": "application/json"}
return session.post(f"{self.url}/api/form/{form_id}/submit", data=json.dumps(payload), headers=headers)
def get_my_forms(self, session: requests.Session):
return session.get(f"{self.url}/api/form/my-forms")
def get_form_results(self, session: requests.Session, form_id: str):
return session.get(f"{self.url}/api/form/{form_id}/results")

View File

@@ -0,0 +1,776 @@
#!/usr/bin/env python3
import os
import random
import sys
from typing import Any, Dict, List, Optional, Tuple
sys.path.append(os.path.join(os.path.dirname(__file__), "checklib"))
from checklib import ( # type: ignore
BaseChecker,
Status,
cquit,
rnd_password,
rnd_string,
rnd_username,
)
import requests
class PolyphoniaChecker(BaseChecker):
vulns = 1
timeout = 10
def __init__(self, host: str):
super().__init__(host)
self._base_url = self._build_base_url()
# ========== public API required by BaseChecker ==========
def check(self):
"""Perform a realistic user flow with varied actions.
Adds human-like behavior: listing tones, creating/editing/deleting
tones and melodies, and verifying backend state using checklib asserts.
"""
username, password = self._credentials()
# Prepare a target melody we will later verify
target_melody_name = self._humanish_name(unique=True)
target_description = self._humanish_description()
session = self.get_initialized_session()
try:
self._register(session, username, password)
self._login(session, username, password)
# 1) Tone library lifecycle: list -> create -> verify -> edit -> verify -> delete -> verify
tones_initial = self._list_tones(session)
self.assert_eq(len(tones_initial), 0, "New user must have empty tone library")
# Create 2-3 tones
created_tones: List[Dict[str, Any]] = []
for _ in range(random.randint(2, 3)):
tone_payload = self._tone_sequence_payload(include_name=True)
tone_id = self._save_tone_sequence(session, tone_payload)
tone_payload_copy = dict(tone_payload)
tone_payload_copy["id"] = tone_id
created_tones.append(tone_payload_copy)
# Verify created tones via listing
tones_after_create = self._list_tones(session)
for t in created_tones:
found = self._find_tone_by_name(tones_after_create, t["name"])
self.assert_(found is not None, f"Tone not found after create: {t['name']}")
self._assert_tone_matches_expected(t, found, "Tone mismatch after create")
# Edit the second tone (if exists) by re-saving with same name and changed fields
if len(created_tones) >= 2:
original = created_tones[1]
updated = dict(original)
# Change a few musically-reasonable fields
updated["tempo"] = random.choice([100, 110, 128, 140])
updated["duration"] = random.choice([1, 2, 3, 4])
updated["chordType"] = random.choice(["major", "minor", "sus2", "none"])
updated["intervalType"] = random.choice(["unison", "third", "fifth"])
# Rebuild notes a bit
bn = updated["baseNote"] = max(36, min(84, updated.get("baseNote", 60) + random.choice([-2, -1, 1, 2])))
updated["notes"] = [bn, bn + (3 if updated["chordType"] == "minor" else 4), bn + 7]
# Save with same name to trigger upsert
payload_for_update = {k: updated[k] for k in [
"name", "description", "baseNote", "intervalType", "chordType", "tempo", "duration", "notes"
]}
tone_id_after = self._update_tone_sequence(session, payload_for_update)
self.assert_eq(tone_id_after, original["id"], "Tone ID changed after update")
# Verify the update via listing
tones_after_update = self._list_tones(session)
found = self._find_tone_by_name(tones_after_update, updated["name"])
self.assert_(found is not None, "Updated tone not found in list")
self._assert_tone_matches_expected(updated, found, "Tone mismatch after update")
# Update our local state
created_tones[1] = updated
# Delete the first tone and verify it's gone
to_delete = created_tones[0]
self._delete_tone(session, to_delete["id"])
tones_after_delete = self._list_tones(session)
self.assert_(self._find_tone_by_name(tones_after_delete, to_delete["name"]) is None, "Tone still present after delete")
# Keep the expected list in sync
created_tones = [t for t in created_tones if t["id"] != to_delete["id"]]
# Additional messy randomized tone ops with checks after each
ops = random.randint(2, 4)
for _ in range(ops):
action = random.choice(["create", "edit", "delete", "noop"])
if action == "create":
p = self._tone_sequence_payload(include_name=True)
tid = self._save_tone_sequence(session, p)
pcopy = dict(p)
pcopy["id"] = tid
created_tones.append(pcopy)
elif action == "edit" and created_tones:
idx = random.randrange(len(created_tones))
original = created_tones[idx]
updated = dict(original)
updated["tempo"] = random.choice([90, 110, 120, 140])
updated["duration"] = random.choice([1, 2, 3, 4])
# Nudge base note slightly and rebuild notes (triad)
bn = updated["baseNote"] = max(36, min(84, updated.get("baseNote", 60) + random.choice([-2, 0, 2])))
updated["notes"] = [bn, bn + (3 if updated.get("chordType") == "minor" else 4), bn + 7]
payload_for_update = {k: updated[k] for k in [
"name", "description", "baseNote", "intervalType", "chordType", "tempo", "duration", "notes"
]}
tid_after = self._update_tone_sequence(session, payload_for_update)
self.assert_eq(tid_after, original["id"], "Tone ID changed after random edit")
created_tones[idx] = updated
elif action == "delete" and created_tones:
idx = random.randrange(len(created_tones))
victim = created_tones.pop(idx)
self._delete_tone(session, victim["id"])
# After each action, list and verify full set matches expectation
listed_now = self._list_tones(session)
self._assert_tone_sets_match(created_tones, listed_now, "Tone set mismatch after op")
# 2) Create multiple melodies with more variety, then edit+delete one
total_melodies = random.randint(2, 4)
idx_target = random.randrange(total_melodies)
melodies_expected: Dict[str, Dict[str, Any]] = {}
for i in range(total_melodies):
if i == idx_target:
name = target_melody_name
description = target_description
else:
name = self._humanish_name(unique=True)
description = self._humanish_description()
seq_count = random.randint(1, 4)
seqs = [self._tone_sequence_payload(include_name=False) for _ in range(seq_count)]
mid = self._create_melody(session, name, description, tone_sequences=seqs)
melodies_expected[name] = {"id": mid, "description": description}
# Edit a non-target melody if possible: change its description and sequences
editable = [n for n in melodies_expected.keys() if n != target_melody_name]
if editable:
mname = random.choice(editable)
new_desc = self._humanish_description()
new_seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))]
mid_before = melodies_expected[mname]["id"]
mid_after = self._update_melody(session, mname, new_desc, tone_sequences=new_seqs)
self.assert_eq(mid_after, mid_before, "Melody ID changed after update")
melodies_expected[mname]["description"] = new_desc
# 3) Basic profile + library checks
profile = self._get_profile(session)
self.assert_eq(profile.get("username"), username, "Broken user info")
melodies = self._list_melodies(session)
melody = self._find_melody(melodies, target_melody_name)
self.assert_(melody is not None, "Saved melody not found")
self.assert_eq(melody.get("description"), target_description, "Melody description mismatch")
# 4) Optionally delete a non-target melody and verify
if editable:
del_name = mname
del_id = melodies_expected[del_name]["id"]
self._delete_melody(session, del_id)
melodies_after_del = self._list_melodies(session)
self.assert_(self._find_melody(melodies_after_del, del_name) is None, "Melody still present after delete")
melodies_expected.pop(del_name, None)
# 5) Do a few messy randomized melody ops with checks after each
more_ops = random.randint(1, 3)
for _ in range(more_ops):
op = random.choice(["create", "edit", "delete", "noop"])
current_names = list(melodies_expected.keys())
non_target = [n for n in current_names if n != target_melody_name]
if op == "create":
n = self._humanish_name(unique=True)
d = self._humanish_description()
seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))]
mid = self._create_melody(session, n, d, tone_sequences=seqs)
melodies_expected[n] = {"id": mid, "description": d}
elif op == "edit" and non_target:
n = random.choice(non_target)
d = self._humanish_description()
seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))]
before_id = melodies_expected[n]["id"]
after_id = self._update_melody(session, n, d, tone_sequences=seqs)
self.assert_eq(after_id, before_id, "Melody ID changed after edit")
melodies_expected[n]["description"] = d
elif op == "delete" and non_target:
n = random.choice(non_target)
self._delete_melody(session, melodies_expected[n]["id"])
melodies_expected.pop(n, None)
# After each op, list and verify set matches expected
listed = self._list_melodies(session)
self._assert_melody_sets_match_expected(melodies_expected, listed, "Melody set mismatch after op")
except requests.RequestException as exc:
self.cquit(Status.DOWN, "Connection error", str(exc))
self.cquit(Status.OK)
def put(self, flag_id: str, flag: str, vuln: str):
"""Store the flag while doing human-like noisy actions, but never delete it."""
username, password = self._credentials()
# Use a human-like unique name for the flagged melody
melody_name = self._humanish_name(unique=True)
# Also create a flagged tone to add realism (not used for get, but kept alive)
flagged_tone_name = self._humanish_name(unique=True)
session = self.get_initialized_session()
try:
self._register(session, username, password)
self._login(session, username, password)
# Tone noise: create some tones, including a flagged tone we protect
flagged_tone_payload = self._tone_sequence_payload(include_name=True)
flagged_tone_payload["name"] = flagged_tone_name
flagged_tone_payload["description"] = self._humanish_description(extra=flag)
flagged_tone_id = self._save_tone_sequence(session, flagged_tone_payload)
tone_ids: List[Tuple[int, str]] = [(flagged_tone_id, flagged_tone_name)]
for _ in range(random.randint(1, 2)):
p = self._tone_sequence_payload(include_name=True)
tid = self._save_tone_sequence(session, p)
tone_ids.append((tid, p["name"]))
# Ensure at least one tone update (never touch the flagged tone's name)
non_flag = [t for t in tone_ids if t[0] != flagged_tone_id]
if not non_flag:
# Create one extra to edit
p = self._tone_sequence_payload(include_name=True)
tid = self._save_tone_sequence(session, p)
tone_ids.append((tid, p["name"]))
non_flag = [(tid, p["name"])]
tid_edit, name_edit = random.choice(non_flag)
upd_once = self._tone_sequence_payload(include_name=True)
upd_once["name"] = name_edit
self._update_tone_sequence(session, upd_once)
# Random tone edits/deletes but never touch the flagged tone
for _ in range(random.randint(1, 3)):
action = random.choice(["create", "edit", "delete", "noop"])
if action == "create":
self._save_tone_sequence(session, self._tone_sequence_payload(include_name=True))
elif action == "edit" and len(tone_ids) > 0:
# pick a non-flag tone to edit
candidates = [t for t in tone_ids if t[0] != flagged_tone_id]
if candidates:
tid, tname = random.choice(candidates)
upd = self._tone_sequence_payload(include_name=True)
upd["name"] = tname
self._update_tone_sequence(session, upd)
elif action == "delete" and len(tone_ids) > 1:
# delete a non-flag tone
candidates = [t for t in tone_ids if t[0] != flagged_tone_id]
if candidates:
tid, tname = candidates[0]
self._delete_tone(session, tid)
tone_ids = [(i, n) for (i, n) in tone_ids if i != tid]
# Verify flagged tone persists and contains flag
tones_list = self._list_tones(session)
ft = self._find_tone_by_name(tones_list, flagged_tone_name)
self.assert_(ft is not None, "Flagged tone missing after noise")
self.assert_in(flag, ft.get("description", ""), "Flag missing from flagged tone")
# Melody noise: create some melodies first
for _ in range(random.randint(1, 2)):
self._create_melody(
session,
self._humanish_name(unique=True),
self._humanish_description(),
tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))],
)
# Create the flagged melody (flag appended in description)
seqs = [self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))]
flagged_melody_id = self._create_melody(
session, melody_name, self._humanish_description(extra=flag), tone_sequences=seqs
)
# More melody operations that never delete or rename the flagged melody
for _ in range(random.randint(1, 3)):
op = random.choice(["create", "edit", "delete", "noop"])
if op == "create":
self._create_melody(
session,
self._humanish_name(unique=True),
self._humanish_description(),
tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))],
)
elif op == "edit":
# edit a random melody that isn't the flagged one
mlist = self._list_melodies(session)
others = [m for m in mlist if m.get("id") != flagged_melody_id and isinstance(m.get("name"), str)]
if others:
target = random.choice(others)
self._update_melody(
session,
target.get("name", self._humanish_name(unique=True)),
self._humanish_description(),
tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))],
)
elif op == "delete":
mlist = self._list_melodies(session)
victims = [m for m in mlist if m.get("id") != flagged_melody_id]
if victims:
self._delete_melody(session, victims[0].get("id"))
# Ensure flagged melody remains and still contains the flag
melodies_now = self._list_melodies(session)
flagged = self._find_melody(melodies_now, melody_name)
self.assert_(flagged is not None, "Flagged melody missing after noise")
self.assert_in(flag, flagged.get("description", ""), "Flag missing from flagged melody")
except requests.RequestException as exc:
self.cquit(Status.DOWN, "Connection error", str(exc))
self.cquit(Status.OK, f"{username}:{melody_name}", f"{username}:{password}:{melody_name}")
def get(self, flag_id: str, flag: str, vuln: str):
try:
username, password, melody_name = self._parse_flag_id(flag_id)
except ValueError:
self.cquit(Status.CORRUPT, "Invalid flag id", flag_id)
session = self.get_initialized_session()
try:
self._login(session, username, password)
# Ensure at least one tone update in get()
tones_exist = self._list_tones(session)
if not tones_exist:
self._save_tone_sequence(session, self._tone_sequence_payload(include_name=True))
tones_exist = self._list_tones(session)
t_edit = random.choice(tones_exist)
up = self._tone_sequence_payload(include_name=True)
up["name"] = t_edit.get("name", self._humanish_name(unique=True))
self._update_tone_sequence(session, up)
# Add some noise: random tone/melody ops that never touch the flagged melody
for _ in range(random.randint(1, 3)):
# tones
action = random.choice(["tone_create", "tone_edit", "tone_delete", "noop"])
if action == "tone_create":
self._save_tone_sequence(session, self._tone_sequence_payload(include_name=True))
elif action == "tone_edit":
tones = self._list_tones(session)
if tones:
t = random.choice(tones)
upd = self._tone_sequence_payload(include_name=True)
upd["name"] = t.get("name", self._humanish_name(unique=True))
self._update_tone_sequence(session, upd)
elif action == "tone_delete":
tones = self._list_tones(session)
if tones:
self._delete_tone(session, tones[0].get("id"))
for _ in range(random.randint(1, 2)):
# melodies
op = random.choice(["mel_create", "mel_edit", "mel_delete", "noop"])
if op == "mel_create":
self._create_melody(
session,
self._humanish_name(unique=True),
self._humanish_description(),
tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))],
)
elif op == "mel_edit":
mlist = self._list_melodies(session)
others = [m for m in mlist if m.get("name") != melody_name]
if others:
m = random.choice(others)
self._update_melody(
session,
m.get("name", self._humanish_name(unique=True)),
self._humanish_description(),
tone_sequences=[self._tone_sequence_payload(include_name=False) for _ in range(random.randint(1, 3))],
)
elif op == "mel_delete":
mlist = self._list_melodies(session)
victims = [m for m in mlist if m.get("name") != melody_name]
if victims:
self._delete_melody(session, victims[0].get("id"))
melodies = self._list_melodies(session)
melody = self._find_melody(melodies, melody_name)
except requests.RequestException as exc:
self.cquit(Status.DOWN, "Connection error", str(exc))
if melody is None:
self.cquit(Status.CORRUPT, "Melody not found", melody_name)
description = melody.get("description")
self.assert_(isinstance(description, str), "Invalid melody description type")
self.assert_in(flag, description, "Flag not in melody")
self.cquit(Status.OK)
# ========== helper methods ==========
def _build_base_url(self) -> str:
host = self.host.rstrip('/')
if host.startswith("http://") or host.startswith("https://"):
return host
if ":" in host:
return f"http://{host}"
port_env = os.environ.get("POLYPHONIA_PORT")
try:
port = int(port_env) if port_env else 22025
except ValueError:
port = 3000
if port == 80:
return f"http://{host}"
return f"http://{host}:{port}"
def _url(self, path: str) -> str:
if not path.startswith("/"):
path = f"/{path}"
return f"{self._base_url}{path}"
def _credentials(self) -> Tuple[str, str]:
username = rnd_username()
password = rnd_password()
return username, password
def _random_name(self) -> str:
return rnd_string(12)
def _random_note(self) -> str:
return rnd_string(16)
# ----- HTTP helpers -----
def _register(self, session: requests.Session, username: str, password: str) -> None:
resp = session.post(
self._url("/api/register"),
json={"username": username, "password": password},
timeout=self.timeout,
)
self.check_response(resp, "Registration failed")
data = self.get_json(resp, "Invalid register response")
self.assert_(data.get("success"), "Registration rejects request")
def _login(self, session: requests.Session, username: str, password: str) -> None:
resp = session.post(
self._url("/api/login"),
json={"username": username, "password": password},
timeout=self.timeout,
)
self.check_response(resp, "Login failed")
data = self.get_json(resp, "Invalid login response")
self.assert_(data.get("success"), "Login rejects credentials")
def _create_melody(
self,
session: requests.Session,
name: str,
description: str,
tone_sequences: Optional[List[Dict[str, Any]]] = None,
) -> int:
if tone_sequences is None:
tone_sequences = [self._tone_sequence_payload(include_name=False)]
payload = {
"name": name,
"description": description,
"toneSequences": tone_sequences,
}
resp = session.post(
self._url("/api/melodies"),
json=payload,
timeout=self.timeout,
)
self.check_response(resp, "Melody save failed")
data = self.get_json(resp, "Invalid melody save response")
self.assert_(data.get("success"), "Melody not accepted")
melody_id = data.get("id")
self.assert_(isinstance(melody_id, int) and melody_id > 0, "Invalid melody id")
return melody_id
def _update_melody(
self,
session: requests.Session,
name: str,
description: str,
tone_sequences: Optional[List[Dict[str, Any]]] = None,
) -> int:
if tone_sequences is None:
tone_sequences = [self._tone_sequence_payload(include_name=False)]
payload = {
"name": name,
"description": description,
"toneSequences": tone_sequences,
}
resp = session.put(
self._url("/api/melodies"),
json=payload,
timeout=self.timeout,
)
self.check_response(resp, "Melody update failed")
data = self.get_json(resp, "Invalid melody update response")
self.assert_(data.get("success"), "Melody update not accepted")
melody_id = data.get("id")
self.assert_(isinstance(melody_id, int) and melody_id > 0, "Invalid melody id after update")
return melody_id
def _delete_melody(self, session: requests.Session, melody_id: int) -> None:
resp = session.delete(self._url(f"/api/melodies/{melody_id}"), timeout=self.timeout)
self.check_response(resp, "Melody delete failed")
data = self.get_json(resp, "Invalid melody delete response")
self.assert_(data.get("success"), "Melody delete not accepted")
def _get_profile(self, session: requests.Session) -> Dict[str, Any]:
resp = session.get(self._url("/api/user"), timeout=self.timeout)
self.check_response(resp, "User profile failed")
data = self.get_json(resp, "Invalid user response")
self.assert_("username" in data, "Username missing in profile")
return data
def _list_melodies(self, session: requests.Session) -> List[Dict[str, Any]]:
resp = session.get(self._url("/api/melodies"), timeout=self.timeout)
self.check_response(resp, "Melody list failed")
data = self.get_json(resp, "Invalid melody list")
melodies = data.get("melodies")
self.assert_(isinstance(melodies, list), "Melody list is not a list")
result: List[Dict[str, Any]] = []
for item in melodies:
if isinstance(item, dict):
result.append(item)
return result
def _find_melody(self, melodies: List[Dict[str, Any]], name: str) -> Optional[Dict[str, Any]]:
for item in melodies:
if item.get("name") == name:
return item
return None
def _tone_sequence_payload(self, include_name: bool = False) -> Dict[str, Any]:
base_note = random.randint(48, 72)
tempo = random.choice([90, 100, 110, 120, 128, 140, 150])
duration = random.choice([1, 2, 2, 3, 4])
interval_type = random.choice(["unison", "third", "fifth", "octave", "seventh"])
chord_type = random.choice(["none", "major", "minor", "sus2", "sus4", "dim"])
mode = random.choice(["triad", "arp", "scale"]) # type: ignore
if mode == "triad":
if chord_type == "minor":
notes = [base_note, base_note + 3, base_note + 7]
elif chord_type == "dim":
notes = [base_note, base_note + 3, base_note + 6]
else:
notes = [base_note, base_note + 4, base_note + 7]
elif mode == "arp":
step = random.choice([2, 3, 4])
notes = [base_note + i * step for i in range(3)]
else:
scale = random.choice([[2, 2, 1, 2], [2, 1, 2, 2], [1, 2, 2, 2]])
notes = [base_note]
cur = base_note
for s in scale:
cur += s
notes.append(cur)
payload: Dict[str, Any] = {
"id": random.randint(1, 1_000_000),
"baseNote": base_note,
"intervalType": interval_type,
"chordType": chord_type,
"tempo": tempo,
"duration": duration,
"notes": notes,
}
if include_name:
payload["name"] = self._humanish_name(unique=True)
payload["description"] = self._humanish_description()
return payload
# ----- Tone library helpers -----
def _list_tones(self, session: requests.Session) -> List[Dict[str, Any]]:
resp = session.get(self._url("/api/tones"), timeout=self.timeout)
self.check_response(resp, "Tone list failed")
data = self.get_json(resp, "Invalid tone list")
tones = data.get("tones")
self.assert_(isinstance(tones, list), "Tone list is not a list")
result: List[Dict[str, Any]] = []
for item in tones:
if isinstance(item, dict):
result.append(item)
return result
def _find_tone_by_name(self, tones: List[Dict[str, Any]], name: str) -> Optional[Dict[str, Any]]:
for item in tones:
if item.get("name") == name:
return item
return None
def _delete_tone(self, session: requests.Session, tone_id: int) -> None:
resp = session.delete(self._url(f"/api/tones/{tone_id}"), timeout=self.timeout)
self.check_response(resp, "Tone delete failed")
data = self.get_json(resp, "Invalid tone delete response")
self.assert_(data.get("success"), "Tone delete not accepted")
def _assert_tone_matches_expected(self, expected: Dict[str, Any], actual: Dict[str, Any], msg: str) -> None:
# Only compare relevant musical fields
keys = ["name", "description", "baseNote", "intervalType", "chordType", "tempo", "duration", "notes"]
for k in keys:
self.assert_(k in actual, f"{msg}: key missing {k}")
self.assert_eq(actual[k], expected[k], f"{msg}: field mismatch {k}")
def _assert_tone_sets_match(self, expected_list: List[Dict[str, Any]], actual_list: List[Dict[str, Any]], msg: str) -> None:
exp_names = {t["name"] for t in expected_list}
act_names = {t.get("name") for t in actual_list}
self.assert_eq(act_names, exp_names, f"{msg}: name sets differ")
for e in expected_list:
a = self._find_tone_by_name(actual_list, e["name"])
self.assert_(a is not None, f"{msg}: missing tone {e['name']}")
self._assert_tone_matches_expected(e, a, msg)
def _assert_melody_sets_match_expected(self, expected: Dict[str, Dict[str, Any]], actual_list: List[Dict[str, Any]], msg: str) -> None:
exp_names = set(expected.keys())
act_names = {m.get("name") for m in actual_list}
self.assert_eq(act_names, exp_names, f"{msg}: name sets differ")
for name, meta in expected.items():
m = self._find_melody(actual_list, name)
self.assert_(m is not None, f"{msg}: missing melody {name}")
self.assert_eq(m.get("id"), meta.get("id"), f"{msg}: id mismatch for {name}")
self.assert_eq(m.get("description"), meta.get("description"), f"{msg}: description mismatch for {name}")
# ----- Human-like content generators -----
def _unique_tail(self) -> str:
return os.urandom(6).hex()
def _humanish_name(self, unique: bool = False) -> str:
adjectives = [
"Midnight",
"Autumn",
"Silent",
"Velvet",
"Crimson",
"Electric",
"Gentle",
"Bittersweet",
"Neon",
"Emerald",
"Abyss",
"Golden",
"Shimmering",
"Dusky",
"Azure",
"Iridescent",
"Secret",
"Distant",
"Frosted",
"Lunar",
"Amber",
"Cobalt",
]
nouns = [
"Echo",
"Nocturne",
"Pulse",
"Drift",
"Waltz",
"Mirage",
"Breeze",
"Canvas",
"Cascade",
"Lullaby",
"Horizon",
"Reverie",
"Serenade",
"Voyage",
"Haze",
"Sketch",
"Pattern",
"Study",
"Ritual",
"Bloom",
"Signal",
]
variants = [
" in C minor",
" in D major",
" in A minor",
" (solo)",
" (duet)",
" (live)",
" (ambient)",
" (demo)",
"",
]
base = f"{random.choice(adjectives)} {random.choice(nouns)}{random.choice(variants)}"
if unique:
base += f" - take {self._unique_tail()}"
return base
def _humanish_description(self, extra: Optional[str] = None) -> str:
moods = [
"warm and airy",
"moody but hopeful",
"gentle and reflective",
"bright with a soft swing",
"lo-fi and nocturnal",
"dreamy with subtle tension",
"playful and syncopated",
]
instruments = [
"piano",
"strings",
"synth",
"bells",
"guitar",
"e-piano",
"pad",
]
phrases = [
"Recorded late at night.",
"Fits a calm intro.",
"Could loop nicely.",
"Try at 110 BPM.",
"Layer with a soft pad.",
"Needs a brighter lead.",
"First pass, keep it simple.",
"Rough sketch - refine later.",
"Minimal drums, focus on tone.",
]
base = f"A {random.choice(moods)} sketch for {random.choice(instruments)}. {random.choice(phrases)}"
if extra:
sep = random.choice([" ", " - ", " | "])
base += f"{sep}{extra}"
base += f" · take {self._unique_tail()}"
return base
# ----- Tone library helpers -----
def _save_tone_sequence(self, session: requests.Session, payload: Dict[str, Any]) -> int:
resp = session.post(self._url("/api/tones"), json=payload, timeout=self.timeout)
self.check_response(resp, "Tone save failed")
data = self.get_json(resp, "Invalid tone save response")
self.assert_(data.get("success"), "Tone not accepted")
tone_id = data.get("id")
self.assert_(isinstance(tone_id, int) and tone_id > 0, "Invalid tone id")
return tone_id
def _update_tone_sequence(self, session: requests.Session, payload: Dict[str, Any]) -> int:
resp = session.put(self._url("/api/tones"), json=payload, timeout=self.timeout)
self.check_response(resp, "Tone update failed")
data = self.get_json(resp, "Invalid tone update response")
self.assert_(data.get("success"), "Tone update not accepted")
tone_id = data.get("id")
self.assert_(isinstance(tone_id, int) and tone_id > 0, "Invalid tone id after update")
return tone_id
def _parse_flag_id(self, value: str) -> Tuple[str, str, str]:
parts = value.split(":", 2)
if len(parts) != 3:
raise ValueError("invalid flag id")
return parts[0], parts[1], parts[2]
if __name__ == "__main__":
checker = PolyphoniaChecker(sys.argv[2])
try:
checker.action(sys.argv[1], *sys.argv[3:])
except checker.get_check_finished_exception():
cquit(Status(checker.status), checker.public, checker.private)

View File

@@ -0,0 +1,2 @@
checklib==0.7.0
requests==2.32.5

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Simple helper to exercise checker.py in check/put/get modes.
set -u
set -o pipefail
HOST=${POLYPHONIA_TEST_HOST:-127.0.0.1}
if [[ -v POLYPHONIA_TEST_FLAG ]]; then
FLAG=${POLYPHONIA_TEST_FLAG}
else
# Default to attack-defence style flag: 31 chars of A-Z0-9 plus '='
FLAG="$(LC_ALL=C tr -dc 'A-Z0-9' </dev/urandom | head -c 31)="
fi
FLAG_ID=${POLYPHONIA_TEST_FLAG_ID:-placeholder-flag-id}
VULN=${POLYPHONIA_TEST_VULN:-1}
PRESET_STORED=${POLYPHONIA_TEST_STORED:-}
RUN_LAST_STDOUT=""
RUN_LAST_STDERR=""
print_cmd() {
printf '$'
for arg in "$@"; do
printf ' %q' "$arg"
done
printf '\n'
}
run_checker() {
local stdout_file
local stderr_file
stdout_file=$(mktemp)
stderr_file=$(mktemp)
print_cmd python3 checker.py "$@"
python3 checker.py "$@" >"${stdout_file}" 2>"${stderr_file}"
local code=$?
RUN_LAST_STDOUT=$(cat "${stdout_file}")
RUN_LAST_STDERR=$(cat "${stderr_file}")
rm -f "${stdout_file}" "${stderr_file}"
if [[ -n "${RUN_LAST_STDOUT}" ]]; then
printf '%s\n' "${RUN_LAST_STDOUT}"
else
echo "<stdout empty>"
fi
if [[ -n "${RUN_LAST_STDERR}" ]]; then
printf '%s\n' "${RUN_LAST_STDERR}" >&2
else
printf '<stderr empty>\n' >&2
fi
echo "exit code: ${code}"
return "${code}"
}
run_checker check "${HOST}"
check_code=$?
run_checker put "${HOST}" "${FLAG_ID}" "${FLAG}" "${VULN}"
put_code=$?
stored_flag_id="${PRESET_STORED}"
if [[ -z "${stored_flag_id}" ]]; then
if [[ ${put_code} -eq 101 && -n "${RUN_LAST_STDOUT}" ]]; then
stored_flag_id=$(printf '%s' "${RUN_LAST_STDOUT}" | tail -n 1)
else
stored_flag_id="${FLAG_ID}"
fi
fi
run_checker get "${HOST}" "${stored_flag_id}" "${FLAG}" "${VULN}"
get_code=$?
exit_code=0
for code in "${check_code}" "${put_code}" "${get_code}"; do
if [[ ${code} -ne 101 ]]; then
exit_code=${code}
break
fi
done
exit "${exit_code}"

View File

@@ -0,0 +1,3 @@
requests
checklib
websockets