From 1f179e6c91dab3635869f0941c3006e981a10493 Mon Sep 17 00:00:00 2001 From: Yu-Fu Fu Date: Tue, 19 Nov 2024 16:58:14 -0500 Subject: [PATCH 1/3] Handle challenge attribution and its related tests --- ctfcli/core/challenge.py | 12 ++++---- tests/core/test_challenge.py | 30 ++++++++++++++++--- .../test-challenge-dockerfile/challenge.yml | 3 +- .../test-challenge-files/challenge.yml | 3 +- .../test-challenge-full/challenge.yml | 1 + .../challenge.yml | 3 +- .../test-challenge-minimal/challenge.yml | 3 +- 7 files changed, 42 insertions(+), 13 deletions(-) diff --git a/ctfcli/core/challenge.py b/ctfcli/core/challenge.py index 100fc8c..eb1cee5 100644 --- a/ctfcli/core/challenge.py +++ b/ctfcli/core/challenge.py @@ -43,7 +43,7 @@ def str_presenter(dumper, data): class Challenge(dict): key_order = [ # fmt: off - "name", "author", "category", "description", "value", + "name", "author", "category", "description", "attribution", "value", "type", "extra", "image", "protocol", "host", "connection_info", "healthcheck", "attempts", "flags", "files", "topics", "tags", "files", "hints", @@ -262,6 +262,7 @@ def _get_initial_challenge_payload(self, ignore: Tuple[str] = ()) -> Dict: "name": self["name"], "category": self.get("category", ""), "description": self.get("description", ""), + "attribution": self.get("attribution", ""), "type": self.get("type", "standard"), # Hide the challenge for the duration of the sync / creation "state": "hidden", @@ -459,12 +460,13 @@ def normalize_requirements(requirements): def _normalize_challenge(self, challenge_data: Dict[str, Any]): challenge = {} - copy_keys = ["name", "category", "value", "type", "state", "connection_info"] + copy_keys = ["name", "category", "attribution", "value", "type", "state", "connection_info"] for key in copy_keys: if key in challenge_data: challenge[key] = challenge_data[key] challenge["description"] = challenge_data["description"].strip().replace("\r\n", "\n").replace("\t", "") + challenge["attribution"] = challenge_data["attribution"].strip().replace("\r\n", "\n").replace("\t", "") challenge["attempts"] = challenge_data["max_attempts"] for key in ["initial", "decay", "minimum"]: @@ -556,7 +558,7 @@ def sync(self, ignore: Tuple[str] = ()) -> None: remote_challenge = self.load_installed_challenge(self.challenge_id) # if value, category, type or description are ignored, revert them to the remote state in the initial payload - reset_properties_if_ignored = ["value", "category", "type", "description"] + reset_properties_if_ignored = ["value", "category", "type", "description", "attribution"] for p in reset_properties_if_ignored: if p in ignore: challenge_payload[p] = remote_challenge[p] @@ -670,7 +672,7 @@ def create(self, ignore: Tuple[str] = ()) -> None: # value is required (unless the challenge is a dynamic value challenge), # and the type will default to standard # if category or description are ignored, set them to an empty string - reset_properties_if_ignored = ["category", "description"] + reset_properties_if_ignored = ["category", "description", "attribution"] for p in reset_properties_if_ignored: if p in ignore: challenge_payload[p] = "" @@ -716,7 +718,7 @@ def lint(self, skip_hadolint=False, flag_format="flag{") -> bool: issues = {"fields": [], "dockerfile": [], "hadolint": [], "files": []} # Check if required fields are present - for field in ["name", "author", "category", "description", "value"]: + for field in ["name", "author", "category", "description", "attribution", "value"]: # value is allowed to be none if the challenge type is dynamic if field == "value" and challenge.get("type") == "dynamic": continue diff --git a/tests/core/test_challenge.py b/tests/core/test_challenge.py index 8c1f4fc..261e3df 100644 --- a/tests/core/test_challenge.py +++ b/tests/core/test_challenge.py @@ -197,6 +197,7 @@ def test_updates_simple_properties(self, mock_api_constructor: MagicMock, *args, "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -239,6 +240,7 @@ def test_updates_attempts(self, mock_api_constructor: MagicMock, *args, **kwargs "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -284,6 +286,7 @@ def test_updates_extra_properties(self, mock_api_constructor: MagicMock, *args, "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "value": 150, "state": "hidden", "type": "application_target", @@ -342,6 +345,7 @@ def test_updates_flags(self, mock_api_constructor: MagicMock, *args, **kwargs): "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -430,6 +434,7 @@ def test_updates_topics(self, mock_api_constructor: MagicMock, *args, **kwargs): "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -490,6 +495,7 @@ def test_updates_tags(self, mock_api_constructor: MagicMock, *args, **kwargs): "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -553,6 +559,7 @@ def test_updates_files(self, mock_api_constructor: MagicMock, *args, **kwargs): "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -654,6 +661,7 @@ def test_updates_hints(self, mock_api_constructor: MagicMock, *args, **kwargs): "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -714,6 +722,7 @@ def test_updates_requirements(self, mock_api_constructor: MagicMock, *args, **kw "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -756,6 +765,7 @@ def test_challenge_cannot_require_itself( "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -816,6 +826,7 @@ def test_defaults_to_standard_challenge_type(self, mock_api_constructor: MagicMo "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -854,6 +865,7 @@ def test_defaults_to_visible_state(self, mock_api_constructor: MagicMock, *args, "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "max_attempts": 0, @@ -904,6 +916,7 @@ def test_does_not_update_dynamic_value(self, mock_api_constructor: MagicMock, *a "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "dynamic", "state": "hidden", "max_attempts": 0, @@ -961,6 +974,7 @@ def test_updates_multiple_attributes_at_once(self, mock_api_constructor: MagicMo "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -1018,7 +1032,7 @@ def test_does_not_update_ignored_attributes(self): properties = [ # fmt: off # simple types - "category", "description", "type", "value", "attempts", "connection_info", "state", + "category", "description", "attribution", "type", "value", "attempts", "connection_info", "state", # complex types "extra", "flags", "topics", "tags", "files", "hints", "requirements", # fmt: on @@ -1028,6 +1042,7 @@ def test_does_not_update_ignored_attributes(self): "name": "Test Challenge", "category": "Old Category", "description": "Old Description", + "attribution": "Old Attribution", "type": "some-custom-type", "value": 100, "state": "visible", @@ -1057,6 +1072,7 @@ def test_does_not_update_ignored_attributes(self): "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -1072,7 +1088,7 @@ def test_does_not_update_ignored_attributes(self): expected_challenge_payload["value"] = remote_installed_challenge["value"] challenge["value"] = 200 - if p in ["category", "description", "type"]: + if p in ["category", "description", "attribution", "type"]: expected_challenge_payload[p] = remote_installed_challenge[p] challenge[p] = "new-value" @@ -1154,6 +1170,7 @@ def test_creates_standard_challenge(self, mock_api_constructor: MagicMock, *args "name": "Test Challenge", "category": "Test", "description": "Test Description", + "attribution": "Test Attribution", "value": 150, "max_attempts": 5, "type": "standard", @@ -1244,7 +1261,7 @@ def test_exits_if_files_do_not_exist(self, mock_api_constructor: MagicMock, *arg def test_does_not_set_ignored_attributes(self): # fmt:off properties = [ - "value", "category", "description", "attempts", "connection_info", "state", # simple types + "value", "category", "description", "attribution", "attempts", "connection_info", "state", # simple types "extra", "flags", "topics", "tags", "files", "hints", "requirements" # complex types ] # fmt:on @@ -1262,6 +1279,7 @@ def test_does_not_set_ignored_attributes(self): "name": "Test Challenge", "category": "New Test", "description": "New Test Description", + "attribution": "New Test Attribution", "type": "standard", "value": 150, "state": "hidden", @@ -1282,7 +1300,7 @@ def test_does_not_set_ignored_attributes(self): expected_challenge_payload[p] = "custom-type" # expect these to be in the payload, with the defaults or empty: - if p in ["category", "description"]: + if p in ["category", "description", "attribution"]: challenge[p] = "new-value" expected_challenge_payload[p] = "" @@ -1520,6 +1538,7 @@ def mock_get(self, *args, **kwargs): "name": "Test Challenge", "value": 150, "description": "Test Description", + "attribution": "Test Attribution", "connection_info": "https://example.com", "next_id": None, "category": "Test", @@ -1681,6 +1700,7 @@ def test_normalize_fetches_and_normalizes_challenge(self, mock_api_constructor: "name": "Test Challenge", "category": "Test", "description": "Test Description", + "attribution": "Test Attribution", "value": 150, "max_attempts": 5, "type": "standard", @@ -1703,6 +1723,7 @@ def test_normalize_fetches_and_normalizes_challenge(self, mock_api_constructor: "state": "hidden", "connection_info": "https://example.com", "description": "Test Description", + "attribution": "Test Attribution", "attempts": 5, "flags": [ "flag{test-flag}", @@ -1755,6 +1776,7 @@ def test_mirror_challenge(self, mock_api_constructor: MagicMock): { "value": 200, "description": "other description", + "attribution": "other attribution", "connection_info": "https://other.example.com", "flags": ["flag{other-flag}", "other-flag"], "topics": ["other-topic-1", "other-topic-2"], diff --git a/tests/fixtures/challenges/test-challenge-dockerfile/challenge.yml b/tests/fixtures/challenges/test-challenge-dockerfile/challenge.yml index 4ef74d0..32fbf9e 100644 --- a/tests/fixtures/challenges/test-challenge-dockerfile/challenge.yml +++ b/tests/fixtures/challenges/test-challenge-dockerfile/challenge.yml @@ -1,9 +1,10 @@ name: Test Challenge category: New Test description: New Test Description +attribution: New Test Attribution value: 150 author: Test type: standard state: hidden image: . -protocol: http \ No newline at end of file +protocol: http diff --git a/tests/fixtures/challenges/test-challenge-files/challenge.yml b/tests/fixtures/challenges/test-challenge-files/challenge.yml index bd692d0..a562988 100644 --- a/tests/fixtures/challenges/test-challenge-files/challenge.yml +++ b/tests/fixtures/challenges/test-challenge-files/challenge.yml @@ -1,6 +1,7 @@ name: Test Challenge category: New Test description: New Test Description +attribution: New Test Attribution value: 150 author: Test type: standard @@ -8,4 +9,4 @@ state: hidden files: - files/test.png - - files/test.pdf \ No newline at end of file + - files/test.pdf diff --git a/tests/fixtures/challenges/test-challenge-full/challenge.yml b/tests/fixtures/challenges/test-challenge-full/challenge.yml index 2654206..c1bc392 100644 --- a/tests/fixtures/challenges/test-challenge-full/challenge.yml +++ b/tests/fixtures/challenges/test-challenge-full/challenge.yml @@ -1,6 +1,7 @@ name: Test Challenge category: Test description: Test Description +attribution: Test Attribution value: 150 author: Test type: standard diff --git a/tests/fixtures/challenges/test-challenge-invalid-dockerfile/challenge.yml b/tests/fixtures/challenges/test-challenge-invalid-dockerfile/challenge.yml index d49ce66..69db168 100644 --- a/tests/fixtures/challenges/test-challenge-invalid-dockerfile/challenge.yml +++ b/tests/fixtures/challenges/test-challenge-invalid-dockerfile/challenge.yml @@ -1,8 +1,9 @@ name: Test Challenge category: New Test description: New Test Description +attribution: New Test Attribution value: 150 author: Test type: standard state: hidden -image: . \ No newline at end of file +image: . diff --git a/tests/fixtures/challenges/test-challenge-minimal/challenge.yml b/tests/fixtures/challenges/test-challenge-minimal/challenge.yml index 00ff4bb..6d5079a 100644 --- a/tests/fixtures/challenges/test-challenge-minimal/challenge.yml +++ b/tests/fixtures/challenges/test-challenge-minimal/challenge.yml @@ -1,7 +1,8 @@ name: Test Challenge category: New Test description: New Test Description +attribution: New Test Attribution value: 150 author: Test type: standard -state: hidden \ No newline at end of file +state: hidden From d3562ab4b5eaf2471fd86b71dcc9552cc5643d32 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Mon, 25 Nov 2024 19:56:46 -0500 Subject: [PATCH 2/3] Update upload-artifact to v3 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fce6723..bfe617c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: - name: Build package run: poetry build - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: path: | ./dist/*.tar.gz From f5915c65a10a56e1031c83bb98aa9a0afaab1c51 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Mon, 25 Nov 2024 19:58:49 -0500 Subject: [PATCH 3/3] Safely access attribution in _normalize_challenge() --- ctfcli/core/challenge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctfcli/core/challenge.py b/ctfcli/core/challenge.py index eb1cee5..8bb4c6e 100644 --- a/ctfcli/core/challenge.py +++ b/ctfcli/core/challenge.py @@ -466,7 +466,7 @@ def _normalize_challenge(self, challenge_data: Dict[str, Any]): challenge[key] = challenge_data[key] challenge["description"] = challenge_data["description"].strip().replace("\r\n", "\n").replace("\t", "") - challenge["attribution"] = challenge_data["attribution"].strip().replace("\r\n", "\n").replace("\t", "") + challenge["attribution"] = challenge_data.get("attribution", "").strip().replace("\r\n", "\n").replace("\t", "") challenge["attempts"] = challenge_data["max_attempts"] for key in ["initial", "decay", "minimum"]: