Release catalyst

This commit is contained in:
Jonas Plum
2021-12-13 00:39:15 +01:00
commit 15cf0ebd49
339 changed files with 111677 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
<template>
<v-container>
<v-card style="background-color: #fff;" class="mt-8">
<div class="swagger" id="swagger" ></div>
</v-card>
</v-container>
</template>
<script lang="ts">
import SwaggerUI from 'swagger-ui';
import 'swagger-ui/dist/swagger-ui.css';
import Vue from "vue";
import spec from '../../../generated/community.json';
export default Vue.extend({
name: "API",
components: {},
mounted() {
SwaggerUI({
spec: spec,
dom_id: '#swagger',
oauth2RedirectUrl: location.href
})
}
});
</script>
<style>
#swagger a,
#swagger h1,
#swagger h2,
#swagger h3,
#swagger h4,
#swagger h5,
#swagger h6 {
color: black !important
}
#swagger .info {
padding: 0 10px 10px;
border-radius: 4px;
}
#swagger .servers-title,
#swagger .servers {
display: none;
}
</style>
+264
View File
@@ -0,0 +1,264 @@
<template>
<v-container>
<h1>{{ name }}</h1>
<v-divider class="my-2"></v-divider>
<h2 class="text--disabled" style="font-size: 12pt" v-if="artifact">
Status:
<v-menu offset-y class="mr-2">
<template v-slot:activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
<v-icon small class="mr-1" :color="statusColor(artifact.status)">{{ statusIcon(artifact.status) }}</v-icon>
<span :class="statusColor(artifact.status) + '--text'">{{ artifact.status | capitalize }}</span>
</span>
</template>
<v-list>
<v-list-item dense link v-for="state in otherStates" :key="state.id" @click="setStatus(state.id)">
<v-list-item-title>
Set status to <v-icon small>{{ statusIcon(state.id) }}</v-icon> {{ state.name }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
&middot;
Type:
<v-menu
:close-on-content-click="false"
offset-y
class="mr-2">
<template v-slot:activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
{{ artifact.type ? artifact.type : "empty" }}
</span>
</template>
<v-combobox
class="pt-6 pb-0 px-3"
style="background-color: white"
v-model="artifacttype"
:items="['ip', 'domain', 'md5', 'sha1', 'sha256', 'filename', 'url']"
label="Artifact Type"
@change="setArtifactType"
></v-combobox>
</v-menu>
</h2>
<v-divider class="mt-0 mb-4"></v-divider>
<div v-if="automations">
<v-card
v-for="automation in artifactautomations" :key="automation.id" class="mb-4" elevation="0" outlined>
<v-card-title>
{{ automation.id }}
<v-spacer></v-spacer>
<v-btn
v-if="artifact && artifact.enrichments && automation.id in artifact.enrichments"
@click="enrich(automation.id)"
elevation="0"
>
<v-icon>mdi-sync</v-icon>
</v-btn>
<v-btn v-else @click="enrich(automation.id)" elevation="0">
<v-icon>mdi-play</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="artifact && artifact.enrichments && automation.id in artifact.enrichments && artifact.enrichments[automation.id].data">
<div class="template" v-if="artifact.enrichments[automation.id].html" v-html="artifact.enrichments[automation.id].html"></div>
<JSONHTML v-else :json="artifact.enrichments[automation.id].data"></JSONHTML>
</v-card-text>
</v-card>
</div>
<div v-if="artifact">
<div v-for="enrichment in artifact.enrichments" :key="enrichment.name" >
<v-card v-if="!hasautomation(enrichment.name)" outlined>
<v-card-title>
{{ enrichment.name }}
</v-card-title>
<v-card-text>
<vue-markdown>
{{ enrichment.data }}
</vue-markdown>
</v-card-text>
</v-card>
</div>
</div>
</v-container>
</template>
<script lang="ts">
import Vue from "vue";
import {
Artifact,
AutomationResponse, AutomationResponseTypeEnum, AutomationTypeEnum,
Type,
TypeColorEnum
} from "@/client";
import VueMarkdown from "vue-markdown";
import { API } from "@/services/api";
import JSONHTML from "../components/JSONHTML.vue";
interface State {
artifact?: Artifact;
automations?: Array<AutomationResponse>;
artifacttype: string;
}
export default Vue.extend({
name: "ArtifactPopup",
components: {
"vue-markdown": VueMarkdown,
JSONHTML,
},
props: ["id", "name"],
data: (): State => ({
artifact: undefined,
automations: undefined,
artifacttype: "unknown",
}),
watch: {
id: function(): void {
this.load();
},
name: function(): void {
this.load();
}
},
computed: {
artifactautomations: function (): Array<AutomationResponse> {
if (!this.automations) {
return [];
}
return this.lodash.filter(this.automations, (automation: AutomationResponse) => {
if (!automation || !automation.type) {
return true;
}
return this.lodash.includes(automation.type, AutomationResponseTypeEnum.Artifact)
})
},
ticketID(): number {
return parseInt(this.id, 10);
},
otherStates: function (): Array<Type> {
return this.lodash.filter(this.$store.state.settings.artifactStates, (state: Type) => {
if (!this.artifact || !this.artifact.status) {
return true;
}
return state.id !== this.artifact.status;
})
},
},
methods: {
setArtifactType() {
if (!this.artifact || !this.artifact.name || this.ticketID === undefined) {
return;
}
let artifact = this.artifact
artifact.type = this.artifacttype
API.setArtifact(this.ticketID, this.artifact.name, artifact).then((response) => {
this.$store.dispatch("alertSuccess", { name: "Artifact type changed", type: "success" })
if (response.data.artifacts) {
this.lodash.forEach(response.data.artifacts, (artifact) => {
if (artifact.name == this.name) {
this.artifact = artifact;
}
})
}
});
},
hasautomation(name: string): boolean {
let found = false;
this.lodash.forEach(this.automations, (automation) => {
if (automation.id === name) {
found = true
}
})
return found;
},
loadArtifact(id: number, artifact: string) {
API.getArtifact(id, artifact).then((response) => {
this.artifact = response.data;
});
},
loadAutomations() {
API.listAutomations().then((response) => {
this.automations = response.data;
});
},
enrich(automation: string) {
if (this.artifact === undefined || this.ticketID === undefined) {
return
}
API.runArtifact(this.ticketID, this.name, automation);
},
statusIcon: function (status: string): string {
let icon = "mdi-help";
this.lodash.forEach(this.$store.state.settings.artifactStates, (state: Type) => {
if (status === state.id) {
icon = state.icon;
}
})
return icon;
},
statusColor: function (status: string) {
let color = TypeColorEnum.Info;
this.lodash.forEach(this.$store.state.settings.artifactStates, (state: Type) => {
if (status === state.id && state.color) {
color = state.color
}
})
return color;
},
setStatus(status: string) {
if (!this.artifact || !this.artifact.name || this.ticketID === undefined) {
return;
}
let artifact = this.artifact
artifact.status = status
API.setArtifact(this.ticketID, this.artifact.name, artifact).then((response) => {
this.$store.dispatch("alertSuccess", { name: "Artifact status changed", type: "success" })
if (response.data.artifacts) {
this.lodash.forEach(response.data.artifacts, (artifact) => {
if (artifact.name == this.name) {
this.artifact = artifact;
}
})
}
});
},
load() {
this.loadArtifact(this.ticketID, this.name);
}
},
mounted(): void {
this.load();
this.loadAutomations();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (id === "tickets/" + this.ticketID) {
this.load();
}
});
})
},
});
</script>
<style>
.template {
overflow: auto;
}
.template table, .template th, .template td {
border: 1px solid #777 !important;
border-left: 0 !important;
border-right: 0 !important;
padding: 4px !important;
}
</style>
+131
View File
@@ -0,0 +1,131 @@
<template>
<div v-if="automation !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
<v-alert v-if="readonly" type="info">You do not have write access to automations.</v-alert>
<h2 v-if="readonly">Automation: {{ automation.id }}</h2>
<h2 v-else-if="this.$route.params.id === 'new'">New Automation: {{ automation.id }}</h2>
<h2 v-else>Edit Automation: {{ automation.id }}</h2>
<v-row class="flex-grow-0 flex-shrink-0">
<v-col cols="12">
<v-text-field :readonly="readonly" label="ID" v-model="automation.id" class="flex-grow-0 flex-shrink-0"></v-text-field>
<v-select
label="Type"
:items="types"
item-text="id"
return-object
multiple
v-model="automation.type"
></v-select>
<v-text-field :readonly="readonly" label="Docker Image" v-model="automation.image" class="flex-grow-0 flex-shrink-0"></v-text-field>
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Script</v-subheader>
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
<Editor v-model="automation.script" lang="python" :readonly="readonly" ></Editor>
</div>
<AdvancedJSONSchemaEditor
@save="save"
:schema="automation.schema ? JSON.parse(automation.schema) : {}"
hidepreview
class="mt-4"
:readonly="readonly"></AdvancedJSONSchemaEditor>
</v-col>
</v-row>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import Editor from "../components/Editor.vue";
import AdvancedJSONSchemaEditor from "../components/AdvancedJSONSchemaEditor.vue";
import {API} from "@/services/api";
import {AutomationResponse, AutomationForm, AutomationResponseTypeEnum} from "@/client";
import {DateTime} from "luxon";
interface State {
automation?: AutomationResponse;
data?: any;
valid: boolean;
}
export default Vue.extend({
name: "Automation",
components: { Editor, AdvancedJSONSchemaEditor },
data: (): State => ({
automation: undefined,
data: undefined,
valid: true,
}),
watch: {
$route: function () {
this.loadAutomation();
},
},
computed: {
readonly: function (): boolean {
return !this.hasRole("engineer:automation:write");
},
types: function (): Array<string> {
return [ AutomationResponseTypeEnum.Global, AutomationResponseTypeEnum.Playbook, AutomationResponseTypeEnum.Artifact ]
}
},
methods: {
save(schema) {
if (this.automation === undefined) {
return;
}
let automation = this.automation as any as AutomationForm;
automation.schema = schema;
if (this.$route.params.id == "new") {
API.createAutomation(automation);
} else {
API.updateAutomation(this.$route.params.id, automation);
}
},
loadAutomation() {
if (!this.$route.params.id) {
return;
}
if (this.$route.params.id == "new") {
this.automation = { id: "my-automation", image: "docker.io/ubuntu", script: "", type: [ AutomationResponseTypeEnum.Global, AutomationResponseTypeEnum.Playbook, AutomationResponseTypeEnum.Artifact ] };
} else {
API.getAutomation(this.$route.params.id).then((response) => {
this.automation = 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;
},
timeformat: function(s: string, locale: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
dateformat: function(s: string, locale: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
datetimeformat: function(s: string, locale: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
},
mounted() {
this.loadAutomation();
},
});
</script>
+64
View File
@@ -0,0 +1,64 @@
<template>
<v-main style="min-height: 100vh">
<List
:items="automations"
routername="Automation"
itemid="id"
itemname="id"
singular="Automation"
plural="Automations"
writepermission="engineer:automation:write"
@delete="deleteAutomation"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {AutomationResponse} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
automations: Array<AutomationResponse>;
}
export default Vue.extend({
name: "AutomationList",
components: {List},
data: (): State => ({
automations: [],
}),
methods: {
loadAutomations() {
API.listAutomations().then((response) => {
this.automations = response.data;
});
},
deleteAutomation(name: string) {
API.deleteAutomation(name).then(() => {
this.loadAutomations();
});
},
},
mounted() {
this.loadAutomations();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "automations/")) {
reload = true;
}
});
if (reload) {
this.loadAutomations()
}
})
},
});
</script>
+219
View File
@@ -0,0 +1,219 @@
<template>
<v-main>
<v-row>
<v-col v-if="statistics" cols="12" lg="7">
<v-row>
<v-col cols="4">
<v-subheader>Unassigned tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND !owner' }
}">
{{ statistics.unassigned }}
</router-link>
</span>
<v-subheader>Your tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND owner == \'' + $store.state.user.id + '\'' }
}">
{{ $store.state.user.id in statistics.open_tickets_per_user ? statistics.open_tickets_per_user[$store.state.user.id] : 0 }}
</router-link>
</span>
</v-col>
<v-col cols="8">
<v-subheader>Open tickets per owner</v-subheader>
<bar-chart
v-if="open_tickets_per_user"
:chart-data="open_tickets_per_user"
:styles="{
width: '100%',
'max-height': '400px',
position: 'relative'
}"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
onClick: clickUser,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
></bar-chart>
</v-col>
</v-row>
<v-row>
<v-col cols="7">
<v-subheader>Tickets created per week</v-subheader>
<line-chart
v-if="tickets_per_week"
:chart-data="tickets_per_week"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
}"
>
</line-chart>
</v-col>
<v-col cols="5">
<v-subheader>Ticket Types</v-subheader>
<pie-chart
v-if="tickets_per_type"
:chart-data="tickets_per_type"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
onClick: clickPie,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
>
</pie-chart>
</v-col>
</v-row>
</v-col>
<v-col cols="12" lg="5">
<TicketList :type="this.$route.params.type" @click="open"></TicketList>
</v-col>
</v-row>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import LineChart from "../components/charts/Line";
import BarChart from "../components/charts/Bar";
import PieChart from "../components/charts/Doughnut";
import { API } from "@/services/api";
import {Statistics, TicketResponse} from "@/client";
import {DateTime} from "luxon";
import { colors } from "@/plugins/vuetify";
import TicketList from "@/components/TicketList.vue";
import { createHash } from "crypto";
export default Vue.extend({
name: "Dashboard",
components: {
LineChart,
BarChart,
PieChart,
TicketList
},
data() {
return {
statistics: (undefined as unknown) as Statistics
};
},
computed: {
tickets_per_type: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.tickets_per_type, (count, type) => {
data.labels.push(type);
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(type));
})
return data
},
open_tickets_per_user: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.open_tickets_per_user, (count, user) => {
if (!user) {
data.labels.push("unassigned");
} else {
data.labels.push(user);
}
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(user));
})
return data
},
tickets_per_week: function () {
let data = {labels: [] as Array<string>, datasets: [{backgroundColor: [] as Array<string>, data: [] as Array<number> }]}
this.lodash.forEach(this.weeks(), (week) => {
data.labels.push(week);
if (week in this.statistics.tickets_per_week) {
data.datasets[0].data.push(this.statistics.tickets_per_week[week]);
} else {
data.datasets[0].data.push(0);
}
data.datasets[0].backgroundColor.push("#607d8b");
})
return data
}
},
methods: {
open: function (ticket: TicketResponse) {
if (ticket.id === undefined) {
return;
}
this.$router.push({
name: "Ticket",
params: {type: '-', id: ticket.id.toString()}
});
},
clickUser: function (evt, elem) {
let owner = this.open_tickets_per_user.labels[elem[0]._index];
let query = 'status == \'open\' AND owner == \'' + owner + '\'';
if (owner == 'unassigned') {
query = 'status == \'open\' AND !owner';
}
this.$router.push({
name: "TicketList",
params: {query: query}
});
},
clickPie: function (evt, elem) {
this.$router.push({
name: "TicketList",
params: {type: this.tickets_per_type.labels[elem[0]._index]}
});
},
color: function (s: string): string {
let pos = createHash('md5').update(s).digest().readUInt32BE(0) % colors.length;
return colors[pos];
},
fillData() {
API.getStatistics().then(response => {
this.statistics = response.data;
});
},
weeks: function () {
let w = [] as Array<string>;
for (let i = 0; i < 53; i++) {
w.push(DateTime.utc().minus({ weeks: i }).toFormat("kkkk-WW"))
}
this.lodash.reverse(w);
return w
}
},
mounted() {
this.fillData();
}
});
</script>
<style>
canvas {
position: relative !important;
margin: 0 auto;
}
</style>
+217
View File
@@ -0,0 +1,217 @@
<template>
<div class="fill-height">
<v-card
v-if="selected !== undefined"
class="mt-3 ml-3 px-0"
style="position: absolute; width: 33%; left: 60px; top: 60px; z-index: 5">
<v-card-title>
{{ selected.name }}
<v-spacer></v-spacer>
<v-btn
text
:to="{ name: 'Graph', params: { col: col(selected.id), 'id': id(selected.id) } }"
>
<v-icon>mdi-bullseye</v-icon>
</v-btn>
<v-btn
vv-if="selected.id.startsWith('artifacts/')"
text
@click="to(selected.id)"
>
<v-icon>mdi-open-in-new</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="ma-0 pa-0">
<!--TicketSnippet v-if="selected.id.startsWith('tickets/')"></TicketSnippet-->
<!--ArtifactSnippet v-if="selected.id.startsWith('artifacts/')"></ArtifactSnippet-->
<IDSnippet :id="selected.id"></IDSnippet>
</v-card-text>
</v-card>
<v-main class="fill-height overflow-hidden">
<div class="d-flex flex-column fill-height overflow-hidden">
<v-row class="flex-grow-0 pb-4">
<v-col cols="4" class="pl-8">
<v-slider
class="mt-4 mb-4"
v-model="depth"
dense
:label="'Depth ' + depth"
hide-details
max="10"
min="0"
></v-slider>
</v-col>
<v-col cols="4" class="pl-4">
<v-slider
class="mt-4 mb-4"
v-model="force"
dense
label="Node Distance"
hide-details
max="10000"
min="0"
></v-slider>
</v-col>
<v-col cols="2">
<v-switch label="Hide Labels"
v-model="hidelabel" class="mt-6 mb-4" hide-details>
</v-switch>
</v-col>
</v-row>
<v-row class="flex-grow-1 mt-0">
<d3-network
v-if="g !== undefined && g.links !== undefined"
:net-nodes="nodes"
:net-links="g.links"
:options="options"
@node-click="nodeclick"
class="pt-0 mt-n4"
style="width: 100%; height: 100%"
/>
</v-row>
</div>
</v-main>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import D3Network from "vue-d3-network";
import {API} from "../services/api";
import {Graph, Node} from "../client";
// import TicketSnippet from "../components/snippets/TicketSnippet.vue";
// import ArtifactSnippet from "../components/snippets/ArtifactSnippet.vue";
import IDSnippet from "../components/snippets/IDSnippet.vue";
interface State {
g?: Graph;
force: number;
depth: number;
hidelabel: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
selected?: any;
}
export default Vue.extend({
name: "Graph",
components: {
IDSnippet,
D3Network
},
data: (): State => ({
g: undefined,
force: 3000,
depth: 2,
hidelabel: false,
selected: undefined,
}),
computed: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: function (): any {
return {
canvas: false,
nodeLabels: !this.hidelabel,
nodeSize: 20,
linkWidth: 2,
fontSize: 16,
force: this.force
}
},
nodes: function (): Array<any> {
if (this.g === undefined || this.g.nodes === undefined) {
return []
}
return this.lodash.map(this.g.nodes, (node: Node) => {
if (node.id === this.$route.params.col + "/" + this.$route.params.id) {
return {id: node.id, name: node.name, _size: 40, _cssClass: "center", _labelClass: "center"}
}
if (node.id.startsWith("tickets/")) {
return {id: node.id, name: node.name, _size: 30, _cssClass: "ticket", _labelClass: "event"}
}
return {id: node.id, name: node.name}
})
}
},
watch: {
depth: function () {
this.fetchGraph();
},
$route: function () {
this.fetchGraph();
},
},
methods: {
col: function (id: string): string {
let parts = id.split("/");
return parts[0];
},
id: function (id: string): string {
let parts = id.split("/");
return parts[1];
},
to: function (fid: string) {
let col = this.col(fid);
let id = this.id(fid);
if (col === 'tickets') {
this.$router.push({
name: "Ticket",
params: { id: id, type: "-" }
});
return
}
this.$router.push({
name: "Artifact",
params: { artifact: id }
});
},
fetchGraph: function(): void {
API.graph(this.$route.params.col, this.$route.params.id, this.depth).then((response) => {
this.g = response.data;
});
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nodeclick(e: any, node: any) {
this.selected = node;
}
},
mounted() {
this.fetchGraph();
},
});
</script>
<style>
.node,
.node.selected {
stroke: #388E3C !important;
}
.node.event {
stroke: #D32F2F !important;
}
.node.center {
stroke: #FFEB3B !important;
fill: #FFEB3B !important;
}
.theme--dark .node-label,
.theme--dark .node-label.event {
fill: #ffffff !important;
}
.node-label,
.node-label.event {
fill: #000000 !important;
}
.link {
stroke: #424242 !important;
}
.link.selected,
.link:hover,.node:hover{
stroke: #FFEB3B !important;
}
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<div></div>
</template>
<script>
export default {
name: "Group"
}
</script>
<style scoped>
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="groups"
routername="Group"
itemid="id"
itemname="name"
singular="Group"
plural="Groups"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {Group} from "../client";
import {API} from "../services/api";
import List from "../components/List.vue";
interface State {
groups: Array<Group>;
}
export default Vue.extend({
name: "GroupList",
components: {List},
data: (): State => ({
groups: [],
}),
methods: {
loadGroups() {
API.listGroups().then((response) => {
this.groups = response.data;
});
},
deleteGroup(name: string) {
// API.deleteU(name).then(() => {
// this.loadAutomations();
// });
},
},
mounted() {
this.loadGroups();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "groups/")) {
reload = true;
}
});
if (reload) {
this.loadGroups()
}
})
},
});
</script>
+186
View File
@@ -0,0 +1,186 @@
<template>
<div>
<div v-if="$route.params.id == 'new'" class="fill-height d-flex flex-column pa-8">
<v-alert v-if="readonly" type="info">
You do not have write access to jobs.
</v-alert>
<h2>Start new job</h2>
<v-select
label="Automations"
:items="globalautomations"
item-text="id"
return-object
v-model="automation"
></v-select>
<v-form v-if="automation" v-model="valid">
<v-jsf
v-model="data"
:schema="JSON.parse(automation.schema)"
:options="{ readonly: true, formats: { time: timeformat, date: dateformat, 'date-time': datetimeformat } }"
/>
<v-btn @click="run" color="success" outlined :disabled="!valid">
Run
</v-btn>
</v-form>
</div>
<div v-else-if="job !== undefined">
<h2>Job: {{ job.id }}</h2>
<div v-if="job.automation" class="flex-grow-0 flex-shrink-0">
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Automation</v-subheader>
{{ job.automation }}
</div>
<div v-if="job.container" class="flex-grow-0 flex-shrink-0">
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Container</v-subheader>
{{ job.container }}
</div>
<div v-if="job.status" class="flex-grow-0 flex-shrink-0">
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Status</v-subheader>
{{ job.status }}
</div>
<div v-if="job.payload" class="flex-grow-0 flex-shrink-0">
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Input Payload</v-subheader>
<Editor :value="JSON.stringify(job.payload, null, 2)" lang="json" :readonly="true"></Editor>
</div>
<div v-if="job.log" class="flex-grow-0 flex-shrink-0">
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Log</v-subheader>
<Editor :value="job.log" lang="log" :readonly="true"></Editor>
</div>
<div v-if="job.output" class="flex-grow-0 flex-shrink-0">
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">Output</v-subheader>
<Editor :value="JSON.stringify(job.output, null, 2)" lang="json" :readonly="true"></Editor>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import {AutomationResponse, AutomationResponseTypeEnum, JobResponse} from "@/client";
import {API} from "@/services/api";
import {DateTime} from "luxon";
import Editor from "@/components/Editor.vue";
interface State {
job?: JobResponse,
data?: any;
valid: boolean;
automation?: AutomationResponse;
automations: Array<AutomationResponse>;
}
export default Vue.extend({
name: "Job",
components: { Editor },
data: (): State => ({
job: undefined,
data: undefined,
valid: true,
automation: undefined,
automations: [],
}),
watch: {
$route: 'loadJob',
},
computed: {
readonly: function (): boolean {
return !this.hasRole("admin:job:write");
},
globalautomations: function (): Array<AutomationResponse> {
if (!this.automations) {
return [];
}
return this.lodash.filter(this.automations, (automation: AutomationResponse) => {
if (!automation || !automation.type) {
return true;
}
return this.lodash.includes(automation.type, AutomationResponseTypeEnum.Global)
})
},
},
methods: {
timeformat: function(s: string, locale: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
dateformat: function(s: string, locale: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
datetimeformat: function(s: string, locale: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
run: function () {
if (!this.automation) {
return;
}
API.runJob({ automation: this.automation.id, payload: this.data }).then(() => {
this.$store.dispatch("alertSuccess", { name: "Job started." });
})
},
loadJob() {
if (!this.$route.params.id) {
return
}
if (this.$route.params.id == 'new') {
// this.template = { name: "MyTemplate", schema: '{ "type": "object", "name": "Incident" }' }
// this.schema = { type: "object", name: "Incident" }
} else {
API.getJob(this.$route.params.id).then((response) => {
this.job = response.data;
});
}
},
loadAutomations() {
API.listAutomations(this.$route.params.id).then((response) => {
this.automations = 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.loadJob();
this.loadAutomations();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "jobs/")) {
reload = true;
}
});
if (reload) {
this.loadJob()
}
})
},
});
</script>
+61
View File
@@ -0,0 +1,61 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="jobs"
routername="Job"
itemid="id"
itemname="id"
singular="Job"
plural="Jobs"
writepermission="admin:job:write"
:deletable="false"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {JobResponse} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
jobs: Array<JobResponse>;
}
export default Vue.extend({
name: "JobList",
components: {List},
data: (): State => ({
jobs: [],
}),
methods: {
loadJobs() {
API.listJobs().then((response) => {
if (response.data) {
this.jobs = response.data;
}
});
},
},
mounted() {
this.loadJobs();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "jobs/")) {
reload = true;
}
});
if (reload) {
this.loadJobs()
}
})
},
});
</script>
+311
View File
@@ -0,0 +1,311 @@
<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>
+66
View File
@@ -0,0 +1,66 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="playbooks"
routername="Playbook"
itemid="id"
itemname="name"
singular="Playbook"
plural="Playbooks"
@delete="deletePlaybook"
writepermission="engineer:playbook:write"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {PlaybookTemplate} from "../client";
import {API} from "../services/api";
import List from "../components/List.vue";
interface State {
playbooks: Array<PlaybookTemplate>;
}
export default Vue.extend({
name: "PlaybookList",
components: {List},
data: (): State => ({
playbooks: [],
}),
methods: {
loadPlaybooks() {
API.listPlaybooks().then((response) => {
if (response.data) {
this.playbooks = response.data;
}
});
},
deletePlaybook(name: string) {
API.deletePlaybook(name).then(() => {
this.loadPlaybooks();
});
},
},
mounted() {
this.loadPlaybooks();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "playbooks/")) {
reload = true;
}
});
if (reload) {
this.loadPlaybooks()
}
})
},
});
</script>
+47
View File
@@ -0,0 +1,47 @@
<template>
<v-main v-if="userdata">
<user-data-editor :userdata="userdata" @save="saveUserData"></user-data-editor>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import { UserData } from "@/client";
import { API } from "@/services/api";
import UserDataEditor from "@/components/UserDataEditor.vue";
interface State {
userdata?: UserData;
}
export default Vue.extend({
name: "Profile",
data: (): State => ({
userdata: undefined,
}),
components: {
UserDataEditor,
},
watch: {
$route: function () {
this.loadUserData();
},
},
methods: {
saveUserData: function(userdata: UserData) {
API.updateCurrentUserData(userdata).then(() => {
this.$store.dispatch("alertSuccess", { name: "User data saved" });
});
},
loadUserData: function () {
API.currentUserData().then((response) => {
this.userdata = response.data;
});
}
},
mounted() {
this.loadUserData();
}
});
</script>
+89
View File
@@ -0,0 +1,89 @@
<template>
<div v-if="rule !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
<h2 v-if="this.$route.params.id === 'new'">New Rule: {{ rule.name }}</h2>
<h2 v-else>Edit Rule: {{ rule.name }}</h2>
<v-text-field label="Name" v-model="rule.name" class="flex-grow-0 flex-shrink-0"></v-text-field>
<v-text-field label="Condition" v-model="rule.condition" class="flex-grow-0 flex-shrink-0"></v-text-field>
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
Update
</v-subheader>
<div class="flex-grow-1 flex-shrink-1 overflow-scroll">
<Editor v-model="update" lang="json"></Editor>
</div>
<v-row 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" outlined @click="save">
<v-icon>mdi-content-save</v-icon>
Save
</v-btn>
</v-row>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { Rule } from "@/client";
import { API } from "@/services/api";
import Editor from "../components/Editor.vue";
interface State {
rule?: Rule;
update: string;
}
export default Vue.extend({
name: "Rule",
components: { Editor },
data: (): State => ({
rule: undefined,
update: "",
}),
watch: {
$route: function () {
this.loadRule();
},
},
methods: {
save() {
if (this.rule === undefined) {
return;
}
let rule = this.rule;
// rule.id = kebabCase(rule.name);
rule.update = JSON.parse(this.update);
if (this.$route.params.id == 'new') {
API.createRule(rule).then(() => {
this.$store.dispatch("alertSuccess", { name: "Rule created" });
});
} else {
API.updateRule(this.$route.params.id, rule).then(() => {
this.$store.dispatch("alertSuccess", { name: "Rule saved" });
});
}
},
loadRule() {
if (!this.$route.params.id) {
return
}
if (this.$route.params.id == 'new') {
this.rule = { name: "MyPlaybook", condition: "type == alert", update: { details: { status: "ignored" } } }
} else {
API.getRule(this.$route.params.id).then((response) => {
this.rule = response.data;
this.update = JSON.stringify(response.data.update, null, 4)
});
}
},
},
mounted() {
this.loadRule();
},
});
</script>
+63
View File
@@ -0,0 +1,63 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="rules"
routername="Rule"
itemid="id"
itemname="name"
singular="Rule"
plural="Rules"
@delete="deleteRule"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {Rule} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
rules: Array<Rule>;
}
export default Vue.extend({
name: "RuleList",
components: {List},
data: (): State => ({
rules: [],
}),
methods: {
loadRules() {
API.listRules().then((response) => {
this.rules = response.data;
});
},
deleteRule(name: string) {
API.deleteRule(name).then(() => {
this.loadRules();
});
},
},
mounted() {
this.loadRules();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "rules/")) {
reload = true;
}
});
if (reload) {
this.loadRules()
}
})
},
});
</script>
+85
View File
@@ -0,0 +1,85 @@
<template>
<v-main>
<v-container>
<v-data-table
:headers="headers"
:items="tasks ? tasks : []"
item-key="name"
multi-sort
class="elevation-1 cards clickable"
:loading="loading"
:footer-props="{ 'items-per-page-options': [10, 25, 50, 100] }"
@click:row="open"
>
</v-data-table>
</v-container>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {TaskResponse, TaskWithContext} from "@/client";
import {API} from "@/services/api";
interface State {
tasks: Array<TaskResponse>;
loading: boolean;
}
export default Vue.extend({
name: "TaskList",
data: (): State => ({
tasks: [],
loading: true,
}),
watch: {
$route: function () {
this.loadTasks();
},
},
computed: {
headers() {
return [
{
text: "Ticket",
align: "start",
value: "ticket_name"
},
{
text: "Playbook",
align: "start",
value: "playbook_name"
},
{
text: "Task",
align: "start",
value: "task.name"
},
{
text: "Owner",
align: "start",
value: "task.owner"
}
];
}
},
methods: {
open(task: TaskWithContext) {
this.$router.push({
name: "Ticket",
params: { id: task.ticket_id.toString(), type: "-" }
});
},
loadTasks() {
this.loading = true;
API.listTasks().then((reponse) => {
this.tasks = reponse.data;
this.loading = false;
})
}
},
mounted() {
this.loadTasks();
}
});
</script>
+87
View File
@@ -0,0 +1,87 @@
<template>
<div v-if="template !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
<v-alert v-if="readonly" type="info">
You do not have write access to templates.
Changes here cannot be saved.
</v-alert>
<div class="d-flex" style="align-items: center">
<h2 v-if="readonly">Template: {{ template.name }}</h2>
<h2 v-else-if="this.$route.params.id === 'new'">New Template: {{ template.name }}</h2>
<h2 v-else>Edit Template: {{ template.name }}</h2>
</div>
<v-text-field label="Name" v-model="template.name" class="flex-grow-0 flex-shrink-0" :readonly="readonly"></v-text-field>
<AdvancedJSONSchemaEditor v-if="schema" @save="save" :schema="schema" :readonly="readonly"></AdvancedJSONSchemaEditor>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { TicketTemplate } from "@/client";
import AdvancedJSONSchemaEditor from "../components/AdvancedJSONSchemaEditor.vue";
interface State {
template?: TicketTemplate,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema?: any;
}
export default Vue.extend({
name: "Template",
components: { AdvancedJSONSchemaEditor },
data: (): State => ({
template: undefined,
schema: undefined,
}),
watch: {
$route: 'loadTemplate',
},
computed: {
readonly: function (): boolean {
return !this.hasRole("engineer:template:write");
},
},
methods: {
save(schema) {
if (this.template === undefined) {
return;
}
let template = this.template;
template.schema = schema;
if (this.$route.params.id == 'new') {
this.$store.dispatch("addTemplate", template).then(() => {
this.$router.push({name: "TemplateList" });
});
} else {
this.$store.dispatch("updateTemplate", { id: this.$route.params.id, template: template });
}
},
loadTemplate() {
if (!this.$route.params.id) {
return
}
if (this.$route.params.id == 'new') {
this.template = { name: "MyTemplate", schema: '{ "type": "object", "name": "Incident" }' }
this.schema = { type: "object", name: "Incident" }
} else {
this.$store.dispatch("getTemplate", this.$route.params.id).then((response: TicketTemplate) => {
this.template = response;
this.schema = JSON.parse(response.schema);
});
}
},
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.loadTemplate();
},
});
</script>
+50
View File
@@ -0,0 +1,50 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="$store.state.templates.templates"
routername="Template"
itemid="id"
itemname="name"
singular="Template"
plural="Templates"
writepermission="engineer:template:write"
@delete="deleteTemplate"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import List from "../components/List.vue";
export default Vue.extend({
name: "TemplateList",
components: {List},
methods: {
deleteTemplate(title: string) {
this.$store.dispatch("deleteTemplate", title).then(() => {
this.$router.push({name: "TemplateList"});
});
}
},
mounted() {
this.$store.dispatch("listTemplates");
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "templates/")) {
reload = true;
}
});
if (reload) {
this.$store.dispatch("listTemplates");
}
})
}
});
</script>
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
<template>
<v-main>
<TicketListComponent :type="this.$route.params.type" :query="query" @click="open"></TicketListComponent>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {TicketResponse} from "@/client";
import TicketListComponent from "../components/TicketList.vue";
export default Vue.extend({
name: "TicketList",
components: {TicketListComponent},
props: ['query'],
methods: {
open: function (ticket: TicketResponse) {
if (ticket.id === undefined) {
return;
}
this.$router.push({
name: "Ticket",
params: {type: this.$route.params.type, id: ticket.id.toString()}
});
}
}
});
</script>
+170
View File
@@ -0,0 +1,170 @@
<template>
<v-main>
<v-container>
<v-card color="cards">
<v-card-title>New {{ $route.params.type | capitalize }}</v-card-title>
<v-card-text>
<v-form class="create clearfix">
<v-text-field label="Title" v-model="name"></v-text-field>
<v-select
label="Playbooks"
:items="playbooks"
item-text="name"
return-object
multiple
v-model="selectedPlaybooks"
></v-select>
<v-select
label="Template"
:items="templates"
item-text="name"
return-object
v-model="selectedTemplate"
></v-select>
<v-subheader class="pl-0 mt-4" style="height: 20px">Details</v-subheader>
<div v-if="selectedTemplate !== undefined" class="details">
<v-lazy>
<v-jsf
v-model="details"
:options="{ fieldProps: { 'hide-details': true } }"
:schema="selectedSchema"
/>
</v-lazy>
</div>
<v-btn color="green" @click="createTicket" outlined>
Create
</v-btn>
</v-form>
</v-card-text>
</v-card>
</v-container>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {
PlaybookTemplateForm,
PlaybookTemplateResponse,
TicketForm,
TicketTemplateResponse,
TicketType
} from "../client";
import { API } from "@/services/api";
interface State {
selectedTemplate?: TicketTemplateResponse;
templates: Array<TicketTemplateResponse>;
selectedPlaybooks: Array<PlaybookTemplateResponse>;
playbooks: Array<PlaybookTemplateResponse>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
details: any;
name: string;
ticketType?: TicketType;
}
export default Vue.extend({
name: "TicketNew",
// components: { VJsf },
data: (): State => ({
selectedTemplate: undefined,
templates: [],
selectedPlaybooks: [],
playbooks: [],
name: "",
details: {},
ticketType: undefined,
}),
computed: {
selectedSchema() {
if (
this.selectedTemplate === undefined ||
this.selectedTemplate.schema === undefined
) {
return {};
}
return JSON.parse(this.selectedTemplate.schema);
}
},
methods: {
createTicket: function() {
if (
this.selectedTemplate === undefined ||
this.selectedTemplate?.schema === undefined
) {
this.$store.dispatch("alertError", { name: "No template selected" });
return;
}
let ticket: TicketForm = {
type: this.$route.params.type,
name: this.name,
details: this.details,
schema: this.selectedTemplate?.schema,
status: "open",
owner: this.$store.state.user.id,
playbooks: this.selectedPlaybooks as Array<PlaybookTemplateForm>,
};
API.createTicket(ticket).then(response => {
if (response.data.id === undefined) {
return;
}
this.$router.push({
name: "Ticket",
params: {
id: response.data.id.toString(),
type: response.data.type,
}
});
});
}
},
mounted() {
API.getTicketType(this.$route.params.type).then((response) => {
this.ticketType = response.data;
API.listTemplates().then(response => {
this.templates = response.data;
this.lodash.forEach(this.templates, (template) => {
if (this.ticketType && template.id == this.ticketType.default_template) {
this.selectedTemplate = template;
}
});
});
API.listPlaybooks().then(response => {
this.playbooks = response.data;
this.lodash.forEach(this.playbooks, (playbook) => {
if (this.ticketType && this.lodash.includes(this.ticketType.default_playbooks, playbook.id)) {
this.selectedPlaybooks.push(playbook);
}
});
});
});
},
});
</script>
<style>
.details > .col > .row > .row,
.details > .col > .row > div > .row {
margin-top: 16px !important;
padding: 16px !important;
background-color: rgba(0, 0, 0, 0.2);
}
.v-application .create .vjsf-property {
margin-bottom: 16px;
}
.v-application .create .error--text {
color: white !important;
}
</style>
+126
View File
@@ -0,0 +1,126 @@
<template>
<div v-if="tickettype !== undefined" class="flex-grow-1 flex-column d-flex fill-height pa-8">
<h2 v-if="this.$route.params.id === 'new'">New Ticket Type: {{ tickettype.name }}</h2>
<h2 v-else>Edit Ticket Type: {{ tickettype.name }}</h2>
<v-text-field label="Name" v-model="tickettype.name" class="flex-grow-0 flex-shrink-0"></v-text-field>
<v-subheader class="pl-0 py-0" style="height: 20px; font-size: 12px">
Icon (see <a href="https://materialdesignicons.com" class="mx-1"> materialdesignicons.com </a> and prefix with "mdi-")
</v-subheader>
<v-text-field v-model="tickettype.icon" placeholder="e.g. mdi-alert" class="flex-grow-0 flex-shrink-0 mt-n3"></v-text-field>
<v-select
v-model="selectedTemplate"
:items="templates"
item-text="name"
label="Default Template"
return-object
class="flex-grow-0 flex-shrink-0"
></v-select>
<v-select
v-model="selectedPlaybooks"
:items="playbooks"
item-text="name"
label="Default Playbooks"
return-object
multiple
class="flex-grow-0 flex-shrink-0"
></v-select>
<v-row 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" outlined @click="save">
<v-icon>mdi-content-save</v-icon>
Save
</v-btn>
</v-row>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { PlaybookTemplateResponse, TicketTemplateResponse, TicketType } from "@/client";
import { API } from "@/services/api";
interface State {
tickettype?: TicketType;
selectedPlaybooks: Array<PlaybookTemplateResponse>;
playbooks?: Array<PlaybookTemplateResponse>;
selectedTemplate?: TicketTemplateResponse;
templates?: Array<TicketTemplateResponse>;
}
export default Vue.extend({
name: "TicketType",
data: (): State => ({
tickettype: undefined,
selectedPlaybooks: [],
playbooks: undefined,
selectedTemplate: undefined,
templates: undefined,
}),
watch: {
$route: function () {
this.loadRule();
},
},
methods: {
save() {
if (this.tickettype === undefined) {
return;
}
let tickettype = this.tickettype;
if (this.selectedTemplate) {
tickettype.default_template = this.selectedTemplate.id;
}
tickettype.default_playbooks = [];
this.lodash.forEach(this.selectedPlaybooks, (playbook) => {
tickettype.default_playbooks.push(playbook.id)
})
if (this.$route.params.id == 'new') {
API.createTicketType(tickettype).then(() => {
this.$store.dispatch("alertSuccess", { name: "Ticket type created" });
});
} else {
API.updateTicketType(this.$route.params.id, tickettype).then(() => {
this.$store.dispatch("alertSuccess", { name: "Ticket type saved" });
});
}
},
loadRule() {
if (!this.$route.params.id) {
return
}
API.listTemplates().then((response) => {
this.templates = response.data;
if (response.data.length > 0) {
this.selectedTemplate = response.data[0];
}
})
API.listPlaybooks().then((response) => {
this.playbooks = response.data;
})
if (this.$route.params.id == 'new') {
this.tickettype = { name: "TI Ticket", default_groups: [], default_playbooks: [], default_template: "", icon: "" }
} else {
API.getTicketType(this.$route.params.id).then((response) => {
this.tickettype = response.data;
});
}
},
},
mounted() {
this.loadRule();
},
});
</script>
+64
View File
@@ -0,0 +1,64 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="tickettypes"
routername="TicketType"
itemid="id"
itemname="name"
singular="Ticket Type"
plural="Ticket Types"
@delete="deleteTicketType"
writepermission="engineer:tickettype:write"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import { TicketType} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
tickettypes: Array<TicketType>;
}
export default Vue.extend({
name: "TicketTypeList",
components: {List},
data: (): State => ({
tickettypes: [],
}),
methods: {
loadTicketType() {
API.listTicketTypes().then((response) => {
this.tickettypes = response.data;
});
},
deleteTicketType(id: string) {
API.deleteTicketType(id).then(() => {
this.loadTicketType();
});
},
},
mounted() {
this.loadTicketType();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "tickettypes/")) {
reload = true;
}
});
if (reload) {
this.loadTicketType()
}
})
},
});
</script>
+105
View File
@@ -0,0 +1,105 @@
<template>
<div>
<div v-if="user === undefined" class="text-sm-center py-16">
<v-progress-circular
indeterminate
color="primary"
:size="70"
:width="7"
class="align-center"
>
</v-progress-circular>
</div>
<div v-else class="fill-height d-flex flex-column pa-8">
<div v-if="$route.params.id === 'new'">
<h2>Create new API key</h2>
<v-form>
<v-text-field label="ID" v-model="user.id" hide-details></v-text-field>
<v-select multiple chips label="Roles" v-model="user.roles" :items="$store.state.settings.roles"></v-select>
<v-btn @click="save" color="success" outlined>
<v-icon>mdi-plus-thick</v-icon>
Create API-Key
</v-btn>
</v-form>
<v-alert v-if="newUserResponse" color="warning" class="mt-4" dismissible>
<b>New API-Secret:</b> {{ newUserResponse.secret }}<br>
Make sure you save it - you won't be able to access it again.
</v-alert>
</div>
<div v-else>
<h2>
{{ user.id }}
<span v-if="user.apikey">(API Key)</span>
</h2>
<v-select multiple chips v-if="!user.apikey" label="Roles" v-model="user.roles" :items="$store.state.settings.roles"></v-select>
<div v-else>
<v-chip v-for="role in user.roles" :key="role">{{ role }}</v-chip>
</div>
<v-btn v-if="!user.apikey" @click="save" color="success" outlined>
<v-icon>mdi-content-save</v-icon>
Save
</v-btn>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { NewUserResponse, UserResponse } from "../client";
import {API} from "@/services/api";
interface State {
user?: UserResponse;
newUserResponse?: NewUserResponse;
}
export default Vue.extend({
name: "User",
components: {},
data: (): State => ({
user: undefined,
newUserResponse: undefined
}),
watch: {
'$route': function () {
this.loadUser();
}
},
methods: {
save() {
if (!this.user) {
return
}
if (this.$route.params.id == 'new') {
API.createUser(this.user).then(response => {
this.newUserResponse = response.data;
})
} else {
API.updateUser(this.$route.params.id, this.user).then(response => {
this.user = response.data;
})
}
},
loadUser() {
if (!this.$route.params.id) {
return
}
if (this.$route.params.id == 'new') {
this.user = {id: "", roles: [], blocked: false, apikey: true}
} else {
API.getUser(this.$route.params.id).then((response) => {
this.user = response.data;
});
}
},
},
mounted() {
this.loadUser();
},
});
</script>
+44
View File
@@ -0,0 +1,44 @@
<template>
<div v-if="userdata">
<user-data-editor :userdata="userdata" @save="saveUserData"></user-data-editor>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { UserData } from "@/client";
import { API } from "@/services/api";
import UserDataEditor from "@/components/UserDataEditor.vue";
interface State {
userdata?: UserData;
}
export default Vue.extend({
name: "UserData",
data: (): State => ({
userdata: undefined,
}),
components: { UserDataEditor },
watch: {
$route: function () {
this.loadUserData();
},
},
methods: {
saveUserData: function(userdata: UserData) {
API.updateUserData(this.$route.params.id, userdata).then(() => {
this.$store.dispatch("alertSuccess", { name: "User data saved" });
});
},
loadUserData: function () {
API.getUserData(this.$route.params.id).then((response) => {
this.userdata = response.data;
});
}
},
mounted() {
this.loadUserData();
}
});
</script>
+60
View File
@@ -0,0 +1,60 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="userdatas"
routername="UserData"
itemid="id"
itemname="name"
singular="User Data"
plural="User Data"
:show-new="false"
:deletable="false"
writepermission="admin:userdata:write"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {UserData} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
userdatas: Array<UserData>;
}
export default Vue.extend({
name: "UserDataList",
components: {List},
data: (): State => ({
userdatas: [],
}),
methods: {
loadSettings() {
API.listUserData().then((response) => {
this.userdatas = response.data;
});
},
},
mounted() {
this.loadSettings();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "settings/")) {
reload = true;
}
});
if (reload) {
this.loadSettings()
}
})
},
});
</script>
+66
View File
@@ -0,0 +1,66 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="users"
routername="User"
itemid="id"
itemname="id"
singular="User / API Key"
plural="Users / API Keys"
@delete="deleteUser"
writepermission="admin:user:write"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {UserResponse} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
users: Array<UserResponse>;
}
export default Vue.extend({
name: "UserList",
components: {List},
data: (): State => ({
users: [],
}),
methods: {
loadUsers() {
API.listUsers().then((response) => {
if (response.data) {
this.users = response.data;
}
});
},
deleteUser(name: string) {
API.deleteUser(name).then(() => {
this.loadUsers();
});
},
},
mounted() {
this.loadUsers();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "users/")) {
reload = true;
}
});
if (reload) {
this.loadUsers()
}
})
},
});
</script>
+21
View File
@@ -0,0 +1,21 @@
<template>
<object data="http://localhost:8003">
<embed src="http://localhost:8003" />
Error: Embedded data could not be displayed.
</object>
</template>
<script>
export default {
name: "Arango",
};
</script>
<style scoped>
object,
embed {
padding-left: 56px;
height: 100%;
width: 100%;
}
</style>
+21
View File
@@ -0,0 +1,21 @@
<template>
<object data="http://localhost:9001/keygen">
<embed src="http://localhost:9001/keygen" />
Error: Embedded data could not be displayed.
</object>
</template>
<script>
export default {
name: "Emitter",
};
</script>
<style scoped>
object,
embed {
padding-left: 56px;
height: 100%;
width: 100%;
}
</style>
+21
View File
@@ -0,0 +1,21 @@
<template>
<object data="http://localhost:9000/minio">
<embed src="http://localhost:9000/minio" />
Error: Embedded data could not be displayed.
</object>
</template>
<script>
export default {
name: "Minio",
};
</script>
<style scoped>
object,
embed {
padding-left: 56px;
height: 100%;
width: 100%;
}
</style>
+21
View File
@@ -0,0 +1,21 @@
<template>
<object data="http://localhost:1880">
<embed src="http://localhost:1880" />
Error: Embedded data could not be displayed.
</object>
</template>
<script>
export default {
name: "Nodered",
};
</script>
<style scoped>
object,
embed {
padding-left: 56px;
height: 100%;
width: 100%;
}
</style>