diff --git a/package-lock.json b/package-lock.json
index 97a3f82..911aafb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,8 @@
"vue-eslint-parser": "^10.2.0",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-router": "^4.0.12",
- "vue-scrollto": "^2.20.0"
+ "vue-scrollto": "^2.20.0",
+ "yaml": "^2.8.3"
},
"devDependencies": {
"@babel/core": "^7.17.12",
@@ -74,6 +75,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2150,6 +2152,7 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -2741,17 +2744,6 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
- "node_modules/@types/node": {
- "version": "24.6.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz",
- "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "undici-types": "~7.13.0"
- }
- },
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
@@ -2915,6 +2907,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3109,6 +3102,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -3323,6 +3317,7 @@
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"license": "ISC",
+ "peer": true,
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-quadtree": "1 - 3",
@@ -3614,6 +3609,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5291,6 +5287,7 @@
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
@@ -5844,6 +5841,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -5881,14 +5879,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/undici-types": {
- "version": "7.13.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
- "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
@@ -6014,6 +6004,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -6105,6 +6096,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -6126,6 +6118,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-sfc": "3.5.22",
@@ -6378,6 +6371,22 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
+ "license": "ISC",
+ "peer": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 9b9dd44..82b4a95 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"vue-eslint-parser": "^10.2.0",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-router": "^4.0.12",
- "vue-scrollto": "^2.20.0"
+ "vue-scrollto": "^2.20.0",
+ "yaml": "^2.8.3"
},
"devDependencies": {
"@babel/core": "^7.17.12",
diff --git a/src/components/abilities/ImportAbilityModal.vue b/src/components/abilities/ImportAbilityModal.vue
new file mode 100644
index 0000000..f9badb4
--- /dev/null
+++ b/src/components/abilities/ImportAbilityModal.vue
@@ -0,0 +1,137 @@
+
+
+
+.modal(:class="{ 'is-active': modals.abilities.showImport }")
+ .modal-background(@click="closeModal")
+ .modal-card
+ header.modal-card-head
+ p.modal-card-title Import Ability YAML
+ .modal-card-body
+ .file.has-name.is-fullwidth
+ label.file-label
+ input.file-input(type="file", ref="input", accept=".yml,.yaml", @change="updateFileName")
+ span.file-cta
+ span.file-icon
+ font-awesome-icon(icon="fas fa-upload")
+ span.file-label Choose a file...
+ span.file-name {{ fileName }}
+ footer.modal-card-foot.is-flex.is-justify-content-flex-end
+ button.button(@click="closeModal") Close
+ button.button.is-primary(:class="{ 'is-loading': isImporting }" :disabled="!isFileSelected || isImporting", @click="submitFile")
+ span.icon
+ font-awesome-icon(icon="fas fa-save")
+ span Import
+
+
+
diff --git a/src/stores/abilityStore.js b/src/stores/abilityStore.js
index f75d32e..b2140a3 100644
--- a/src/stores/abilityStore.js
+++ b/src/stores/abilityStore.js
@@ -68,6 +68,16 @@ export const useAbilityStore = defineStore("abilityStore", {
console.error("Error fetching abilities", error);
}
},
+ async importAbility($api, ability) {
+ try {
+ const response = await $api.post("/api/v2/abilities", ability);
+ await this.getAbilities($api);
+ return response.data;
+ } catch(error) {
+ console.error("Error importing ability.", error);
+ throw error;
+ }
+ },
async getPayloads($api, sort=false, excludePlugins=false, addPath=false) {
try {
const response = await $api.get("/api/v2/payloads", {params: {sort: sort, exclude_plugins: excludePlugins, add_path: addPath}});
diff --git a/src/stores/coreDisplayStore.js b/src/stores/coreDisplayStore.js
index eea7ba0..5b284e9 100644
--- a/src/stores/coreDisplayStore.js
+++ b/src/stores/coreDisplayStore.js
@@ -14,6 +14,9 @@ export const useCoreDisplayStore = defineStore("coreDisplayStore", {
payloads: {
showUpload: false,
},
+ abilities: {
+ showImport: false,
+ },
adversaries: {
showFactBreakdown: false,
showImport: false,
diff --git a/src/views/AbilitiesView.vue b/src/views/AbilitiesView.vue
index 8bae5c7..e15abf4 100644
--- a/src/views/AbilitiesView.vue
+++ b/src/views/AbilitiesView.vue
@@ -2,13 +2,16 @@
import { storeToRefs } from "pinia";
import { reactive, ref, inject, onMounted, computed } from "vue";
import { useRoute } from "vue-router";
+import { useCoreDisplayStore } from "@/stores/coreDisplayStore";
import { useAbilityStore } from "@/stores/abilityStore";
import { getAbilityPlatforms } from "@/utils/abilityUtil.js";
import CreateEditAbility from "@/components/abilities/CreateEditAbility.vue";
+import ImportAbilityModal from "@/components/abilities/ImportAbilityModal.vue";
const $api = inject("$api");
const route = useRoute();
+const coreDisplayStore = useCoreDisplayStore();
const abilityStore = useAbilityStore();
const { abilities, tactics, techniques, plugins, platforms } = storeToRefs(abilityStore);
@@ -64,6 +67,10 @@ hr
span.icon
font-awesome-icon(icon="fas fa-plus")
span Create an Ability
+ button.button.is-fullwidth.mb-4(@click="coreDisplayStore.modals.abilities.showImport = true")
+ span.icon
+ font-awesome-icon(icon="fas fa-file-import")
+ span Import
form
.field
.control.has-icons-left
@@ -113,6 +120,7 @@ hr
//- Modals
CreateEditAbility(:ability="selectedAbility" :active="showAbilityModal" :creating="isCreatingAbility" @close="showAbilityModal = false")
+ImportAbilityModal