From 27f1b0df79687905598252501a8dfd2925e0a417 Mon Sep 17 00:00:00 2001
From: Jonas Plum <114533931+jonas-plum@users.noreply.github.com>
Date: Sat, 21 Jan 2023 23:25:55 +0100
Subject: [PATCH] Add playbook editor (#702)
---
ui/package.json | 3 +
ui/src/components/playbookeditor/EditTask.vue | 273 ++++++++++
ui/src/components/playbookeditor/NewTask.vue | 80 +++
ui/src/components/playbookeditor/PanZoom.vue | 164 ++++++
.../playbookeditor/PlaybookEditor.vue | 235 ++++++++
.../playbookeditor/PlaybookGraph.vue | 511 ++++++++++++++++++
ui/src/views/Graph.vue | 24 +-
ui/src/views/Playbook.vue | 139 ++---
ui/src/views/Ticket.vue | 27 +-
ui/yarn.lock | 309 ++++++++++-
10 files changed, 1656 insertions(+), 109 deletions(-)
create mode 100644 ui/src/components/playbookeditor/EditTask.vue
create mode 100644 ui/src/components/playbookeditor/NewTask.vue
create mode 100644 ui/src/components/playbookeditor/PanZoom.vue
create mode 100644 ui/src/components/playbookeditor/PlaybookEditor.vue
create mode 100644 ui/src/components/playbookeditor/PlaybookGraph.vue
diff --git a/ui/package.json b/ui/package.json
index a41fc8b..a211cfa 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -9,6 +9,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
+ "@crinkles/digl": "^2.0.2",
"@koumoul/vjsf": "2.21.1",
"@mdi/font": "7.1.96",
"@mdi/util": "0.3.2",
@@ -23,6 +24,7 @@
"axios": "1.2.2",
"chart.js": "2.9.4",
"core-js": "3.27.2",
+ "d3": "^7.8.0",
"graphlib": "2.1.8",
"json-schema-editor-vue": "2.2.1",
"just-kebab-case": "4.2.0",
@@ -30,6 +32,7 @@
"less-loader": "11.1.0",
"lodash": "4.17.21",
"luxon": "3.2.1",
+ "panzoom": "^9.4.3",
"register-service-worker": "1.7.2",
"splitpanes": "2.4.1",
"swagger-ui": "4.13.0",
diff --git a/ui/src/components/playbookeditor/EditTask.vue b/ui/src/components/playbookeditor/EditTask.vue
new file mode 100644
index 0000000..82d1a16
--- /dev/null
+++ b/ui/src/components/playbookeditor/EditTask.vue
@@ -0,0 +1,273 @@
+
+
+
+ Edit Task
+
+
+
+
+ mdi-delete
+
+
+
+ Delete Task
+ Are you sure you want to delete this task?
+
+
+ Cancel
+ Delete
+
+
+
+
+ mdi-close
+
+
+
+
+
+
+
+
+
+
+
+
+ Payload Mapping
+
+ {{ key }}:
+
+
+ mdi-delete
+
+
+
+ :
+
+
+ mdi-plus
+
+
+
+
+
+ Next Task(s)
+
+ If
+
+ run
+
+ {{ playbook.tasks[key].name }}
+
+
+ mdi-delete
+
+
+
+ If
+
+ run
+
+
+ mdi-plus
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/components/playbookeditor/NewTask.vue b/ui/src/components/playbookeditor/NewTask.vue
new file mode 100644
index 0000000..091a65a
--- /dev/null
+++ b/ui/src/components/playbookeditor/NewTask.vue
@@ -0,0 +1,80 @@
+
+
+
+ Create a new step
+
+
+ mdi-close
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
diff --git a/ui/src/components/playbookeditor/PanZoom.vue b/ui/src/components/playbookeditor/PanZoom.vue
new file mode 100644
index 0000000..d0cb265
--- /dev/null
+++ b/ui/src/components/playbookeditor/PanZoom.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+ mdi-image-filter-center-focus
+
+
+ mdi-magnify-plus-outline
+
+
+ mdi-magnify-minus-outline
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/components/playbookeditor/PlaybookEditor.vue b/ui/src/components/playbookeditor/PlaybookEditor.vue
new file mode 100644
index 0000000..5a3f0d7
--- /dev/null
+++ b/ui/src/components/playbookeditor/PlaybookEditor.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+
+ mdi-plus
+ New Step
+
+
+
+
+ mdi-format-rotate-90
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/components/playbookeditor/PlaybookGraph.vue b/ui/src/components/playbookeditor/PlaybookGraph.vue
new file mode 100644
index 0000000..d06289e
--- /dev/null
+++ b/ui/src/components/playbookeditor/PlaybookGraph.vue
@@ -0,0 +1,511 @@
+
+
+
+
+
+
+
diff --git a/ui/src/views/Graph.vue b/ui/src/views/Graph.vue
index 8383dfa..d78ed85 100644
--- a/ui/src/views/Graph.vue
+++ b/ui/src/views/Graph.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/ui/src/views/Playbook.vue b/ui/src/views/Playbook.vue
index b21f3e2..29caf72 100644
--- a/ui/src/views/Playbook.vue
+++ b/ui/src/views/Playbook.vue
@@ -19,25 +19,35 @@
{{ error }}
-
-
-
-
- Playbook
-
@@ -62,6 +72,7 @@ import Editor from "../components/Editor.vue";
import {alg, Graph} from "graphlib";
import yaml from 'yaml';
import Ajv from "ajv";
+import PlaybookEditor from "@/components/playbookeditor/PlaybookEditor.vue";
const playbookSchema = {
type: "object",
@@ -100,8 +111,10 @@ interface State {
playbook?: PlaybookTemplate;
g: Record;
selected: any;
- pipelineData: any;
error: string;
+ tab: number;
+ playbookYAML: string;
+ playbookJSON: any;
}
interface TaskWithID {
@@ -140,17 +153,26 @@ const inityaml = "name: VirusTotal hash check\n" +
export default Vue.extend({
name: "Playbook",
- components: { Editor },
+ components: { Editor, PlaybookEditor },
data: (): State => ({
playbook: undefined,
g: {},
selected: undefined,
- pipelineData: undefined,
error: "",
+ tab: 1,
+ playbookJSON: undefined,
+ playbookYAML: inityaml
}),
watch: {
'$route': function () {
this.loadPlaybook();
+ },
+ tab: function (value) {
+ if (value === 0) {
+ this.playbookJSON = yaml.parse(this.playbookYAML);
+ } else {
+ this.playbookYAML = yaml.stringify(this.playbookJSON);
+ }
}
},
computed: {
@@ -198,77 +220,26 @@ export default Vue.extend({
}
return tasks;
},
- updatePipeline: function () {
- if (this.playbook) {
- this.pipeline(this.playbook.yaml);
- }
- },
- pipeline: function(playbookYAML: string) {
- try {
- let playbook = yaml.parse(playbookYAML);
-
- this.error = "";
-
- let g = new Graph();
-
- for (const stepKey in playbook.tasks) {
- g.setNode(stepKey);
- }
-
- this.lodash.forEach(playbook.tasks, (task: Task, stepKey: string) => {
- if ("next" in task) {
- this.lodash.forEach(task.next, (condition, nextKey) => {
- g.setEdge(stepKey, nextKey);
- });
- }
- });
-
- let tasks = this.tasks(g, playbook);
- let elements = [] as Array;
- this.lodash.forEach(tasks, task => {
- elements.push({
- id: task.id,
- name: task.task.name,
- next: [],
- status: "unknown"
- });
- });
-
- this.lodash.forEach(tasks, (task: TaskWithID) => {
- if ("next" in task.task) {
- this.lodash.forEach(task.task.next, (condition, nextKey) => {
- let nextID = this.lodash.findIndex(elements, ["id", nextKey]);
- let stepID = this.lodash.findIndex(elements, ["id", task.id]);
- if (nextID !== -1) {
- // TODO: invalid schema
- elements[stepID].next.push({index: nextID});
- }
- });
- }
- });
-
- this.pipelineData = undefined;
- this.$nextTick(() => {
- this.pipelineData = this.lodash.values(elements);
- })
- }
- catch (e: unknown) {
- console.log(e);
- this.error = this.lodash.toString(e);
- }
- },
save() {
if (this.playbook === undefined) {
return;
}
+ let playbook = this.playbook;
+ if (this.tab === 0) {
+ let jsonData = this.playbookJSON;
+ jsonData["name"] = playbook.name;
+ playbook.yaml = yaml.stringify(jsonData);
+ } else {
+ playbook.yaml = this.playbookYAML;
+ }
+
if (this.$route.params.id == 'new') {
- let playbook = this.playbook;
// playbook.id = kebabCase(playbook.name);
API.createPlaybook(playbook).then(() => {
this.$store.dispatch("alertSuccess", { name: "Playbook created" });
});
} else {
- API.updatePlaybook(this.$route.params.id, this.playbook).then(() => {
+ API.updatePlaybook(this.$route.params.id, playbook).then(() => {
this.$store.dispatch("alertSuccess", { name: "Playbook saved" });
});
}
@@ -279,9 +250,13 @@ export default Vue.extend({
}
if (this.$route.params.id == 'new') {
this.playbook = { name: "MyPlaybook", yaml: inityaml }
+ this.playbookJSON = yaml.parse(this.playbook.yaml);
+ this.playbookYAML = this.playbook.yaml;
} else {
API.getPlaybook(this.$route.params.id).then((response) => {
this.playbook = response.data;
+ this.playbookJSON = yaml.parse(this.playbook.yaml);
+ this.playbookYAML = this.playbook.yaml;
});
}
},
@@ -290,7 +265,7 @@ export default Vue.extend({
return this.lodash.includes(this.$store.state.settings.roles, s);
}
return false;
- }
+ },
},
mounted() {
this.loadPlaybook();
diff --git a/ui/src/views/Ticket.vue b/ui/src/views/Ticket.vue
index fb9670e..70c96a1 100644
--- a/ui/src/views/Ticket.vue
+++ b/ui/src/views/Ticket.vue
@@ -295,19 +295,13 @@
mdi-close-circle
-
@@ -319,7 +313,6 @@
link
@click="selectTask(taskwithid, playbookid)"
>
-
= 2.1.2 < 3"
-iconv-lite@^0.6.3:
+iconv-lite@0.6, iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -7222,6 +7495,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
interpret@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
@@ -9424,6 +9702,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+ngraph.events@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/ngraph.events/-/ngraph.events-1.2.2.tgz#3ceb92d676a04a4e7ce60a09fa8e17a4f0346d7f"
+ integrity sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==
+
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -9895,6 +10178,15 @@ pako@~1.0.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+panzoom@^9.4.3:
+ version "9.4.3"
+ resolved "https://registry.yarnpkg.com/panzoom/-/panzoom-9.4.3.tgz#195c4031bb643f2e6c42f1de0ca87cc10e224042"
+ integrity sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==
+ dependencies:
+ amator "^1.1.0"
+ ngraph.events "^1.2.2"
+ wheel "^1.0.0"
+
parallel-transform@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
@@ -11364,6 +11656,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
+robust-predicates@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
+ integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
+
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -11388,6 +11685,11 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
+rw@1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+ integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
rxjs@^6.6.0:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
@@ -13540,6 +13842,11 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
+wheel@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wheel/-/wheel-1.0.0.tgz#6cf46e06a854181adb8649228077f8b0d5c574ce"
+ integrity sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==
+
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"