mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2026-02-22 13:05:26 +01:00
312 lines
8.3 KiB
Vue
312 lines
8.3 KiB
Vue
<template>
|
|
<div v-if="playbook !== undefined" class="fill-height d-flex flex-column pa-8">
|
|
<v-alert v-if="readonly" type="info">You do not have write access to playbooks.</v-alert>
|
|
<h2 v-if="this.$route.params.id === 'new'">New Playbook</h2>
|
|
<h2 v-else>Edit Playbook: {{ playbook.name }}</h2>
|
|
|
|
<v-alert v-if="formaterrors.length" color="warning">
|
|
<div v-for="(formaterror, index) in formaterrors" :key="index">
|
|
{{ formaterror.instancePath }}{{ formaterror.instancePath ? ": " : "" }}{{ formaterror.message }}
|
|
</div>
|
|
</v-alert>
|
|
<v-alert v-else-if="error" color="warning">
|
|
{{ error }}
|
|
</v-alert>
|
|
<div v-else class="px-4 overflow-scroll">
|
|
<vue-pipeline
|
|
v-if="pipelineData"
|
|
ref="pipeline"
|
|
:x="50"
|
|
:y="55"
|
|
:data="pipelineData"
|
|
:showArrow="true"
|
|
:ystep="70"
|
|
:xstep="100"
|
|
lineStyle="default"
|
|
/>
|
|
</div>
|
|
|
|
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
|
|
Playbook
|
|
</v-subheader>
|
|
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
|
|
<Editor v-model="playbook.yaml" @input="updatePipeline" lang="yaml" :readonly="readonly"></Editor>
|
|
</div>
|
|
|
|
<v-row v-if="!readonly" class="px-3 my-6 flex-grow-0 flex-shrink-0">
|
|
<v-btn v-if="this.$route.params.id === 'new'" color="success" @click="save" outlined>
|
|
<v-icon>mdi-plus-thick</v-icon>
|
|
Create
|
|
</v-btn>
|
|
<v-btn v-else color="success" @click="save" outlined>
|
|
<v-icon>mdi-content-save</v-icon>
|
|
Save
|
|
</v-btn>
|
|
</v-row>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import Vue from "vue";
|
|
|
|
import {Playbook, PlaybookTemplate, Task, TaskResponse} from "../client";
|
|
import { API } from "@/services/api";
|
|
import Editor from "../components/Editor.vue";
|
|
import {alg, Graph} from "graphlib";
|
|
import yaml from 'yaml';
|
|
import Ajv from "ajv";
|
|
|
|
const playbookSchema = {
|
|
type: "object",
|
|
required: ["name", "tasks"],
|
|
properties: {
|
|
name: { type: "string" },
|
|
tasks: {
|
|
type: "object",
|
|
additionalProperties: { $ref: "#/definitions/Task" }
|
|
}
|
|
},
|
|
// additionalProperties: false,
|
|
$id: "#/definitions/Playbook"
|
|
};
|
|
|
|
const taskSchema = {
|
|
type: "object",
|
|
required: ["name", "type"],
|
|
properties: {
|
|
automation: { type: "string" },
|
|
join: { type: "boolean" },
|
|
msg: { type: "object", additionalProperties: { type: "string" } },
|
|
name: { type: "string" },
|
|
next: {
|
|
type: "object",
|
|
additionalProperties: { type: ["string", "null"] }
|
|
},
|
|
schema: { type: "object" },
|
|
type: { type: "string", enum: ["task", "input", "automation"] }
|
|
},
|
|
// additionalProperties: false,
|
|
$id: "#/definitions/Task"
|
|
};
|
|
|
|
interface State {
|
|
playbook?: PlaybookTemplate;
|
|
g: Record<string, any>;
|
|
selected: any;
|
|
pipelineData: any;
|
|
error: string;
|
|
}
|
|
|
|
interface TaskWithID {
|
|
id: string;
|
|
task: Task;
|
|
}
|
|
|
|
const inityaml = "name: VirusTotal hash check\n" +
|
|
"tasks:\n" +
|
|
" input:\n" +
|
|
" name: Please enter a word\n" +
|
|
" type: input\n" +
|
|
" schema:\n" +
|
|
" title: Word\n" +
|
|
" type: object\n" +
|
|
" properties:\n" +
|
|
" word:\n" +
|
|
" type: string\n" +
|
|
" title: Enter a Word\n" +
|
|
" default: \"\"\n" +
|
|
" next:\n" +
|
|
" hash: \"word != ''\"\n" +
|
|
"\n" +
|
|
" hash:\n" +
|
|
" name: Hash the word\n" +
|
|
" type: automation\n" +
|
|
" automation: hash.sha1\n" +
|
|
" msg:\n" +
|
|
" payload: \"playbook.tasks['input'].data['word']\"\n" +
|
|
" next:\n" +
|
|
" end:\n" +
|
|
"\n" +
|
|
" end:\n" +
|
|
" name: Finish the incident\n" +
|
|
" type: task\n"
|
|
|
|
export default Vue.extend({
|
|
name: "Playbook",
|
|
components: { Editor },
|
|
data: (): State => ({
|
|
playbook: undefined,
|
|
g: {},
|
|
selected: undefined,
|
|
pipelineData: undefined,
|
|
error: "",
|
|
}),
|
|
watch: {
|
|
'$route': function () {
|
|
this.loadPlaybook();
|
|
}
|
|
},
|
|
computed: {
|
|
formaterrors: function (): Array<any> {
|
|
if (!this.playbook) {
|
|
return [];
|
|
}
|
|
try {
|
|
let playbook = yaml.parse(this.playbook.yaml);
|
|
const ajv = new Ajv({validateFormats: false});
|
|
ajv.addSchema(taskSchema, "#/definitions/Task")
|
|
const validate = ajv.compile(playbookSchema);
|
|
const valid = validate(playbook)
|
|
if (!valid && validate.errors) {
|
|
return validate.errors;
|
|
}
|
|
return [];
|
|
}
|
|
catch (e) {
|
|
return [e];
|
|
}
|
|
},
|
|
readonly: function (): boolean {
|
|
return !this.hasRole("engineer:playbook:write");
|
|
},
|
|
},
|
|
methods: {
|
|
gstatus: function(task: TaskResponse) {
|
|
if (task.active) {
|
|
return "open"
|
|
}
|
|
return "inactive"
|
|
},
|
|
tasks: function(g: any, playbook: Playbook): Array<TaskWithID> {
|
|
let taskKeys = alg.topsort(g);
|
|
let tasks = [] as Array<TaskWithID>;
|
|
for (const tasksKey in taskKeys) {
|
|
let taskWithID = {} as TaskWithID;
|
|
if (playbook.tasks[taskKeys[tasksKey]] === undefined) {
|
|
continue; // TODO
|
|
}
|
|
taskWithID.task = playbook.tasks[taskKeys[tasksKey]];
|
|
taskWithID.id = taskKeys[tasksKey];
|
|
tasks.push(taskWithID);
|
|
}
|
|
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<any>;
|
|
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;
|
|
}
|
|
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(() => {
|
|
this.$store.dispatch("alertSuccess", { name: "Playbook saved" });
|
|
});
|
|
}
|
|
},
|
|
loadPlaybook() {
|
|
if (!this.$route.params.id) {
|
|
return
|
|
}
|
|
if (this.$route.params.id == 'new') {
|
|
this.playbook = { name: "MyPlaybook", yaml: inityaml }
|
|
} else {
|
|
API.getPlaybook(this.$route.params.id).then((response) => {
|
|
this.playbook = response.data;
|
|
});
|
|
}
|
|
},
|
|
hasRole: function (s: string): boolean {
|
|
if (this.$store.state.user.roles) {
|
|
return this.lodash.includes(this.$store.state.user.roles, s);
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadPlaybook();
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.my-code {
|
|
background: #2d2d2d;
|
|
color: #ccc;
|
|
|
|
width: inherit;
|
|
height: inherit;
|
|
|
|
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
padding: 15px;
|
|
}
|
|
|
|
.pipeline-node-label {
|
|
fill: #333 !important;
|
|
}
|
|
</style>
|