Skip to content

Models

Challenge

Bases: Model

Represents a challenge.

Attributes:

Name Type Description
author str

The author of the challenge.

category str

The category of the challenge.

description str

The description of the challenge.

difficulty str

The difficulty of the challenge.

name str

The name of the challenge.

folder_name str

The folder name for the challenge. Must follow the pattern /^[a-zA-Z][a-zA-Z0-9 _-]*$/. If not provided, it will be generated from the challenge name.

files list[Path | HttpUrl]

The list of files for the challenge.

requirements list[str]

The list of requirements needed to unlock the challenge.

extras dict[str, str | int | float | bool]

Extra information for the challenge.

flags list[Flag]

The list of flags for the challenge.

hints list[Hint]

The list of hints for the challenge.

services list[Service]

The list of services for the challenge.

Source code in src\ctf_architect\models\challenge.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
class Challenge(Model):
    """Represents a challenge.

    Attributes:
        author (str): The author of the challenge.
        category (str): The category of the challenge.
        description (str): The description of the challenge.
        difficulty (str): The difficulty of the challenge.
        name (str): The name of the challenge.
        folder_name (str, optional): The folder name for the challenge. Must follow the pattern `/^[a-zA-Z][a-zA-Z0-9 _-]*$/`. If not provided, it will be generated from the challenge name.
        files (list[Path | HttpUrl], optional): The list of files for the challenge.
        requirements (list[str], optional): The list of requirements needed to unlock the challenge.
        extras (dict[str, str | int | float | bool], optional): Extra information for the challenge.
        flags (list[Flag]): The list of flags for the challenge.
        hints (list[Hint], optional): The list of hints for the challenge.
        services (list[Service], optional): The list of services for the challenge.
    """

    author: str
    category: Annotated[str, StringConstraints(to_lower=True)]
    description: str
    difficulty: Annotated[str, StringConstraints(to_lower=True)]
    name: str
    folder_name: Annotated[
        str, StringConstraints(pattern=r"^[a-zA-Z][a-zA-Z0-9 _-]*$")
    ] = None  # type: ignore
    files: Annotated[list[HttpUrl | Path], Field(min_length=1)] | None = None
    requirements: Annotated[list[str], Field(min_length=1)] | None = None
    extras: dict[str, str | int | float | bool] | None = None
    flags: Annotated[list[Flag], Field(min_length=1)] | None = None
    hints: Annotated[list[Hint], Field(min_length=1)] | None = None
    services: Annotated[list[Service], Field(min_length=1)] | None = None

    @model_validator(mode="after")
    def _ensure_folder_name(self) -> Challenge:
        if not self.folder_name:
            sanitized = re.sub(r"^[^a-zA-Z]+|[^a-zA-Z0-9 _-]", "", self.name).strip()
            if not sanitized:
                raise ValueError(
                    f'Invalid challenge name, unable to create a valid folder name for "{self.name}"'
                )
            self.folder_name = sanitized
        return self

    @property
    def network_name(self) -> str:
        return f"{self.category}-{self.folder_name}-network".lower().replace(" ", "-")

    @property
    def repo_path(self) -> Path:
        """The path to the challenge folder relative to the repository root."""
        return Path("challenges") / self.category.lower() / self.folder_name

    @property
    def readme(self) -> str:
        """The README content for the challenge."""

        if self.extras is None:
            extras = ""
        else:
            extras = "\n".join(
                f"- **{key.capitalize()}:** {value}"
                for key, value in self.extras.items()
            )

        if self.hints is None:
            hints = "None"
        else:
            hints = "\n".join(
                f"- `{hint.content}` ({hint.cost} points)" for hint in self.hints
            )

        if self.files is None:
            files = "None"
        else:
            files = ""
            for file in self.files:
                if isinstance(file, Path):
                    files += f"- [{file.name}](<{file.as_posix()}>)\n"
                else:
                    files += f"- {file}\n"
            files = files.strip()

        if self.flags is None:
            flags = "None"
        else:
            flags = "\n".join(
                f"- `{flag.flag}` ({'regex' if flag.regex else 'static'}, {'case-insensitive' if flag.case_insensitive else 'case-sensitive'})"
                for flag in self.flags
            )

        if self.services is None:
            services = "None"
        else:
            services = "\n".join(
                f"| [`{service.name}`](<{service.path.as_posix()}>) | {service.port} | {service.type} |"
                for service in self.services
            )

        return CHALL_README_TEMPLATE.format(
            name=self.name,
            description=self.description,
            author=self.author,
            category=self.category,
            difficulty=self.difficulty,
            extras=extras,
            hints=hints,
            files=files,
            flags=flags,
            services=services,
        )

