Metadata-Version: 2.4
Name: ctf-attackapi
Version: 0.3.0
Summary: Get attack infos in attack-defense CTFs quickly to your exploits. CTF-agnostic and cached.
Keywords: Attack-Defense,CTF,Attack API,Attack Info,Flag IDs,FAUST CTF,ENOWARS,saarCTF
Author: Markus Bauer
Author-email: Markus Bauer <markus.bauer@cispa.saarland>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Education
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Dist: aiohttp>=3.13.3
Requires-Dist: aiologic>=0.16.0
Requires-Dist: filelock>=3.19.1
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: typing-extensions>=4.15.0
Requires-Dist: gunicorn>=23.0.0 ; extra == 'server'
Requires-Python: >=3.9
Project-URL: Homepage, https://github.com/Attacking-Lab/ctf-attackapi
Project-URL: Repository, https://github.com/Attacking-Lab/ctf-attackapi
Project-URL: Issues, https://github.com/Attacking-Lab/ctf-attackapi/issues
Project-URL: Background, https://wiki.attacking-lab.com/attack-defense/
Provides-Extra: server
Description-Content-Type: text/markdown

CTF AttackAPI - Cached and Unified!
===================================

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/license/mit)
![Python](https://img.shields.io/pypi/pyversions/ctf-attackapi)
![Types](https://img.shields.io/pypi/types/ctf-attackapi)
[![Python package tests](https://github.com/Attacking-Lab/ctf-attackapi/actions/workflows/python-package.yml/badge.svg)](https://github.com/Attacking-Lab/ctf-attackapi/actions/workflows/python-package.yml)
[![PyPI version](https://img.shields.io/pypi/v/ctf-attackapi)](https://pypi.org/project/ctf-attackapi)
![Downloads](https://img.shields.io/pypi/dm/ctf-attackapi)
![Repo size](https://img.shields.io/github/repo-size/Attacking-Lab/ctf-attackapi)


Gather attack information quickly in your attack-defense CTF exploits!

During [attack-defense CTF competitions](https://wiki.attacking-lab.com/attack-defense/), you have to write exploits
quickly and run them on a large scale.
These exploits often require information about the targets to attack (teams and sometimes usernames).
This attack info is available as a big JSON file which is updated every few minutes.
Downloading that file for every exploit you're firing is costing time and bandwidth.

This package fetches, parses, and caches attack info for you, so you can focus on writing exploits!

Changes in 0.3.0
----------------

- `flag_id_flat()` is now `flag_ids()`, and `flag_id_raw()` is `flag_ids_raw()`. The list of
  strings is what an exploit wants essentially every time, so it gets the plain name, and only the
  one that hands back the game's own structure carries a suffix. The `attack_info_*` aliases are
  gone with them: flag IDs is what most games call these, so the package calls them that once.
  The REST API follows -- `/api/v1/flag_ids/...` and `/api/v1/flag_ids_raw/...`, returning a
  `flag_ids` key.
- Lookups never raise. `flag_ids()` returns `[]` and `flag_ids_raw()` returns `None` for
  anything they cannot answer, including a `None` team -- so the common
  `flag_ids(service, info.team(name))` no longer dies on a name that did not resolve.
- The mistakes that are wrong on *every* call -- an unknown service or team, a team that never
  resolved, a team of a type the API never took -- now raise a `UserWarning` pointing at your
  line, instead of being indistinguishable from a team that simply has no flag IDs yet. The
  ordinary case stays silent: a known team with nothing published this round is not your mistake.
  Silence the warnings with `warnings.simplefilter("ignore")` if your exploit prefers it.
- Both take an optional `round`, counting back from the newest published round when negative
  (`-1` is the newest). The default is unchanged and still returns every round the game API
  published -- it only publishes the rounds whose flags are still valid, so narrowing by default
  would cost you flags. The selector is public as `attackapi.select_round()`, next to
  `flatten_flag_ids()`.
- New `has_service()`, for the case-insensitive "does this game have that service?" check that
  previously meant reaching into the `flag_ids` field. That field is now private.

Changes in 0.2.0
----------------

- New `atklab` dialect for the ATKLAB gameserver (ECSC 2026): attack info under `attack_info`
  instead of `flag_ids`, and rounds instead of ticks. The `saarctf`, `faustctf` and `enowars`
  dialects are unchanged.
- `AttackInfo` gained `flag_regex` and `current_round`, filled in for the games that report them.
- The helper behind `flag_id_flat()` (now `flag_ids()`) is public as `attackapi.flatten_flag_ids()`, for callers that
  flatten a subset of the raw structure themselves.
- `flag_id_flat()` drops `null` flag IDs instead of returning them as `None` -- a flag store with
  no ID for a round is a hole, not a value.
- `GenericAdCtfApiAsync` accepts a plain callable decoder, a `progress` hook, and an injectable
  `memory_cache`, so it can back a whole game API rather than just `attack.json`.

Features
--------

- Efficient caching between threads, processes, or containers
- Direct access from your Python exploits ([sync](./examples/basic.py) or [async](./examples/basic_async.py))
- Optional REST API for exploits in other languages (with [OpenAPI spec](./api.yaml))
- Unifies team, IP, and flag info lookup between different CTFs:
    - Supports [ENOWARS](https://enowars.com)
    - Supports [FAUST CTF](https://faustctf.net)
    - Supports [saarCTF](https://ctf.saarland)
    - Supports the [Attacking-Lab](https://attacking-lab.com) gameserver (ECSC 2026)

Quick-Start
-----------
See [examples](./examples) directory for more full scripts.

Install the package (possibly in a virtual environment):

```shell
pip install ctf-attackapi
```

Get attack infos for your python exploit:

```python
from attackapi import *

# 1. Set the API URL in code (or use CTF_API environment variable)
configure("https://scoreboard.ctf.saarland/api/attack.json")
# 2. Get attack infos!
for username in attack_info().flag_ids("servicename", "10.32.1.2"):
  pwn("10.32.1.2", username)
```

List all teams that you can attack:

```python
from attackapi import *

configure("https://scoreboard.ctf.saarland/api/attack.json")
for team in attack_info().teams:
  print(team.id, team.ip, team.name)
```

Get attack infos from REST API if you're not pwning in Python:

```shell
python -m attackapi.server --url "https://scoreboard.ctf.saarland/api/attack.json"
curl "http://localhost:14320/api/v1/teams"
```

The server has documentation on its frontpage, and here is [the OpenAPI specification](./api.yaml).

If you're not pwning in Python and dislike pip, try docker:
```shell
# edit compose.yaml and insert your CTF API URL
docker compose up -d
# visit http://localhost:14320/
```


Structure
---------

- Attack info data is retrieved and cached twice: in-memory and on disk (`/tmp` by default)
- Each request goes to the caches. If the cached data is outdated, it is refreshed in the background.
- No concurrent requests are made to the game API.
- Game-specific decoders process the game APIs data and make it accessible.
- You can query the data via python API from your exploits, or via REST API from other languages.
- Relying on the disk cache is good enough for typical exploitation scenarios.

Python Library Documentation
----------------------------
There are different ways to get an `AttackInfo` object:

```python
# 1. Functional
from attackapi import *

# Set the API URL in code (or use CTF_API environment variable)
configure("https://scoreboard.ctf.saarland/api/attack.json")
# sync:
info: AttackInfo = attack_info()
# async
info: AttackInfo = await attack_info_async()

# 2. By manually using the classes
from attackapi.sync_api import AdCtfApiSync
from attackapi.async_api import AdCtfApiAsync

api = AdCtfApiSync("https://scoreboard.ctf.saarland/api/attack.json")
info = api.attack_info()
api2 = AdCtfApiAsync("https://scoreboard.ctf.saarland/api/attack.json")
info = await api2.attack_info()
```

Optional parameters can be passed to the `configure` function or the API constructors:

- `url: str` (default: `CTF_API` environment variable)
- `tmp_directory: str | Path` (default: `/tmp` or OS-specific alternative)
- `lifetime: float` (default: 30 seconds) - after this time, cached data is invalidated and refreshed
- `timeout: float` (default: 10 seconds) - abort game API requests after this duration
- `decoder: Decoder` (default: generic decoder) - custom decoder, if your game's format is different from what we've
  seen so far
- `aiohttp_arguments: dict` - additional arguments passed to the aiohttp Session which contacts the game API

The `AttackInfo` class itself has these methods:

```python
info: AttackInfo

# Get attackable teams
print(info.teams)  # list of Team objects
print(info.teams[0].id, info.teams[0].ip, info.teams[0].name)  # Team is ID, IP, and optional name
print(info.team("10.32.1.2"))  # query Team object by ID, IP, or name

# set of service names, and a case-insensitive check for one
print(info.services)
print(info.has_service("servicename"))

# flag format and the round this info was generated for, where the game reports them
print(info.flag_regex, info.current_round)

# flag IDs for a service and team, as a string list -- the same shape whatever game you play.
# team can be ID, IP, or name.
print(info.flag_ids("servicename", "10.32.1.2"))
# => ["abc", "def"]

# One round only, for the games that report rounds. -1 is the newest published round.
print(info.flag_ids("servicename", "10.32.1.2", -1))
# => ["def"]

# the same flag IDs in the game API's own format, when you need the structure it keeps.
# Return data format is determined by game API.
print(info.flag_ids_raw("servicename", "10.32.1.2"))
# => {"227": "abc", "228": "def", ...}
```

Nothing above raises. A lookup that cannot be answered gives you `[]` (or `None` for the raw
form) and warns, so a typo in an exploit costs a line on stderr rather than the round it was in
the middle of.

Server Documentation
--------------------

```shell
# Simple usage:
python -m attackapi.server --help
```

Options:

- `--port PORT`
- `--url URL`: API url to get CTF info from.
- `--tmp-directory TMP_DIRECTORY`: Cache directory
- `--lifetime LIFETIME`: Lifetime of cached data in seconds
- `--timeout TIMEOUT`: Timeout for API calls in seconds

```shell
# Usage for higher load scenarios:
pip install ctf-attackapi[gunicorn]
gunicorn attackapi.server:create_app --bind :14320 --worker-class attackapi.server.worker.MyGunicornWebWorker --workers 4
```

Environment variables:

- `CTF_API`: URL to get CTF info from.
- `CTF_API_TMP_DIR`: Cache directory (gunicorn only)
- `CTF_API_LIFETIME`: Lifetime of cached data in seconds (gunicorn only)
- `CTF_API_TIMEOUT`: Timeout for API calls in seconds (gunicorn only)

You can also use docker to run the server:
```shell
# edit compose.yaml and insert your CTF API URL before!
docker compose up -d
```


Using attackapi for other information (scoreboard etc.)
-------------------------------------------------------
Feel free to re-use the caching layers for other information, like the current scoreboard.
The class `JsonAdCtfApiAsync` accepts arbitrary JSON endpoints:

```python
from attackapi.async_api import JsonAdCtfApiAsync

info = await JsonAdCtfApiAsync("https://scoreboard.ctf.saarland/api/scoreboard_current.json").retrieve()
```

To get parsed objects instead of raw dicts, `GenericAdCtfApiAsync` takes any `bytes -> object` callable:

```python
from attackapi.async_api import GenericAdCtfApiAsync

scoreboard = await GenericAdCtfApiAsync(
    parse_scoreboard, "https://scoreboard.ctf.saarland/api/scoreboard_round_237.json"
).retrieve()
```

Both accept `progress=`, a context-manager factory called with the URL around remote fetches (to drive a
spinner, for example), and `memory_cache=`, an own `GlobalCache` instead of the process-wide one. The
process-wide cache is keyed by URL alone, so tests that need isolation should inject their own.
