Skip to content

Commit

Permalink
Merge pull request #54 from jbaptperez/feature/manage-payloads
Browse files Browse the repository at this point in the history
Feature - Manage payloads
  • Loading branch information
elegantmoose authored Jul 15, 2024
2 parents e0269b5 + 81c90e1 commit 47613a8
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
9 changes: 6 additions & 3 deletions src/components/core/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function promptToEnablePlugin(pluginName) {
font-awesome-icon(icon="fas fa-user")
span {{ version }}
aside.menu(v-if="!userSettings.collapseNavigation")

p.menu-label
font-awesome-icon(icon="fas fa-flag").pr-2
| Campaigns
Expand Down Expand Up @@ -88,6 +88,8 @@ function promptToEnablePlugin(pluginName) {
router-link.menu-item(to="/contacts") contacts
li
router-link.menu-item(to="/exfilledfiles") exfilled files
li
router-link.menu-item(to="/payloads") payloads
p.menu-label
font-awesome-icon(icon="fas fa-book").pr-2
| Resources
Expand All @@ -97,7 +99,7 @@ function promptToEnablePlugin(pluginName) {
li
router-link.menu-item(to="/obfuscators") obfuscators
li
a.menu-item(href="/api/docs" target="_blank")
a.menu-item(href="/api/docs" target="_blank")
| api docs
font-awesome-icon(icon="fas fa-external-link-alt").pl-1.is-size-7
ul.menu-list.has-text-centered.mt-2
Expand Down Expand Up @@ -145,6 +147,7 @@ function promptToEnablePlugin(pluginName) {
router-link.dropdown-item(to="/objectives") objectives
router-link.dropdown-item(to="/contacts") contacts
router-link.dropdown-item(to="/exfilledfiles") exfilled files
router-link.dropdown-item(to="/payloads") payloads
.dropdown.is-hoverable.mb-2
.dropdown-trigger
button.button(aria-haspopup="true" aria-controls="dropdown-menu")
Expand All @@ -154,7 +157,7 @@ function promptToEnablePlugin(pluginName) {
.dropdown-content.ml-2
router-link.dropdown-item(to="/planners") planners
router-link.dropdown-item(to="/obfuscators") obfuscators
a.dropdown-item(href="/api/docs" target="_blank")
a.dropdown-item(href="/api/docs" target="_blank")
| api docs
font-awesome-icon(icon="fas fa-external-link-alt").pl-1.is-size-7

Expand Down
64 changes: 64 additions & 0 deletions src/components/payloads/UploadModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script setup>
import { ref, inject} from "vue";
import { useAbilityStore } from "../../stores/abilityStore";
import { useCoreDisplayStore } from "../../stores/coreDisplayStore";
import { storeToRefs } from "pinia";
const $api = inject("$api");
const abilityStore = useAbilityStore();
const coreDisplayStore = useCoreDisplayStore();
const { modals } = storeToRefs(coreDisplayStore);
const fileUploadPlaceholder = "No file selected.";
const fileName = ref(fileUploadPlaceholder);
const isFileSelected = ref(false);
const input = ref(null);
async function updateFileName($event) {
if ($event.target.files.length > 0) {
fileName.value = $event.target.files[0].name;
isFileSelected.value = true;
} else {
isFileSelected.value = false;
}
}
async function submitFile($event) {
const file = input.value.files[0];
await abilityStore.savePayload($api, file, true, true);
fileName.value = fileUploadPlaceholder;
isFileSelected.value = false;
modals.value.payloads.showUpload = false;
}
</script>

<template lang="pug">
.modal(:class="{ 'is-active': modals.payloads.showUpload }")
.modal-background(@click="modals.payloads.showUpload = false")
.modal-card
header.modal-card-head
p.modal-card-title Upload a payload
.modal-card-body
.file.has-name.is-fullwidth
label.file-label
input.file-input(type="file", ref="input", @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="modals.payloads.showUpload = false") Close
button.button.is-primary(:disabled="!isFileSelected", @click="submitFile($event)")
span.icon
font-awesome-icon(icon="fas fa-save")
span Upload
</template>

<style scoped>
.modal-card{
width: 70%;
}
</style>
6 changes: 6 additions & 0 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import axios from "axios";
import HomeView from "./views/HomeView.vue";
import LoginView from "./views/LoginView.vue";
import AgentsView from "./views/AgentsView.vue";
import PayloadsView from "./views/PayloadsView.vue";
import AbilitiesView from "./views/AbilitiesView.vue";
import AdversariesView from "./views/AdversariesView.vue";
import OperationsView from "./views/OperationsView.vue";
Expand Down Expand Up @@ -42,6 +43,11 @@ const router = createRouter({
name: "agents",
component: AgentsView,
},
{
path: "/payloads",
name: "payloads",
component: PayloadsView,
},
{
path: "/abilities",
name: "abilities",
Expand Down
39 changes: 36 additions & 3 deletions src/stores/abilityStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,46 @@ export const useAbilityStore = defineStore("abilityStore", {
console.error("Error fetching abilities", error);
}
},
async getPayloads($api) {
async getPayloads($api, sort=false, excludePlugins=false, addPath=false) {
try {
const response = await $api.get("/api/v2/payloads");
const response = await $api.get("/api/v2/payloads", {params: {sort: sort, exclude_plugins: excludePlugins, add_path: addPath}});
this.payloads = response.data;
} catch(error) {
console.error("Error fetching payloads", error);
}
}
},
async savePayload($api, file, sort=false, addPath=false) {
try {
let formData = new FormData();
formData.append("file", file);
const response = await $api.post(`/api/v2/payloads`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
let name = response.data["payloads"][0]
if (addPath) {
name = "data/payloads/" + name;
}
let index = sort ?
this.payloads.findIndex((payload) => name.localeCompare(payload) < 0) : -1;
if (index === -1) {
this.payloads.push(name);
} else {
this.payloads.splice(index, 0, name);
}
} catch(error) {
console.error("Error uploading payload.", error);
}
},
async deletePayload($api, payloadName, addPath=false) {
try {
await $api.delete(`/api/v2/payloads/${payloadName}`);
this.payloads.splice(this.payloads.findIndex((payload) =>
(addPath ? payload.replace(/^data\/payloads\//, '') : payload) === payloadName), 1);
} catch(error) {
console.error("Error deleting payload.", error);
}
},
},
});
3 changes: 3 additions & 0 deletions src/stores/coreDisplayStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export const useCoreDisplayStore = defineStore("coreDisplayStore", {
showConfig: false,
showDetails: false,
},
payloads: {
showUpload: false,
},
adversaries: {
showFactBreakdown: false,
showImport: false,
Expand Down
15 changes: 15 additions & 0 deletions src/tests/PayloadsView.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { mount } from "@vue/test-utils";
import { axe, toHaveNoViolations } from "jest-axe";
import PayloadsView from "../views/PayloadsView.vue";

expect.extend(toHaveNoViolations);
const wrapper = mount(PayloadsView);
test("PayloadsView should have no accessibility violations", async () => {
const results = await axe(wrapper.element, {
// Set axe rules: https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
rules: {
region: { enabled: false },
},
});
expect(results).toHaveNoViolations();
});
115 changes: 115 additions & 0 deletions src/views/PayloadsView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script setup>
import { inject, onMounted, computed } from "vue";
import { storeToRefs } from "pinia";
import { useCoreDisplayStore } from "@/stores/coreDisplayStore";
import UploadModal from "@/components/payloads/UploadModal.vue";
import { useAbilityStore } from "@/stores/abilityStore";
const $api = inject("$api");
const abilityStore = useAbilityStore();
const coreDisplayStore = useCoreDisplayStore();
const { payloads } = storeToRefs(abilityStore);
const { modals } = storeToRefs(coreDisplayStore);
const structuredPayloads = computed(() => {
const regex = /^(?:plugins\/([^/]+)|data)?\/payloads\/(.+)$/;
return payloads.value.map(payload => {
const match = payload.match(regex);
if (!match) {
console.error(`Payload path "${payload}" does not match the expected format.`);
return null;
}
let belongsToAPlugin = match[1] !== undefined;
return {
fullPath: payload,
belongsToAPlugin: match[1] !== undefined,
pluginName: match[1] || null,
fileName: match[2],
};
}).filter(Boolean); // Remove null values
});
const pluginStructuredPayloads = computed(() => {
return structuredPayloads.value.filter(payload => payload.belongsToAPlugin);
});
const nonPluginStructuredPayloads = computed(() => {
return structuredPayloads.value.filter(payload => !payload.belongsToAPlugin);
});
onMounted(async () => {
await abilityStore.getPayloads($api, true, false, true);
});
</script>

<template lang="pug">
.content
h2 Payloads
p
| Payloads are any files that you can reference in ability executors.
| They are transferred to an agent which can then use them.<br/>
| You can only add or delete local payloads, not plugin ones.
hr

.content
h2 Local Payloads
.columns.mb-4
.column.is-one-quarter.is-flex.buttons.mb-0
button.button(@click="modals.payloads.showUpload = true")
span.icon
font-awesome-icon(icon="fas fa-file-import")
span Upload a payload
.column.is-half.is-flex.is-justify-content-center
span.tag.is-medium.m-0
span.has-text-success
strong
| {{ nonPluginStructuredPayloads.length }}
| payload{{ nonPluginStructuredPayloads.length === 0 || nonPluginStructuredPayloads.length > 1 ? 's' : '' }}
table.table.is-striped.is-fullwidth.is-narrow
thead
tr
th File name
th File path
th
tbody
tr.pointer(v-for="(payload, index) in nonPluginStructuredPayloads")
td {{ payload.fileName }}
td.is-four-fifths {{ payload.fullPath }}
td.has-text-centered
button.delete.is-white(@click.stop="abilityStore.deletePayload($api, payload.fileName, true)")

.content
h2 Plugin Payloads
.columns.mb-4
.column.is-full.is-flex.is-justify-content-center
span.tag.is-medium.m-0
span.has-text-success
strong
| {{ pluginStructuredPayloads.length }}
| payload{{ pluginStructuredPayloads.length === 0 || pluginStructuredPayloads.length > 1 ? 's' : '' }}
table.table.is-striped.is-fullwidth.is-narrow
thead
tr
th File name
th File path
th Plugin Name
tbody
tr.pointer(v-for="payload in pluginStructuredPayloads")
td {{ payload.fileName }}
td.is-four-fifths {{ payload.fullPath }}
td {{ payload.pluginName }}
UploadModal
</template>

<style scoped>
tr {
cursor: pointer;
}
td.has-text-centered {
width: 40px;
}
</style>

0 comments on commit 47613a8

Please sign in to comment.