From 79b3461e47aaf530099ca9940956ef4b54f9a05e Mon Sep 17 00:00:00 2001 From: ahenao Date: Tue, 11 Nov 2025 12:23:28 -0500 Subject: [PATCH 1/3] Make any combination of manual or potential links result in successful operation --- app/flags/operations/flag_3.py | 40 ++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/app/flags/operations/flag_3.py b/app/flags/operations/flag_3.py index e7a213c..48beeca 100644 --- a/app/flags/operations/flag_3.py +++ b/app/flags/operations/flag_3.py @@ -1,8 +1,8 @@ +import logging from plugins.training.app.c_flag import Flag - class OperationsFlag3(Flag): - name = 'Empty operation' + name = 'Empty Op' challenge = ( 'Run and finish an operation without selecting any agent groups or adversary profiles. Add at least 5 manual ' @@ -10,12 +10,38 @@ class OperationsFlag3(Flag): ) extra_info = ( - 'During an autonomous adversary emulation exercise, the operation will only run tasks in the adversary profile. ' - 'Manual and potential links allow an operator to "toss in" additional TTPs into a live, autonomous operation.' + 'During an autonomous adversary emulation exercise, the operation will only run tasks in the adversary profile. ' + 'Manual and potential links allow an operator to "toss in" additional TTPs into a live, autonomous operation.' ) async def verify(self, services): - for op in await services.get('data_svc').locate('operations'): - if op.finish and op.adversary.adversary_id == 'ad-hoc' and len(op.chain) >= 5 and not op.group: + data_svc = services.get('data_svc') + if not data_svc: + logging.error("[training.flag3] data_svc is None!") + return False + + operations = await data_svc.locate('operations') + for op in operations: + logging.info(f"[training.flag3] Checking operation '{op.name}' | state={op.state}, group={op.group}, adversary_id={getattr(op.adversary, 'adversary_id', None)}") + + # Gather all link lists that might exist + chain_links = getattr(op, 'chain', []) or [] + potential_links = getattr(op, 'potential_links', []) or [] + manual_links = getattr(op, 'links', []) or [] + + total_links = len(chain_links) + len(potential_links) + len(manual_links) + + logging.info(f"[training.flag3] Found {len(chain_links)} chain links, {len(potential_links)} potential links, {len(manual_links)} manual links (total={total_links})") + + # Verify completion conditions + if ( + op.finish + and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc' + and total_links >= 5 + and not op.group + ): + logging.info(f"[training.flag3] ✅ Operation '{op.name}' meets all criteria!") return True - return False + + logging.info("[training.flag3] ❌ No operation met the criteria.") + return False \ No newline at end of file From 80a46ddad7d09f448c6764dfbd6dee2af705aa54 Mon Sep 17 00:00:00 2001 From: deacon Date: Mon, 16 Mar 2026 00:29:48 -0400 Subject: [PATCH 2/3] fix: indentation, selective flag evaluation, localStorage key not full object - Fix indentation of comment block in retrieve_flags so it sits inside the try block rather than at column 0 (avoids misleading dead-code look) - Store only selectedBadge.name in localStorage instead of the full badge object; restore as a minimal {name} stub so existing .name accesses work --- gui/views/training.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gui/views/training.vue b/gui/views/training.vue index eef9805..7dc9063 100644 --- a/gui/views/training.vue +++ b/gui/views/training.vue @@ -122,7 +122,9 @@ const restoreSelections = () => { try { const parsed = JSON.parse(savedState); selectedCert.value = parsed.selectedCert || ""; - selectedBadge.value = parsed.selectedBadge || ""; + selectedBadge.value = parsed.selectedBadgeName + ? { name: parsed.selectedBadgeName } + : ""; } catch (err) { console.warn("Failed to parse saved training state:", err); } @@ -134,7 +136,7 @@ const persistSelections = () => { if (!selectedCert.value) return; const state = { selectedCert: selectedCert.value, - selectedBadge: selectedBadge.value, + selectedBadgeName: selectedBadge.value?.name || "", }; localStorage.setItem("trainingState", JSON.stringify(state)); }; From c6da2c02e08b3fa60025660c1a850ebbffa171db Mon Sep 17 00:00:00 2001 From: deacon Date: Wed, 18 Mar 2026 09:11:15 -0400 Subject: [PATCH 3/3] fix: address Copilot review feedback - flag_3.py: add module logger, use log.debug instead of logging.info, two-step getattr for adversary, two blank lines before class, remove emoji from log messages, revert flag name to 'Empty operation' - training.vue: wrap all localStorage accesses in try/catch, add schema migration to support old selectedBadge key alongside new selectedBadgeName --- app/flags/operations/flag_3.py | 24 ++++++++++++++++-------- gui/views/training.vue | 19 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/app/flags/operations/flag_3.py b/app/flags/operations/flag_3.py index 48beeca..cd1e98b 100644 --- a/app/flags/operations/flag_3.py +++ b/app/flags/operations/flag_3.py @@ -1,8 +1,12 @@ import logging + from plugins.training.app.c_flag import Flag +log = logging.getLogger(__name__) + + class OperationsFlag3(Flag): - name = 'Empty Op' + name = 'Empty operation' challenge = ( 'Run and finish an operation without selecting any agent groups or adversary profiles. Add at least 5 manual ' @@ -17,12 +21,15 @@ class OperationsFlag3(Flag): async def verify(self, services): data_svc = services.get('data_svc') if not data_svc: - logging.error("[training.flag3] data_svc is None!") + log.debug("[training.flag3] data_svc is None!") return False operations = await data_svc.locate('operations') for op in operations: - logging.info(f"[training.flag3] Checking operation '{op.name}' | state={op.state}, group={op.group}, adversary_id={getattr(op.adversary, 'adversary_id', None)}") + adversary = getattr(op, 'adversary', None) + adversary_id = getattr(adversary, 'adversary_id', None) + log.debug("[training.flag3] Checking operation '%s' | state=%s, group=%s, adversary_id=%s", + op.name, op.state, op.group, adversary_id) # Gather all link lists that might exist chain_links = getattr(op, 'chain', []) or [] @@ -31,17 +38,18 @@ async def verify(self, services): total_links = len(chain_links) + len(potential_links) + len(manual_links) - logging.info(f"[training.flag3] Found {len(chain_links)} chain links, {len(potential_links)} potential links, {len(manual_links)} manual links (total={total_links})") + log.debug("[training.flag3] Found %d chain links, %d potential links, %d manual links (total=%d)", + len(chain_links), len(potential_links), len(manual_links), total_links) # Verify completion conditions if ( op.finish - and getattr(op.adversary, 'adversary_id', None) == 'ad-hoc' + and adversary_id == 'ad-hoc' and total_links >= 5 and not op.group ): - logging.info(f"[training.flag3] ✅ Operation '{op.name}' meets all criteria!") + log.debug("[training.flag3] Operation '%s' meets all criteria!", op.name) return True - logging.info("[training.flag3] ❌ No operation met the criteria.") - return False \ No newline at end of file + log.debug("[training.flag3] No operation met the criteria.") + return False diff --git a/gui/views/training.vue b/gui/views/training.vue index 7dc9063..eb653ce 100644 --- a/gui/views/training.vue +++ b/gui/views/training.vue @@ -17,7 +17,9 @@ const certificates = ref([ { name: "Blue Certificate" }, ]); -const learnerName = ref(localStorage.getItem('trainingCertName') || ''); +let _savedCertName = ''; +try { _savedCertName = localStorage.getItem('trainingCertName') || ''; } catch (e) { /* storage unavailable */ } +const learnerName = ref(_savedCertName); const showIssueModal = ref(false); const skipSelectionWatcher = ref(false); @@ -67,7 +69,9 @@ watch( function saveNameLocally() { const n = learnerName.value.trim(); - if (n) localStorage.setItem('trainingCertName', n); + if (n) { + try { localStorage.setItem('trainingCertName', n); } catch (e) { /* storage unavailable */ } + } } async function issueCertificate() { @@ -117,14 +121,15 @@ async function issueCertificate() { } const restoreSelections = () => { - const savedState = localStorage.getItem("trainingState"); + let savedState = null; + try { savedState = localStorage.getItem("trainingState"); } catch (e) { /* storage unavailable */ } if (savedState) { try { const parsed = JSON.parse(savedState); selectedCert.value = parsed.selectedCert || ""; - selectedBadge.value = parsed.selectedBadgeName - ? { name: parsed.selectedBadgeName } - : ""; + // Support both new key (selectedBadgeName) and old key (selectedBadge) for migration + const badgeName = parsed.selectedBadgeName || parsed.selectedBadge?.name || ""; + selectedBadge.value = badgeName ? { name: badgeName } : null; } catch (err) { console.warn("Failed to parse saved training state:", err); } @@ -138,7 +143,7 @@ const persistSelections = () => { selectedCert: selectedCert.value, selectedBadgeName: selectedBadge.value?.name || "", }; - localStorage.setItem("trainingState", JSON.stringify(state)); + try { localStorage.setItem("trainingState", JSON.stringify(state)); } catch (e) { /* storage unavailable */ } }; const getTraining = () => {