repo_path property

repo_path: Path

The path to the challenge folder relative to the repository root.

readme property

readme: str

The README content for the challenge.

ChallengeFile

Bases: Model

Represents a chall.toml config file.

Attributes:

Name Type Description
version str

The specification version.

challenge Challenge

The challenge object.

Source code in src\ctf_architect\models\challenge.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
class ChallengeFile(Model):
    """Represents a chall.toml config file.

    Attributes:
        version (str): The specification version.
        challenge (Challenge): The challenge object.
    """

    version: str
    challenge: Challenge

    @field_validator("version")
    def _validate_version(cls, value: str) -> str:
        if not is_supported_challenge_version(value):
            raise ValueError(
                f'Unsupported specification version: "{value}", current version "{CHALLENGE_SPEC_VERSION}"'
            )
        return value

    @classmethod
    def from_challenge(cls, challenge: Challenge) -> ChallengeFile:
        """Create a ChallengeFile object from a Challenge object.

        Args:
            challenge (Challenge): The Challenge object to convert.

        Returns:
            ChallengeFile: The converted ChallengeFile object.
        """
        return cls(version=str(CHALLENGE_SPEC_VERSION), challenge=challenge)

from_challenge classmethod

from_challenge(challenge: Challenge) -> ChallengeFile

Create a ChallengeFile object from a Challenge object.

Parameters:

Name Type Description Default
challenge Challenge

The Challenge object to convert.

required

Returns:

Name Type Description
ChallengeFile ChallengeFile

The converted ChallengeFile object.

Source code in src\ctf_architect\models\challenge.py
230
231
232
233
234
235
236
237
238
239
240
@classmethod
def from_challenge(cls, challenge: Challenge) -> ChallengeFile:
    """Create a ChallengeFile object from a Challenge object.

    Args:
        challenge (Challenge): The Challenge object to convert.

    Returns:
        ChallengeFile: The converted ChallengeFile object.
    """
    return cls(version=str(CHALLENGE_SPEC_VERSION), challenge=challenge)

Flag

Bases: Model

Represents a challenge flag.

Attributes:

Name Type Description
flag str

The flag string.

regex bool

Specifies whether the flag is a regular expression. Defaults to False.

case_insensitive bool

Specifies whether the flag is case-insensitive. Defaults to False.

Source code in src\ctf_architect\models\challenge.py
14
15
16
17
18
19
20
21
22
23
24
25
class Flag(Model):
    """Represents a challenge flag.

    Attributes:
        flag (str): The flag string.
        regex (bool, optional): Specifies whether the flag is a regular expression. Defaults to False.
        case_insensitive (bool, optional): Specifies whether the flag is case-insensitive. Defaults to False.
    """

    flag: str
    regex: bool = False
    case_insensitive: bool = False

Hint

Bases: Model

Represents a challenge hint.

Attributes:

Name Type Description
cost int

The cost of the hint.

content str

The content of the hint.

requirements list[int]

The list of requirements needed to unlock the hint.

Source code in src\ctf_architect\models\challenge.py
28
29
30
31
32
33
34
35
36
37
38
39
class Hint(Model):
    """Represents a challenge hint.

    Attributes:
        cost (int): The cost of the hint.
        content (str): The content of the hint.
        requirements (list[int], optional): The list of requirements needed to unlock the hint.
    """

    cost: int
    content: str
    requirements: list[int] | None = None

Service

Bases: Model

Represents a challenge service.

Attributes:

Name Type Description
name str

The name of the service.

path Path

The path to the service.

port int

The port of the service. Allowed to be unspecified if the service is internal or ports is specified.

ports list[int]

The list of ports of the service. Allowed to be unspecified if the service is internal or port is specified.

type Literal['web', 'tcp', 'ssh', 'secret', 'internal']

The type of the service.

extras dict[str, Any]

The extra information about the service to be passed to the docker compose file. Defaults to None.

Source code in src\ctf_architect\models\challenge.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class Service(Model):
    """Represents a challenge service.

    Attributes:
        name (str): The name of the service.
        path (Path): The path to the service.
        port (int, optional): The port of the service. Allowed to be unspecified if the service is internal or `ports` is specified.
        ports (list[int], optional): The list of ports of the service. Allowed to be unspecified if the service is internal or `port` is specified.
        type (Literal["web", "tcp", "ssh", "secret", "internal"]): The type of the service.
        extras (dict[str, Any], optional): The extra information about the service to be passed to the docker compose file. Defaults to None.
    """

    name: Annotated[str, StringConstraints(pattern=r"^[a-z][a-z0-9_-]*$")]
    path: Path
    port: PortInt | None = None
    ports: Annotated[list[PortInt], Field(min_length=1)] | None = None
    type: Literal["web", "tcp", "ssh", "secret", "internal"]
    extras: dict[str, Any] | None = None

    @model_validator(mode="after")
    def _validate_port(self) -> Service:
        if self.port is not None and self.ports is not None:
            raise ValueError("Port and ports cannot both be specified")

        if self.port is None and self.ports is None and self.type != "internal":
            raise ValueError(
                "Port or ports must be specified for non-internal services"
            )

        if self.ports is None:
            self.ports = [self.port]

        return self

    @property
    def ports_list(self) -> list[PortInt]:
        """
        The list of ports for the service.

        Returns:
            list[int]: The list of ports for the service.
        """
        # TODO: remove this method and use `self.ports` directly
        return self.ports or [self.port]

    def unique_name(self, challenge: Challenge) -> str:
        return (
            f"{challenge.category}-{challenge.folder_name}-{self.name}".lower().replace(
                " ", "-"
            )
        )

ports_list property

ports_list: list[PortInt]

The list of ports for the service.

Returns:

Type Description
list[PortInt]

list[int]: The list of ports for the service.

ConfigFile

Bases: Model

Represents a ctf_config.toml config file.

Attributes:

Name Type Description
version str

The specification version.

config CTFConfig

The CTF config object.

Source code in src\ctf_architect\models\ctf_config.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class ConfigFile(Model):
    """Represents a ctf_config.toml config file.

    Attributes:
        version (str): The specification version.
        config (CTFConfig): The CTF config object.
    """

    version: str
    config: CTFConfig

    @field_validator("version")
    def _validate_version(cls, value: str) -> str:
        if not is_supported_ctf_config_version(value):
            raise ValueError(
                f'Unsupported specification version: "{value}", current version "{CTF_CONFIG_SPEC_VERSION}"'
            )
        return value

    @classmethod
    def from_ctf_config(cls, config: CTFConfig) -> ConfigFile:
        """Create a ConfigFile object from a CTFConfig object.

        Args:
            config (CTFConfig): The CTFConfig object to convert.

        Returns:
            ConfigFile: The converted ConfigFile object.
        """
        return cls(version=str(CTF_CONFIG_SPEC_VERSION), config=config)

from_ctf_config classmethod

from_ctf_config(config: CTFConfig) -> ConfigFile

Create a ConfigFile object from a CTFConfig object.

Parameters:

Name Type Description Default
config CTFConfig

The CTFConfig object to convert.

required

Returns:

Name Type Description
ConfigFile ConfigFile

The converted ConfigFile object.

Source code in src\ctf_architect\models\ctf_config.py
71
72
73
74
75
76
77
78
79
80
81
@classmethod
def from_ctf_config(cls, config: CTFConfig) -> ConfigFile:
    """Create a ConfigFile object from a CTFConfig object.

    Args:
        config (CTFConfig): The CTFConfig object to convert.

    Returns:
        ConfigFile: The converted ConfigFile object.
    """
    return cls(version=str(CTF_CONFIG_SPEC_VERSION), config=config)

CTFConfig

Bases: Model

Represents the configuration for a CTF.

Attributes:

Name Type Description
categories list[str]

The list of categories for the CTF.

difficulties list[str]

The list of difficulties for the CTF.

flag_format str | None

The flag format for the CTF.

starting_port int | None

The starting port for services in the CTF.

name str

The name of the CTF.

extras list[ExtraField] | None

The list of extra fields for challenges in the CTF.

Source code in src\ctf_architect\models\ctf_config.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class CTFConfig(Model):
    """Represents the configuration for a CTF.

    Attributes:
        categories (list[str]): The list of categories for the CTF.
        difficulties (list[str]): The list of difficulties for the CTF.
        flag_format (str | None): The flag format for the CTF.
        starting_port (int | None): The starting port for services in the CTF.
        name (str): The name of the CTF.
        extras (list[ExtraField] | None): The list of extra fields for challenges in the CTF.
    """

    categories: list[str]
    difficulties: list[str]
    flag_format: str | None = None
    starting_port: int | None = None
    name: str
    extras: Annotated[list[ExtraField], Field(min_length=1)] | None = None

CheckResult

Bases: Model

Represents the result of a check.

Attributes:

Name Type Description
status CheckStatus

The status of the check.

code str

The code of the check.

level SeverityLevel

The severity level of the check.

message str

The message of the check. Defaults to None.

Source code in src\ctf_architect\models\lint.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class CheckResult(Model):
    """Represents the result of a check.

    Attributes:
        status (CheckStatus): The status of the check.
        code (str): The code of the check.
        level (SeverityLevel): The severity level of the check.
        message (str, optional): The message of the check. Defaults to None.
    """

    status: CheckStatus
    code: str
    level: SeverityLevel
    message: str | None = None

CheckStatus

Bases: StrEnum

Status of a check.

Source code in src\ctf_architect\models\lint.py
32
33
34
35
36
37
38
39
40
41
42
43
44
class CheckStatus(StrEnum):
    """Status of a check."""

    PASSED = "passed"
    """Check passed."""
    IGNORED = "ignored"
    """Check was explicitly ignored."""
    SKIPPED = "skipped"
    """Check was unable to be performed."""
    FAILED = "failed"
    """Check failed."""
    ERROR = "error"
    """Check had an unexpected error."""

PASSED class-attribute instance-attribute

PASSED = 'passed'

Check passed.

IGNORED class-attribute instance-attribute

IGNORED = 'ignored'

Check was explicitly ignored.

SKIPPED class-attribute instance-attribute

SKIPPED = 'skipped'

Check was unable to be performed.

FAILED class-attribute instance-attribute

FAILED = 'failed'

Check failed.

ERROR class-attribute instance-attribute

ERROR = 'error'

Check had an unexpected error.

LintResult

Bases: Model

Represents the result of a lint check.

Attributes:

Name Type Description
challenge_path Path

The path to the challenge.

passed list[CheckResult]

Checks that passed.

ignored list[CheckResult]

Checks that were ignored.

skipped list[CheckResult]

Checks that were skipped.

failed list[CheckResult]

Checks that failed.

errors list[CheckResult]

Checks that had errors

