Pluginstuff#84
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces UI and client-side plumbing for plugin enable/disable workflows that trigger a Caldera restart (with a global “rebuild/reconnect” overlay), and expands the “Adversaries” area into an “Automation” editor that supports importing profiles and editing per-step executor facts.
Changes:
- Added Vite dev-server proxying for auth/core API/plugin routes and configurable backend URL.
- Implemented plugin enable/disable restart flow: axios interceptor handling, global restart overlay/polling, and modal/navigation updates.
- Reworked adversary/automation editing to support importing profiles, editing ability instances, and executor-facts normalization utilities.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.js | Adds env-driven backend URL, dev proxy rules, and server config for local development. |
| src/views/LoginView.vue | Adds form submit handler and browser autocomplete hints for credentials. |
| src/views/AdversariesView.vue.bkp | Backup copy of prior adversaries view (added in PR). |
| src/views/AdversariesView.vue | Reworks view into “Automation” editor with import + per-step ability editing modal. |
| src/views/AbilitiesView.vue | Adjusts selected ability initialization and modal props/flags for editor reuse. |
| src/utils/executorUtils.js | Adds utilities to rebuild/normalize executor facts for load/save paths. |
| src/stores/coreStore.js | Changes default plugin visibility setting and adjusts available-plugins error handling. |
| src/stores/coreDisplayStore.js | Adds restart flag and expands core plugin-modal UI state (mode, selections). |
| src/stores/authStore.js | Updates auth status tracking to set isUserAuthenticated on success/failure. |
| src/stores/adversaryStore.js.bkp | Backup copy of prior adversary store (added in PR). |
| src/stores/adversaryStore.js | Adds richer adversary load/save logic, step UUIDs, and executor-facts normalization. |
| src/main.js | Adds axios response interceptor and adds spinner icon for restart overlay UX. |
| src/components/operations/CreateModal.vue | Refactors operation creation logic (payload builder + jitter normalization). |
| src/components/core/PluginModal.vue | Updates modal to support enable + multi-disable flows and restart timing. |
| src/components/core/Navigation.vue | Adds “Disable a Plugin” entry and wires modal state for enable/disable flows. |
| src/components/adversaries/ImportModal.vue.bkp | Backup copy of prior YAML import modal (added in PR). |
| src/components/adversaries/ImportModal.vue | Replaces custom YAML parsing with js-yaml + emits imported profile to parent. |
| src/components/adversaries/FactBreakdownModal.vue | Replaces tag-based breakdown with detailed usage tables tied to fact sources. |
| src/components/adversaries/DetailsTable.vue.bkp | Backup copy of prior details table (added in PR). |
| src/components/adversaries/DetailsTable.vue | Major refactor: local abilities state, DnD ordering, fact source dropdown, warning icons. |
| src/components/adversaries/DeleteAdversaryConfirmationModal.vue.bkp | Backup copy of prior delete confirmation modal (added in PR). |
| src/components/adversaries/DeleteAdversaryConfirmationModal.vue | Updates store import paths to alias-based imports. |
| src/components/abilities/CreateEditAbility.vue.bkp | Backup copy of prior ability editor (added in PR). |
| src/components/abilities/CreateEditAbility.vue | Expands editor to support adversary-step instances + trait/fact selection UI. |
| src/App.vue | Adds global restart overlay with delayed health polling + reconnect option; moves data refresh to route watcher. |
| package-lock.json | Lockfile updates reflecting dependency metadata/structure changes. |
Comments suppressed due to low confidence (1)
src/stores/coreDisplayStore.js:12
modals.automationImportis used elsewhere, but the initialmodalsstate doesn’t define it. Add an explicitautomationImport: falseflag undermodalsto keep modal state shape consistent.
modals: {
agents: {
showDeploy: false,
showConfig: false,
showDetails: false,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'row-hover-below': ability.step_uuid === tableDragHoverId.value && tableDragHoverId.value != undefined && tableDragEndIndex.value > tableDragTargetIndex.value, | ||
| 'orange-row': needsParser.value.indexOf(ability.name) > -1, | ||
| 'row-hover': ability.step_uuid === tableDragHoverId.value && tableDragHoverId.value != undefined, | ||
| 'red-row-unclickable': undefinedAbilities.value.indexOf(ability.step_uuid) > -1 |
There was a problem hiding this comment.
getRowClass() checks undefinedAbilities against ability.step_uuid, but undefinedAbilities is populated with ability_id values. Align these so the undefined-ability styling can actually trigger.
| 'red-row-unclickable': undefinedAbilities.value.indexOf(ability.step_uuid) > -1 | |
| 'red-row-unclickable': undefinedAbilities.value.indexOf(ability.ability_id) > -1 |
| this.modals.core.selectedPlugin = pluginName; | ||
| this.modals.core.showPluginPopup = true; | ||
| if (!modals.value?.core) return; | ||
|
|
There was a problem hiding this comment.
promptToEnablePlugin() should explicitly set modals.value.core.mode = 'enable' before opening the modal. Otherwise, after using the disable flow, the modal can reopen in “disable” mode when prompting to enable a plugin.
| modals.value.core.mode = "enable"; |
| const { agents } = storeToRefs(agentStore); | ||
| const { abilities } = storeToRefs(abilityStore); |
There was a problem hiding this comment.
agents and abilities refs are created but never used in this view. Removing them avoids lint warnings and reduces reactive overhead.
| const { agents } = storeToRefs(agentStore); | |
| const { abilities } = storeToRefs(abilityStore); |
| button.delete(@click.stop="deleteAbility(index)") | ||
|
|
||
| .container.has-text-centered( | ||
| v-if="abilitiesReady && Array.isArray(localAbilities.value) && !localAbilities.value.length" |
There was a problem hiding this comment.
In the template, localAbilities is a ref but Vue templates auto-unwrap refs. Using localAbilities.value inside expressions (e.g. Array.isArray(localAbilities.value)) will be undefined and the empty-state block will never render. Use localAbilities (no .value) in template expressions.
| v-if="abilitiesReady && Array.isArray(localAbilities.value) && !localAbilities.value.length" | |
| v-if="abilitiesReady && Array.isArray(localAbilities) && !localAbilities.length" |
| }); | ||
| } | ||
| } else { | ||
| undefinedAbilities.value.push(ability.ability_id); |
There was a problem hiding this comment.
undefinedAbilities is populated with ability.ability_id (and in the else branch that value will be undefined), but later row styling checks against ability.step_uuid. Track a consistent identifier for “undefined ability” (either push step_uuid or check against ability_id).
| undefinedAbilities.value.push(ability.ability_id); | |
| undefinedAbilities.value.push(ability.step_uuid); |
| const router = useRouter(); | ||
|
|
||
| const adversaryStore = useAdversaryStore(); | ||
| const { adversaries, selectedAdversary } = storeToRefs(adversaryStore); | ||
| const coreStore = useCoreStore(); |
There was a problem hiding this comment.
router and coreStore are instantiated but never used in this view. If linting is enforced, this will fail CI. Remove unused store/router setup (or use them).
| @@ -1,5 +1,5 @@ | |||
| <script setup> | |||
| import { inject } from "vue"; | |||
| import { inject, onMounted, watch, ref, computed } from "vue"; | |||
There was a problem hiding this comment.
onMounted, watch, ref, and computed are imported but not used in this component. If linting is enforced, this will fail CI. Remove unused imports.
| import { inject, onMounted, watch, ref, computed } from "vue"; | |
| import { inject } from "vue"; |
| ) | ||
| span.icon.is-small.is-left | ||
| font-awesome-icon(icon="fas fa-lock") | ||
| button.button.fancy-button.is-fullwidth(type="submit" @click="handleLogin") Log In |
There was a problem hiding this comment.
This button submits the form (type="submit") and also calls handleLogin on click while the form already has @submit.prevent. That can trigger the login flow twice. Remove the @click handler and rely on the form submit handler only.
| button.button.fancy-button.is-fullwidth(type="submit" @click="handleLogin") Log In | |
| button.button.fancy-button.is-fullwidth(type="submit") Log In |
| // Ensure abilities are available | ||
| if (!abilityStoreInstance.abilities.length) { | ||
| await abilityStoreInstance.getAbilities($api); | ||
|
|
||
| } | ||
|
|
There was a problem hiding this comment.
loadAdversaryFromProfile() calls abilityStoreInstance.getAbilities($api), but $api isn’t defined in this store’s scope. Pass $api into this action (and update callers) or remove the implicit fetch and require the caller to preload abilities.
| // Ensure abilities are available | |
| if (!abilityStoreInstance.abilities.length) { | |
| await abilityStoreInstance.getAbilities($api); | |
| } |
| */ | ||
| function handleClickOutside(event) { | ||
| for (const [index, executor] of (abilityToEdit.value.executors || []).entries()) { | ||
| const key = getExecutorKey(executor, index); |
There was a problem hiding this comment.
Superfluous argument passed to function getExecutorKey.
| const key = getExecutorKey(executor, index); | |
| const key = getExecutorKey(executor); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 28 changed files in this pull request and generated 20 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -23,6 +23,9 @@ export const useCoreDisplayStore = defineStore("coreDisplayStore", { | |||
| core: { | |||
| showPluginPopup: false, | |||
| selectedPlugin: "", | |||
| mode: "enable", | |||
| selectedPlugins: [], | |||
| availableToDisable: [] | |||
| }, | |||
There was a problem hiding this comment.
modals.automationImport is used in AdversariesView.vue, but this store’s initial state doesn’t define it. Please add an explicit automationImport flag under modals (or nest it under modals.adversaries) so the modal state is declared and survives store resets.
| import { useCoreDisplayStore } from "../../stores/coreDisplayStore"; | ||
| import { useCoreStore } from "../../stores/coreStore"; | ||
|
|
||
| const $api = inject("$api"); | ||
| const coreStore = useCoreStore(); | ||
|
|
||
| const coreDisplayStore = useCoreDisplayStore(); | ||
| const { modals } = storeToRefs(coreDisplayStore); | ||
|
|
There was a problem hiding this comment.
coreStore is imported/instantiated but never used in this component. Please remove the unused import/variable to avoid lint failures and keep the module clean.
| export function buildExecutorsFromFacts(originalExecutors, executorFactsMap = {}) { | ||
| if (!Array.isArray(originalExecutors)) return []; | ||
|
|
||
| return originalExecutors.map((executor) => { | ||
| const platformFacts = executorFactsMap?.[executor.platform] || []; | ||
|
|
||
| return { | ||
| ...cloneDeep(executor), | ||
| executor_facts: cloneDeep(platformFacts), | ||
| payloads: Array.isArray(executor.payloads) ? executor.payloads : [], | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Normalizes unstructured executor_facts using ability executor info | ||
| * | ||
| * @param {Object} step - Adversary step with optional executor_facts | ||
| * @param {Array} abilityExecutors - Ability.executors list to match | ||
| * @returns {Object} Step with normalized executor_facts | ||
| */ | ||
| export function normalizeStepExecutorFacts(step, abilityExecutors) { | ||
| const executorFacts = step?.metadata?.executor_facts; | ||
| if (!executorFacts || typeof executorFacts !== "object") return step; | ||
|
|
||
| return { | ||
| ...step, | ||
| metadata: { | ||
| ...step.metadata, | ||
| executor_facts: executorFacts, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| // utils/executorUtils.js | ||
| export function normalizeExecutorFactsForSave(executorFacts = {}) { | ||
| const normalized = {}; | ||
|
|
||
| for (const [key, facts] of Object.entries(executorFacts)) { | ||
| if (!Array.isArray(facts) || facts.length === 0) continue; | ||
|
|
||
| // UI keys supported: | ||
| // - uuid::linux | ||
| // - linux_sh_0 | ||
| // - linux (already backend style) | ||
| let platform = key; | ||
|
|
||
| if (key.includes("::")) { | ||
| platform = key.split("::").pop(); // uuid::linux -> linux | ||
| } else if (key.includes("_")) { | ||
| platform = key.split("_")[0]; // linux_sh_0 -> linux | ||
| } | ||
|
|
||
| normalized[platform] ??= []; | ||
| normalized[platform].push( | ||
| ...facts.map(({ trait, value }) => ({ trait, value })) | ||
| ); | ||
| } | ||
|
|
||
| return normalized; | ||
| } |
There was a problem hiding this comment.
New executorUtils functions don’t have unit tests. The repo already has src/tests/utils.test.js; please add coverage for normalizeExecutorFactsForSave (key parsing/merging) and buildExecutorsFromFacts (fact injection, payload defaulting) to prevent regressions.
| <script setup> | ||
| import { ref, inject } from "vue"; | ||
| import { useCoreDisplayStore } from "../../stores/coreDisplayStore"; | ||
| import { storeToRefs } from "pinia"; | ||
| import { useAdversaryStore } from "../../stores/adversaryStore"; | ||
|
|
||
| const $api = inject("$api"); | ||
|
|
||
| const adversaryStore = useAdversaryStore(); | ||
| const coreDisplayStore = useCoreDisplayStore(); | ||
| const { modals } = storeToRefs(coreDisplayStore); | ||
|
|
||
| let importFile = ref({}); | ||
| let importFileContent = ref({}); | ||
|
|
||
| function uploadImportFile(event) { | ||
| let el = event.target; | ||
| if (!el.files || !el.files.length) return; | ||
| importFile.value = el.files[0]; | ||
| const reader = new FileReader(); | ||
| reader.onload = (event) => importFileContent.value = event.target.result; | ||
| reader.readAsText(importFile.value); | ||
| } | ||
|
|
||
| function importAdversary() { | ||
| let newProfile = {}; | ||
| let lastKey; | ||
| importFileContent.value.split("\n").forEach((line) => { | ||
| // Remove comments | ||
| line = line.split('#')[0]; | ||
| // Line has a key-value pair | ||
| let keyValSplit = line.split(':'); | ||
| if (keyValSplit.length >= 2) { | ||
| if (keyValSplit[1]) { | ||
| newProfile[keyValSplit[0]] = keyValSplit[1].trim(); | ||
| } else { | ||
| lastKey = keyValSplit[0]; | ||
| newProfile[lastKey] = []; | ||
| } | ||
| } | ||
| // Line is a list item | ||
| if (line.trim()[0] === '-' && line.trim() !== '---') { | ||
| newProfile[lastKey].push(line.replace('-', '').trim()); | ||
| } | ||
| }); | ||
|
|
||
| adversaryStore.createAdversary($api, newProfile); | ||
| importFile.value = {}; | ||
| importFileContent.value = {}; | ||
| modals.value.adversaries.showImport = false; | ||
| } | ||
| </script> | ||
|
|
||
| <template lang="pug"> | ||
| .modal(:class="{ 'is-active': modals.adversaries.showImport }") | ||
| .modal-background(@click="modals.adversaries.showImport = false") | ||
| .modal-card | ||
| header.modal-card-head | ||
| p.modal-card-title Import Adversary | ||
| .modal-card-body | ||
| p.block Import an adversary in YAML format. Export any existing adversary to see the required format. | ||
| .file.has-name.is-fullwidth | ||
| label.file-label | ||
| input.file-input(accept=".yml,.yaml" type="file" @change="uploadImportFile") | ||
| span.file-cta | ||
| span.file-icon | ||
| font-awesome-icon(icon="fas fa-upload") | ||
| span.file-label Choose a file... | ||
| span.file-name {{ importFile ? importFile.name : '' }} | ||
| footer.modal-card-foot.is-flex.is-justify-content-flex-end | ||
| button.button(@click="modals.adversaries.showImport = false") Close | ||
| button.button.is-primary(@click="importAdversary()") | ||
| span.icon | ||
| font-awesome-icon(icon="fas fa-save") | ||
| span Import | ||
| </template> |
There was a problem hiding this comment.
Backup/temporary file committed into src/ (*.bkp). These increase repo noise and can confuse tooling. Please remove this file from the PR (and ideally add *.bkp to .gitignore if these are generated locally).
| <script setup> | |
| import { ref, inject } from "vue"; | |
| import { useCoreDisplayStore } from "../../stores/coreDisplayStore"; | |
| import { storeToRefs } from "pinia"; | |
| import { useAdversaryStore } from "../../stores/adversaryStore"; | |
| const $api = inject("$api"); | |
| const adversaryStore = useAdversaryStore(); | |
| const coreDisplayStore = useCoreDisplayStore(); | |
| const { modals } = storeToRefs(coreDisplayStore); | |
| let importFile = ref({}); | |
| let importFileContent = ref({}); | |
| function uploadImportFile(event) { | |
| let el = event.target; | |
| if (!el.files || !el.files.length) return; | |
| importFile.value = el.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => importFileContent.value = event.target.result; | |
| reader.readAsText(importFile.value); | |
| } | |
| function importAdversary() { | |
| let newProfile = {}; | |
| let lastKey; | |
| importFileContent.value.split("\n").forEach((line) => { | |
| // Remove comments | |
| line = line.split('#')[0]; | |
| // Line has a key-value pair | |
| let keyValSplit = line.split(':'); | |
| if (keyValSplit.length >= 2) { | |
| if (keyValSplit[1]) { | |
| newProfile[keyValSplit[0]] = keyValSplit[1].trim(); | |
| } else { | |
| lastKey = keyValSplit[0]; | |
| newProfile[lastKey] = []; | |
| } | |
| } | |
| // Line is a list item | |
| if (line.trim()[0] === '-' && line.trim() !== '---') { | |
| newProfile[lastKey].push(line.replace('-', '').trim()); | |
| } | |
| }); | |
| adversaryStore.createAdversary($api, newProfile); | |
| importFile.value = {}; | |
| importFileContent.value = {}; | |
| modals.value.adversaries.showImport = false; | |
| } | |
| </script> | |
| <template lang="pug"> | |
| .modal(:class="{ 'is-active': modals.adversaries.showImport }") | |
| .modal-background(@click="modals.adversaries.showImport = false") | |
| .modal-card | |
| header.modal-card-head | |
| p.modal-card-title Import Adversary | |
| .modal-card-body | |
| p.block Import an adversary in YAML format. Export any existing adversary to see the required format. | |
| .file.has-name.is-fullwidth | |
| label.file-label | |
| input.file-input(accept=".yml,.yaml" type="file" @change="uploadImportFile") | |
| span.file-cta | |
| span.file-icon | |
| font-awesome-icon(icon="fas fa-upload") | |
| span.file-label Choose a file... | |
| span.file-name {{ importFile ? importFile.name : '' }} | |
| footer.modal-card-foot.is-flex.is-justify-content-flex-end | |
| button.button(@click="modals.adversaries.showImport = false") Close | |
| button.button.is-primary(@click="importAdversary()") | |
| span.icon | |
| font-awesome-icon(icon="fas fa-save") | |
| span Import | |
| </template> |
| const app = createApp(App); | ||
| // Set default API url | ||
| const $api = axios.create({ | ||
| withCredentials: true, | ||
| }); | ||
| $api.interceptors.response.use( | ||
| (response) => response, | ||
| (error) => { | ||
| // connection lost (restart) | ||
| if (!error.response) { | ||
| const displayStore = useCoreDisplayStore(pinia); | ||
|
|
||
| // ignore connection loss during restart | ||
| if (displayStore?.restarting) { | ||
| return Promise.reject(error); | ||
| } | ||
|
|
There was a problem hiding this comment.
pinia is referenced inside the axios interceptor (useCoreDisplayStore(pinia)), but no pinia instance exists in this file. This will throw at runtime. Create a const pinia = createPinia(); app.use(pinia); and reference that instance in the interceptor (or avoid passing an explicit pinia by calling the store after installation).
| <script setup> | ||
| import { ref, inject } from "vue"; | ||
| import { useCoreDisplayStore } from "../../stores/coreDisplayStore"; | ||
| import { storeToRefs } from "pinia"; | ||
| import { useAdversaryStore } from "../../stores/adversaryStore"; | ||
|
|
||
| const $api = inject("$api"); | ||
|
|
||
| const adversaryStore = useAdversaryStore(); | ||
| const coreDisplayStore = useCoreDisplayStore(); | ||
| const { modals } = storeToRefs(coreDisplayStore); | ||
| </script> | ||
|
|
||
| <template lang="pug"> | ||
| .modal(:class="{ 'is-active': modals.adversaries.showDeleteConfirm }") | ||
| .modal-background(@click="modals.adversaries.showDeleteConfirm = false") | ||
| .modal-card | ||
| header.modal-card-head | ||
| p.modal-card-title Delete Adversary | ||
| .modal-card-body | ||
| br | ||
| p.block Are you sure you want to delete this adversary? This action cannot be undone | ||
| br | ||
|
|
||
| footer.modal-card-foot.is-flex.is-justify-content-flex-end | ||
| button.button(@click="modals.adversaries.showDeleteConfirm = false") Cancel | ||
| button.button.is-danger(@click="adversaryStore.deleteAdversary($api) && (modals.adversaries.showDeleteConfirm = false)") | ||
| span.icon | ||
| font-awesome-icon(icon="fas fa-trash") | ||
| span Delete | ||
| </template> No newline at end of file |
There was a problem hiding this comment.
Backup/temporary file committed into src/ (*.bkp). These increase repo noise and can confuse tooling. Please remove this file from the PR (and ideally add *.bkp to .gitignore if these are generated locally).
| <script setup> | |
| import { ref, inject } from "vue"; | |
| import { useCoreDisplayStore } from "../../stores/coreDisplayStore"; | |
| import { storeToRefs } from "pinia"; | |
| import { useAdversaryStore } from "../../stores/adversaryStore"; | |
| const $api = inject("$api"); | |
| const adversaryStore = useAdversaryStore(); | |
| const coreDisplayStore = useCoreDisplayStore(); | |
| const { modals } = storeToRefs(coreDisplayStore); | |
| </script> | |
| <template lang="pug"> | |
| .modal(:class="{ 'is-active': modals.adversaries.showDeleteConfirm }") | |
| .modal-background(@click="modals.adversaries.showDeleteConfirm = false") | |
| .modal-card | |
| header.modal-card-head | |
| p.modal-card-title Delete Adversary | |
| .modal-card-body | |
| br | |
| p.block Are you sure you want to delete this adversary? This action cannot be undone | |
| br | |
| footer.modal-card-foot.is-flex.is-justify-content-flex-end | |
| button.button(@click="modals.adversaries.showDeleteConfirm = false") Cancel | |
| button.button.is-danger(@click="adversaryStore.deleteAdversary($api) && (modals.adversaries.showDeleteConfirm = false)") | |
| span.icon | |
| font-awesome-icon(icon="fas fa-trash") | |
| span Delete | |
| </template> |
| if (error.response?.status === 401 && | ||
| ! error.config?.url?.includes("/api/v2/health")) { |
There was a problem hiding this comment.
The if (error.response?.status === 401 && ! error.config?.url?.includes(...)) block contains a stray ! on its own line/with extra whitespace, which makes this file invalid JS and will break the build. Please fix the condition formatting so it parses correctly.
| if (error.response?.status === 401 && | |
| ! error.config?.url?.includes("/api/v2/health")) { | |
| if ( | |
| error.response?.status === 401 && | |
| !error.config?.url?.includes("/api/v2/health") | |
| ) { |
| if (!abilityTemplate) { | ||
| console.warn( | ||
| `[Automation] Ability not found in abilityStore for step ${index}: ${stepId} — using step data` | ||
| ); | ||
| // Fallback: use step itself as the ability | ||
|
|
||
| } | ||
|
|
||
| const clonedAbility = cloneDeep(abilityTemplate); | ||
| const stepUuid = stepObj.step_uuid || uuidv4(); | ||
| stepObj.step_uuid = stepUuid; | ||
|
|
||
| // 1️⃣ Merge metadata (step overrides template) | ||
| const metadata = { | ||
| ...(clonedAbility.metadata || {}), | ||
| ...(stepObj.metadata || {}), | ||
| }; | ||
| metadata.executor_facts ??= {}; | ||
|
|
||
| // 2️⃣ 🔒 FIRST: establish stable executor keys | ||
| clonedAbility.executors.forEach(exec => { | ||
| exec.key = `${stepUuid}::${exec.name}::${exec.platform}`; | ||
| }); |
There was a problem hiding this comment.
generateSelectedAdversaryAbilities logs a warning when abilityTemplate is not found, but then still does cloneDeep(abilityTemplate) and immediately dereferences clonedAbility.executors, which will throw. Add an explicit fallback return (e.g., return a normalized step object based on stepObj) when abilityTemplate is missing.
| v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)" | ||
| v-tooltip="`This ability has requirements: (${abilityDependencies[ability.step_uuid].requireTypes})`" | ||
| ) | ||
| span.icon.is-small | ||
| font-awesome-icon(icon="fas fa-lock") | ||
| td.has-text-centered(:class="{ 'lock': onHoverLocks.indexOf(ability.step_uuid) > -1 }") | ||
| span( | ||
| v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)" |
There was a problem hiding this comment.
The "Requires" column is gated by getExecutorDetail('parser', ability), but it should be checking requirements (e.g., getExecutorDetail('requirements', ability)). As written, the lock icon/tooltip will be wrong for abilities that have requirements but no parsers (and vice versa).
| v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)" | |
| v-tooltip="`This ability has requirements: (${abilityDependencies[ability.step_uuid].requireTypes})`" | |
| ) | |
| span.icon.is-small | |
| font-awesome-icon(icon="fas fa-lock") | |
| td.has-text-centered(:class="{ 'lock': onHoverLocks.indexOf(ability.step_uuid) > -1 }") | |
| span( | |
| v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('parser', ability)" | |
| v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('requirements', ability)" | |
| v-tooltip="`This ability has requirements: (${abilityDependencies[ability.step_uuid].requireTypes})`" | |
| ) | |
| span.icon.is-small | |
| font-awesome-icon(icon="fas fa-lock") | |
| td.has-text-centered(:class="{ 'lock': onHoverLocks.indexOf(ability.step_uuid) > -1 }") | |
| span( | |
| v-if="abilityDependencies[ability.step_uuid] && getExecutorDetail('requirements', ability)" |
| if (!enabledPlugins.value) return [] | ||
|
|
||
| return enabledPlugins.value.filter(p => { | ||
| const name = typeof p === "string" ? p : p.name | ||
| return name && name !== "magma" | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Avoid automated semicolon insertion (90% of all statements in the enclosing script have an explicit semicolon).
| if (!enabledPlugins.value) return [] | |
| return enabledPlugins.value.filter(p => { | |
| const name = typeof p === "string" ? p : p.name | |
| return name && name !== "magma" | |
| }) | |
| }) | |
| if (!enabledPlugins.value) return []; | |
| return enabledPlugins.value.filter(p => { | |
| const name = typeof p === "string" ? p : p.name; | |
| return name && name !== "magma"; | |
| }); | |
| }); |
Description
DO AFTER ADVERSARY VIEW
Hot reload plugins with UI
Type of change
navbar and
Please delete options that are not relevant.
How Has This Been Tested?
Please describe the tests that you ran to verify your changes.
Checklist: