diff --git a/package-lock.json b/package-lock.json
index 591ea08..86d4322 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"bulma": "^0.9.3",
"bulma-switch": "^2.0.4",
"bulma-toast": "^2.4.3",
+ "cron-validate": "^1.4.5",
"d3-force": "^3.0.0",
"echarts": "^5.3.1",
"floating-vue": "^2.0.0-beta.20",
@@ -1712,7 +1713,6 @@
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.3.tgz",
"integrity": "sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==",
- "dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
@@ -2475,6 +2475,11 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.6",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz",
+ "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA=="
+ },
"node_modules/@types/node": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz",
@@ -3572,6 +3577,14 @@
"url": "https://opencollective.com/core-js"
}
},
+ "node_modules/cron-validate": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/cron-validate/-/cron-validate-1.4.5.tgz",
+ "integrity": "sha512-nKlOJEnYKudMn/aNyNH8xxWczlfpaazfWV32Pcx/2St51r2bxWbGhZD7uwzMcRhunA/ZNL+Htm/i0792Z59UMQ==",
+ "dependencies": {
+ "yup": "0.32.9"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -7182,8 +7195,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
@@ -7363,6 +7375,11 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
+ "node_modules/nanoclone": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
+ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
+ },
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@@ -7945,6 +7962,11 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
+ "node_modules/property-expr": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
+ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
+ },
"node_modules/psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -8099,8 +8121,7 @@
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
- "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
- "dev": true
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"node_modules/regenerator-transform": {
"version": "0.15.0",
@@ -8806,6 +8827,11 @@
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ="
},
+ "node_modules/toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
+ },
"node_modules/tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
@@ -9462,6 +9488,23 @@
"node": ">=12"
}
},
+ "node_modules/yup": {
+ "version": "0.32.9",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.9.tgz",
+ "integrity": "sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==",
+ "dependencies": {
+ "@babel/runtime": "^7.10.5",
+ "@types/lodash": "^4.14.165",
+ "lodash": "^4.17.20",
+ "lodash-es": "^4.17.15",
+ "nanoclone": "^0.2.1",
+ "property-expr": "^2.0.4",
+ "toposort": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/zrender": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.1.tgz",
diff --git a/package.json b/package.json
index a731c2a..c2a9ec6 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"bulma": "^0.9.3",
"bulma-switch": "^2.0.4",
"bulma-toast": "^2.4.3",
+ "cron-validate": "^1.4.5",
"d3-force": "^3.0.0",
"echarts": "^5.3.1",
"floating-vue": "^2.0.0-beta.20",
diff --git a/src/components/core/Navigation.vue b/src/components/core/Navigation.vue
index 5aaf1c6..130daa5 100644
--- a/src/components/core/Navigation.vue
+++ b/src/components/core/Navigation.vue
@@ -66,6 +66,8 @@ function promptToEnablePlugin(pluginName) {
router-link.menu-item(to="/adversaries") adversaries
li
router-link.menu-item(to="/operations") operations
+ li
+ router-link.menu-item(to="/schedules") schedules
p.menu-label
font-awesome-icon(icon="fas fa-puzzle-piece").pr-2
| Plugins
@@ -124,6 +126,7 @@ function promptToEnablePlugin(pluginName) {
router-link.dropdown-item(to="/abilities") abilities
router-link.dropdown-item(to="/adversaries") adversaries
router-link.dropdown-item(to="/operations") operations
+ router-link.dropdown-item(to="/schedules") schedules
.dropdown.is-hoverable.mb-2
.dropdown-trigger
button.button(aria-haspopup="true" aria-controls="dropdown-menu")
diff --git a/src/components/schedules/CreateScheduleModal.vue b/src/components/schedules/CreateScheduleModal.vue
new file mode 100644
index 0000000..ffd87af
--- /dev/null
+++ b/src/components/schedules/CreateScheduleModal.vue
@@ -0,0 +1,359 @@
+
+
+
+.modal(:class="{ 'is-active': modals.schedules.showCreate }")
+ .modal-background(@click="closeModal()")
+ .modal-card
+ header.modal-card-head
+ p.modal-card-title {{ scheduleStore.currentSchedule ? "Update" : "Create New" }} Schedule
+ .modal-card-body
+ .field.is-horizontal
+ .field-label.is-normal
+ label.label Schedule
+ .field-body
+ input.input(placeholder="* * * * * " v-model="schedule")
+ label.label.ml-3.mt-1.has-text-danger {{ `${validation.schedule}` }}
+ .field.is-horizontal
+ .field-label.is-normal
+ label.label Task Name
+ .field-body
+ input.input(placeholder="Operation Name" v-model="taskName" :disabled="scheduleStore.currentSchedule")
+ label.label.ml-3.mt-1.has-text-danger {{ `${validation.name}` }}
+ .field.is-horizontal
+ .field-label.is-normal
+ label.label Adversary
+ .field-body
+ .control
+ .select
+ select(v-model="selectedAdversary" :disabled="scheduleStore.currentSchedule")
+ option(selected value="") No Adversary (manual)
+ option(v-for="adversary in adversaryStore.adversaries" :key="adversary.id" :value="adversary") {{ `${adversary.name}` }}
+ .field.is-horizontal
+ .field-label.is-normal
+ label.label Fact Source
+ .field-body
+ .control
+ .select
+ select(v-model="selectedSource" :disabled="scheduleStore.currentSchedule")
+ option(disabled selected value="") Choose a Fact Source
+ option(v-for="source in sources" :key="source.id" :value="source") {{ `${source.name}` }}
+ .field.is-horizontal
+ .field-label.is-normal
+ label.label Group
+ .field-body
+ button.button(:class="{ 'is-primary': selectedGroup === '' }" @click="selectedGroup = ''") All groups
+ button.button.mx-2(v-for="group in agentStore.agentGroups" :key="group" :class="{ 'is-primary': selectedGroup === group }", @click="selectedGroup = group") {{`${group}`}}
+ .field.is-horizontal
+ .field-label.is-normal
+ label.label Planner
+ .field-body
+ .control
+ .select
+ select(v-model="selectedPlanner" :disabled="scheduleStore.currentSchedule")
+ option(v-for="planner in coreStore.planners" :key="planner.id" :value="planner") {{ `${planner.name}` }}
+ .field.is-horizontal
+ .field-label
+ label.label Obfuscators
+ .field-body
+ .field.is-grouped-multiline
+ button.button.my-1.mr-2(v-for="obf in coreStore.obfuscators" :key="obf.id" :value="obf" :class="{ 'is-primary': selectedObfuscator.name === obf.name }" @click="selectedObfuscator = obf") {{ `${obf.name}` }}
+ .field.is-horizontal
+ .field-label
+ label.label Autonomous
+ .field-body
+ .field.is-grouped
+ input.is-checkradio(type="radio" id="auto" :checked="isAuto" @click="isAuto = true")
+ label.label.ml-3.mt-1(for="auto") Run autonomously
+ input.is-checkradio.ml-3(type="radio" id="manual" :checked="!isAuto" @click="isAuto = false")
+ label.label.ml-3.mt-1(for="manual") Require manual approval
+ .field.is-horizontal
+ .field-label
+ label.label Parser
+ .field-body
+ .field.is-grouped
+ input.is-checkradio(type="radio" id="defaultparser" :checked="isDefParser" @click="isDefParser = true" :disabled="scheduleStore.currentSchedule")
+ label.label.ml-3.mt-1(for="defaultparser") Use Default Parser
+ input.is-checkradio.ml-3(type="radio" id="nondefaultparser" :checked="!isDefParser" @click="isDefParser = false" :disabled="scheduleStore.currentSchedule")
+ label.label.ml-3.mt-1(for="nondefaultparser") Don't use default learning parsers
+ .field.is-horizontal
+ .field-label
+ label.label Auto Close
+ .field-body.is-grouped
+ input.is-checkradio(type="radio" id="keepopen" :checked="!isAutoClose" @click="isAutoClose = false" :disabled="scheduleStore.currentSchedule")
+ label.label.ml-3.mt-1(for="keepopen") Keep open forever
+ input.is-checkradio.ml-3(type="radio" id="autoclose" :checked="isAutoClose" @click="isAutoClose = true" :disabled="scheduleStore.currentSchedule")
+ label.label.ml-3.mt-1(for="autoclose") Auto close operation
+ .field.is-horizontal
+ .field-label
+ label.label Run State
+ .field-body.is-grouped
+ input.is-checkradio(type="radio" id="runimmediately" :checked="!isPause" @click="isPause = false")
+ label.label.ml-3.mt-1(for="runimmediately") Run immediately
+ input.is-checkradio.ml-3(type="radio" id="pausestart" :checked="isPause" @click="isPause = true")
+ label.label.ml-3.mt-1(for="pausestart") Pause on start
+ .field.is-horizontal
+ .field-label
+ label.label Jitter (sec/sec)
+ .field-body
+ input.input.is-small(v-model="minJitter" :disabled="scheduleStore.currentSchedule")
+ span /
+ input.input.is-small(v-model="maxJitter" :disabled="scheduleStore.currentSchedule")
+
+ footer.modal-card-foot.is-justify-content-space-between
+ div(v-if="!scheduleStore.currentSchedule")
+ button.button.is-danger(@click="modals.schedules.showDelete = true" v-if="scheduleStore.currentSchedule") Delete
+ div
+ button.button(@click="closeModal()") Cancel
+ button.button.is-primary(@click="callApi().then(() => {})")
+ span(v-if="scheduleStore.currentSchedule") Update
+ span(v-else) Create
+
+
+
diff --git a/src/components/schedules/DeleteScheduleModal.vue b/src/components/schedules/DeleteScheduleModal.vue
new file mode 100644
index 0000000..89ac4b4
--- /dev/null
+++ b/src/components/schedules/DeleteScheduleModal.vue
@@ -0,0 +1,42 @@
+
+
+
+.modal(:class="{ 'is-active': modals.schedules.showDelete }")
+ .modal-background(@click="modals.schedules.showDelete = false")
+ .modal-card
+ header.modal-card-head
+ p.modal-card-title Delete Schedule?
+ .modal-card-body(v-if="scheduleStore.currentSchedule")
+ p Are you sure you want to delete the schedule "{{ scheduleStore.currentSchedule.id }}"? This cannot be undone.
+ footer.modal-card-foot.has-text-right
+ button.button(@click="modals.schedules.showDelete = false") Cancel
+ button.button.is-danger(@click="deleteSchedule().then(() => {})")
+ span.icon
+ font-awesome-icon(icon="fa-trash")
+ span Delete
+
+
+
+
diff --git a/src/router.js b/src/router.js
index 603d160..b974b3b 100644
--- a/src/router.js
+++ b/src/router.js
@@ -9,6 +9,7 @@ import PayloadsView from "./views/PayloadsView.vue";
import AbilitiesView from "./views/AbilitiesView.vue";
import AdversariesView from "./views/AdversariesView.vue";
import OperationsView from "./views/OperationsView.vue";
+import SchedulesView from "@/views/SchedulesView.vue";
import FactSourcesView from "./views/FactSourcesView.vue";
import ObjectivesView from "./views/ObjectivesView.vue";
import PlannersView from "./views/PlannersView.vue";
@@ -63,6 +64,11 @@ const router = createRouter({
name: "operations",
component: OperationsView,
},
+ {
+ path: "/schedules",
+ name: "schedules",
+ component: SchedulesView,
+ },
{
path: "/factsources",
name: "fact sources",
diff --git a/src/stores/coreDisplayStore.js b/src/stores/coreDisplayStore.js
index 565f25e..eea7ba0 100644
--- a/src/stores/coreDisplayStore.js
+++ b/src/stores/coreDisplayStore.js
@@ -34,6 +34,10 @@ export const useCoreDisplayStore = defineStore("coreDisplayStore", {
showFilters: false,
showOutput: false,
},
+ schedules: {
+ showCreate: false,
+ showDelete: false,
+ },
},
};
},
diff --git a/src/stores/schedulesStore.js b/src/stores/schedulesStore.js
new file mode 100644
index 0000000..ca27bf6
--- /dev/null
+++ b/src/stores/schedulesStore.js
@@ -0,0 +1,56 @@
+import { defineStore } from "pinia";
+
+export const useScheduleStore = defineStore("scheduleStore", {
+ state: () => {
+ return {
+ schedules: {},
+ selectedScheduleID: "",
+ };
+ },
+ getters: {
+ currentSchedule(state) {
+ return state.schedules[state.selectedScheduleID];
+ },
+ },
+ actions: {
+ async getSchedules($api) {
+ try {
+ const response = await $api.get("/api/v2/schedules");
+ // TODO: Sort schedules
+ for (const schedule of response.data) {
+ this.schedules[schedule.id] = schedule;
+ }
+ } catch (error) {
+ console.error("Error fetching schedules", error);
+ }
+ },
+ async createSchedule($api, schedule) {
+ try {
+ const response = await $api.post("/api/v2/schedules", schedule);
+ this.schedules[response.data.id] = response.data;
+ this.selectedScheduleID = response.data.id;
+ } catch (error) {
+ console.error("Error creating schedule", error);
+ throw error;
+ }
+ },
+ async deleteSchedule($api, scheduleID) {
+ try {
+ await $api.delete(`/api/v2/schedules/${scheduleID}`);
+ delete this.schedules[scheduleID];
+ this.selectedScheduleID = "";
+ this.getSchedules($api);
+ } catch (error) {
+ console.error("Error deleting schedule", error);
+ }
+ },
+ async updateSchedule($api, newSchedule) {
+ try {
+ await $api.put(`/api/v2/schedules/${this.selectedScheduleID}`, newSchedule);
+ this.getSchedules($api);
+ } catch (error) {
+ console.error("Error updating schedule", error);
+ }
+ },
+ },
+});
diff --git a/src/views/SchedulesView.vue b/src/views/SchedulesView.vue
new file mode 100644
index 0000000..4363906
--- /dev/null
+++ b/src/views/SchedulesView.vue
@@ -0,0 +1,77 @@
+
+
+
+//- Header
+.content
+ h2 Schedules
+hr
+
+//- Button row
+.columns.mb-4
+ .column.is-one-quarter.is-flex.buttons.mb-0
+ button.button.is-primary.level-item(@click="modals.schedules.showCreate = true")
+ span.icon
+ font-awesome-icon(icon="fas fa-plus")
+ span Create a schedule
+ .column.is-half.is-flex.is-justify-content-center
+ span.tag.is-medium.m-0
+ span.has-text-success
+ strong {{ scheduleCount }} schedule{{ scheduleCount === 0 || scheduleCount > 1 ? 's' : '' }}
+table.table.is-striped.is-fullwidth.is-narrow
+ thead
+ tr
+ th.is-one-third Task Name
+ th Schedule
+ tbody
+ tr.pointer(v-for="(schedule, id) in schedules" @click="handleClick(id)")
+ td {{ schedule.task.name }}
+ td {{ schedule.schedule }}
+
+//- Modals
+CreateScheduleModal
+DeleteScheduleModal
+
+
+