Source code in src\ctf_architect\models\lint.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class LintResult(Model):
    """Represents the result of a lint check.

    Attributes:
        challenge_path (Path): The path to the challenge.
        passed (list[CheckResult]): Checks that passed.
        ignored (list[CheckResult]): Checks that were ignored.
        skipped (list[CheckResult]): Checks that were skipped.
        failed (list[CheckResult]): Checks that failed.
        errors (list[CheckResult]): Checks that had errors
    """

    challenge_path: Path
    passed: list[CheckResult]
    ignored: list[CheckResult]
    skipped: list[CheckResult]
    failed: list[CheckResult]
    errors: list[CheckResult]

SeverityLevel

Bases: Enum

Severity level of a rule.

Source code in src\ctf_architect\models\lint.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@total_ordering
class SeverityLevel(Enum):
    """Severity level of a rule."""

    INFO = 0
    """Just for informational purposes"""
    WARNING = 1
    """Challenge will be able to be loaded, but may have issues"""
    ERROR = 2
    """Challenge will be unable to be loaded, but other rules can be checked"""
    FATAL = 3
    """Challenge will be unable to be loaded, non-fatal rules cannot be checked"""

    def __lt__(self, other: SeverityLevel) -> bool:
        if self.__class__ is other.__class__:
            return self.value < other.value
        return NotImplemented

INFO class-attribute instance-attribute

INFO = 0

Just for informational purposes

WARNING class-attribute instance-attribute

WARNING = 1

Challenge will be able to be loaded, but may have issues

ERROR class-attribute instance-attribute

ERROR = 2

Challenge will be unable to be loaded, but other rules can be checked

FATAL class-attribute instance-attribute

FATAL = 3

Challenge will be unable to be loaded, non-fatal rules cannot be checked

PortMapping

Bases: Model

Represents a port mapping for a service.

Attributes:

Name Type Description
from_port int

The starting port for the mapping.

to_port int | None

The ending port for the mapping.

Source code in src\ctf_architect\models\port_mapping.py
 7
 8
 9
10
11
12
13
14
15
16
class PortMapping(Model):
    """Represents a port mapping for a service.

    Attributes:
        from_port (int): The starting port for the mapping.
        to_port (int | None): The ending port for the mapping.
    """

    from_port: int
    to_port: int | None

PortMappingFile

Bases: Model

Represents a port_mapping.json file.

Attributes:

Name Type Description
version str

The specification version.

mapping dict[str, list[PortMapping]]

The port mapping object.

Source code in src\ctf_architect\models\port_mapping.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class PortMappingFile(Model):
    """Represents a port_mapping.json file.

    Attributes:
        version (str): The specification version.
        mapping (dict[str, list[PortMapping]]): The port mapping object.
    """

    version: str
    mapping: dict[str, list[PortMapping]]

    @classmethod
    def from_mapping(cls, mapping: dict[str, list[PortMapping]]) -> PortMappingFile:
        """Create a PortMappingFile object from a port mapping object.

        Args:
            mapping (dict[str, list[PortMapping]]): The port mapping object to convert.

        Returns:
            PortMappingFile: The converted PortMappingFile object.
        """
        return cls(version=str(PORT_MAPPING_SPEC_VERSION), mapping=mapping)

from_mapping classmethod

from_mapping(
    mapping: dict[str, list[PortMapping]]
) -> PortMappingFile

Create a PortMappingFile object from a port mapping object.

Parameters:

Name Type Description Default
mapping dict[str, list[PortMapping]]

The port mapping object to convert.

required

Returns:

Name Type Description
PortMappingFile PortMappingFile

The converted PortMappingFile object.

Source code in src\ctf_architect\models\port_mapping.py
30
31
32
33
34
35
36
37
38
39
40
@classmethod
def from_mapping(cls, mapping: dict[str, list[PortMapping]]) -> PortMappingFile:
    """Create a PortMappingFile object from a port mapping object.

    Args:
        mapping (dict[str, list[PortMapping]]): The port mapping object to convert.

    Returns:
        PortMappingFile: The converted PortMappingFile object.
    """
    return cls(version=str(PORT_MAPPING_SPEC_VERSION), mapping=mapping)