blob: 5a974759798c2265f8675707bf8e0e7034234392 [file]
<div id="operationsPage" x-data="alpineOperations()" x-init="initOperations()"
xmlns="http://www.w3.org/1999/html">
<div>
<div>
<div x-ref="operationsHeader">
<h2>Operations</h2>
</div>
<hr>
</div>
<form>
<div id="select-operation" class="field has-addons">
<label class="label">Select an operation &nbsp;&nbsp;&nbsp;</label>
<div class="control is-expanded">
<div class="select is-small is-fullwidth">
<select x-on:change="getOperations(); getOperation();" x-model="selectedOperationID">
<option value="" default disabled selected>Select one...</option>
<template x-for="op in operations" :key="op.id">
<option x-bind:value="op.id"
x-bind:title="op.name + ' has ' + op.chain.length + ' decisions'">
<span x-text="op.name"></span>
<span x-text="' - ' + op.chain.length + ' decisions | ' + getHumanFriendlyTimeISO8601(op.start)"></span>
</option>
</template>
</select>
</div>
</div>
<div class="control">
<button type="button" class="button is-primary is-small"
x-on:click="openOperationCreateModal()">
<em class="pr-1 fas fa-plus"></em>
Create Operation
</button>
</div>
</div>
</form>
<template x-if="selectedOperation && selectedOperationID">
<div class="section p-2 pb-2">
<div class="control-buttons is-flex is-flex-direction-row is-justify-content-space-between">
<div class="buttons">
<button class="button is-small" x-on:click="openModal='showOperationDetails'" title="view operation details">Operation Details</button>
<button title="download reports" class="button is-primary is-small" x-on:click="openModal = 'showDownloadMenu'">
<span class="icon">
<em class="fas fa-arrow-down"></em>
</span>
<span>Download</span>
</button>
<button type="button" title="delete operation" class="button is-danger is-outlined is-small" x-on:click="openModal = 'deleteOperationModal'">
<span class="icon">
<em class="fas fa-trash"></em>
</span>
<span>Delete</span>
</button>
<div class="vr"></div>
<span>Current state: <span class="level-item tag is-medium m-0"><span
class="has-text-success" x-text="selectedOperation.state"></span></span></span>
</div>
<!-- OPERATION CONTROLS -->
<div class="controls-container is-flex is-justify-content-center is-align-items-center">
<template x-if="isOperationRunning">
<div class="is-flex is-flex-direction-column is-align-items-center">
<button x-bind:disabled="!isOperationRunning" class="button no-style is-large"
x-on:click="updateOperation('state', 'cleanup')" id="op-stop-button"><em
title="stop" class="fas fa-stop"></em></button>
<label class="label is-small" for="op-stop-button">Stop</label>
</div>
</template>
<template x-if="selectedOperation.state === 'running' && isOperationRunning">
<div class="is-flex is-flex-direction-column is-align-items-center">
<button x-bind:disabled="!isOperationRunning"
class="button no-style is-larger" id="op-pause-button"
x-on:click="updateOperation('state', 'paused')"><em
title="pause" class="fas fa-pause-circle"></em></button>
<label class="label is-small" for="op-pause-button">Pause</label>
</div>
</template>
<template x-if="selectedOperation.state !== 'running' && isOperationRunning">
<div class="is-flex is-flex-direction-column is-align-items-center">
<button x-bind:disabled="!isOperationRunning"
class="button no-style is-larger" id="op-run-button"
x-on:click="updateOperation('state', 'running')">
<em title="run" class="fas fa-play-circle"></em></button>
<label class="label is-small" for="op-run-button">Run</label>
</div>
</template>
<template x-if="isOperationRunning">
<div class="is-flex is-flex-direction-column is-align-items-center">
<button x-bind:disabled="!isOperationRunning" class="button no-style is-medium"
x-on:click="updateOperation('state', 'run_one_link')" id="op-run-1-button">
<em title="run one step" class="fas fa-play"><sub>1</sub></em></button>
<label class="label is-small" for="op-run-1-button">Run 1 Link</label>
</div>
</template>
<template x-if="!isOperationRunning">
<div class="is-flex is-flex-direction-column is-align-items-center">
<button class="button no-style is-large" x-on:click="rerunOperation()">
<em title="Re-run operation with same settings" class="fas fa-redo-alt"></em>
</button>
<label class="label is-small">Re-run operation</label>
</div>
</template>
</div>
<div class="is-flex is-flex-direction-row">
<!-- OBFUSCATION DROPDOWN -->
<div>
<span class="is-flex is-align-items-center">
<span class="mr-2">Obfuscation:</span>
<template x-if="!isOperationRunning">
<span class="level-item tag is-medium m-0"><span
class="has-text-success"
x-text="selectedOperation.obfuscator"></span></span>
</template>
<template x-if="isOperationRunning">
<div class="select is-small">
<select id="operation-obfuscator"
x-on:change="updateOperation('obfuscator', $el.value)"
x-model="selectedOperation.obfuscator">
<option value="" default disabled selected>Select one...</option>
<template x-for="o in OBFUSCATORS" :key="o.name">
<option x-bind:value="o.name" x-text="o.name"></option>
</template>
</select>
</div>
</template>
</span>
</div>
<div class="vr"></div>
<!-- TOGGLE -->
<div class="mb-5 is-flex is-flex-direction-row is-justify-content-center is-align-items-center">
<template x-if="!isOperationRunning">
<span class="level-item tag is-medium m-0">
<span class="has-text-success" x-text="selectedOperation.autonomous === 0 ? 'manual' : 'autonomous'"></span>
</span>
</template>
<template x-if="isOperationRunning">
<div class="toggleContainer flex is-flex-direction-column is-align-items-left">
<div>
<label for="toggleAutonomous">Manual</label>
<div class="toggleSwitch mx-2">
<input type="checkbox"
id="toggleAutonomous"
x-model="selectedOperation.autonomous"
x-bind:checked="Boolean(selectedOperation.autonomous) ? 'checked' : null"/>
<span class="toggleSlider toggleSliderRound"
title="Toggle operation to run autonomously"
x-on:click="selectedOperation.autonomous = (selectedOperation.autonomous === 0) ? 1 : 0; updateOperation('autonomous', Number(selectedOperation.autonomous))"
x-bind:class="Boolean(selectedOperation.autonomous) ? 'toggle-on' : 'toggle-off'">
<span x-bind:class="Boolean(selectedOperation.autonomous) ? 'toggle-slider-on' : 'toggle-slider-off'"></span>
</span>
</div>
<label for="toggleAutonomous">Autonomous</label>
</div>
<div class="mt-4">
<label for="toggleScroll">Autoscroll</label>
<div class="toggleSwitch">
<input type="checkbox"
id="toggleAutonomous"
x-model="linkScrollHistory.isEnabled"
x-bind:checked="linkScrollHistory.isEnabled ? 'checked' : null"/>
<span class="toggleSlider toggleSliderRound"
title="Toggle autoscrolling"
x-on:click="linkScrollHistory.isEnabled = (linkScrollHistory.isEnabled === false) ? true : false"
x-bind:class="linkScrollHistory.isEnabled ? 'toggle-on' : 'toggle-off'">
<span x-bind:class="linkScrollHistory.isEnabled ? 'toggle-slider-on' : 'toggle-slider-off'"></span>
</span>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<div>
<!-- TIMELINE-->
<template x-if="selectedOperation && selectedOperationID">
<div>
<div class="navbar-divider mb-5"></div>
<template x-if="isOperationRunning">
<div class="is-flex is-flex-direction-row is-justify-content-space-between">
<div class="has-text-grey-light is-size-7">
<em x-show="selectedOperation.chain.length > 0"><span
class="has-text-grey-light"
x-text="'Last ran ' + selectedOperation.chain[selectedOperation.chain.length-1]?.ability.name + ' (' + getLastDecideTime() + ')'"></span></em>
</div>
<div class="is-flex is-flex-direction-row is-justify-content-flex-end">
<div class="mb-6 mr-4 text-align-right">
<button title="add manual command" class="button is-small" x-on:click="addNewRow = true; getAgents(); setTimeout(() => {document.querySelector('#input-command').scrollIntoView()}, 2);">
<span class="icon"><em class="pr-1 fas fa-plus"></em></span>
<span>Manual Command</span>
</button>
</div>
<div class="mb-6 mr-4">
<button title="add potential link" class="button is-small" x-on:click="openPotentialLinkModal()">
<span class="icon"><em class="pr-1 fas fa-plus"></em></span>
<span>Potential Link</span>
</button>
</div>
</div>
</div>
</template>
<div class="timeline-table">
<table class="table">
<thead>
<tr class="table-header">
<th class="decideColumn">Decide</th>
<th id="link-status-header" class="statusColumn csv-exclude pl-4"
title="Link Status Indicator">
Status
</th>
<th class="infoColumn">Link/Ability Name</th>
<th>Agent #paw</th>
<th>Host</th>
<th>pid</th>
<th>Link Command</th>
<th>Link Output</th>
<th title="Link Actions" class="csv-exclude"></th>
</tr>
</thead>
<template x-if="selectedOperation.chain.length === 0">
<tbody>
<tr class="csv-exclude">
<td colspan="9" class="has-text-centered"><em>No links––click the buttons on the
right to add
commands/links.</em>
</td>
</tr>
</tbody>
</template>
<template x-for="(link, index) in selectedOperation.chain" :key="link.id">
<tbody>
<tr x-bind:id="link.id"
x-on:click="selectedLink = link" x-ref="linkRow" x-init="$nextTick(() => scrollToLastLink($refs.linkRow, index))">
<td class="decideColumn">
<span class="has-text-grey-light" x-text="getReadableTime(link.decide)"></span>
</td>
<td class="linkStatusColumn csv-exclude" headers="link-status-header">
<span x-bind:class="'linkStatus status-'+((link.status in LINK_STATUSES) ? LINK_STATUSES[link.status] : 'queued')"
x-bind:data-content="link.status in LINK_STATUSES ? LINK_STATUSES[link.status] : 'queued'"
x-bind:title="link.status in LINK_STATUSES ? LINK_STATUSES[link.status] : 'queued'"></span>
</td>
<td class="linkInfoColumn"
x-text="link.ability.name + ((link.ability.cleanup) ? '(CLEANUP)' : '')"></td>
<!--AGENT COLUMN-->
<td class="agentDetails">
<p x-show="!link.paw"><em>n/a</em></p>
<p x-show="link.paw"><em x-text="link.paw"></em></p>
</td>
<td class="agentDetails">
<p x-show="!link.host"><em>n/a</em></p>
<p x-show="link.host"><em x-text="link.host"></em></p>
</td>
<td class="agentDetails">
<p x-show="!link.pid" class="mb-0"><em>n/a</em></p>
<p x-show="link.pid"><em x-text="link.pid"></em></p>
</td>
<!--COMMAND COLUMN-->
<td class="commandDetails">
<div class="is-flex is-justify-content-flex-start">
<template x-if="!isLinkEditable(link)">
<button class="button is-small"
x-on:click="getLink(link); openModal='showCommand'"
title="view output">View Command
</button>
</template>
<template x-if="isLinkEditable(link)">
<button class="button is-small is-warning is-outlined"
x-on:click="getLink(link); openModal='showCommand'"
title="view output">
<i class="fas fa-exclamation-triangle pr-2"></i>
Review Command
</button>
</template>
</div>
</td>
<!--OUTPUT/FACTS COLUMN-->
<td class="outputDetails">
<template x-if="link.output !== 'True'">
<p class="is-size-7 is-italic ml-4">No output.</p>
</template>
<template x-if="link.output === 'True'">
<div>
<button class="button is-small"
x-on:click="getLink(link); openModal='showOutput'"
title="view output">View Output
</button>
</div>
</template>
</td>
<td class="csv-exclude">
<button x-show="selectedLink === link && isOperationRunning && !(link.status in LINK_STATUSES)"
type="button" title="delete link"
class="button is-danger is-outlined is-small ml-2"
x-on:click="openModal = 'deleteLinkModal'"><em
class="pr-1 fas fa-trash"></em>Discard Link
</button>
</td>
</tr>
</tbody>
</template>
<!-- NEW ROW TO ADD MANUAL COMMAND-->
<template x-if="addNewRow">
<tbody>
<tr class="csv-exclude" x-data="{manualCommand: EMPTY_MANUAL_COMMAND_OBJECT}">
<template x-if="selectedOperation.host_group.length < 1">
<td colspan="9" class="has-text-right"><em>No agents running. Add an agent
to run an operation.</em></td>
</template>
</tr>
<template x-if="selectedOperation.host_group.length > 0">
<tr class="csv-exclude" x-data="{manualCommand: EMPTY_MANUAL_COMMAND_OBJECT}">
<td></td>
<td></td>
<td>New Manual Command</td>
<td>
<label x-show="false" for="manual-agent">Choose an agent:</label>
<div class="select is-small my-2 ml-3">
<select id="manual-agent" x-model="selectedAgentIndex"
x-on:change="getAgent(selectedAgent.paw)" required>
<option value="" disabled>Choose agent</option>
<template x-for="(agent, index) in selectedOperation.host_group"
:key="agent.paw">
<option x-bind:selected="index === 0"
:value="index"
x-text="agent.display_name + '-' + agent.paw"></option>
</template>
</select>
</div>
</td>
<td>
<div class="select is-small my-2 ml-3">
<select x-model="manualCommand.executor.name"
id="manual-executors"
class="executor-selector">
<option value="" disabled>Choose executor</option>
<template x-for="(executor, index) in selectedAgentExecutors"
:key="executor.executor">
<option x-bind:selected="index === 0"
x-text="executor.display"
:value="executor.executor"></option>
</template>
</select>
</div>
</td>
<td colspan="2">
<label x-show="false" for="input-command">Type manual command:</label>
<div class="is-flex is-align-items-flex-end is-justify-content-center is-flex-direction-column">
<textarea x-model="manualCommand.executor.command"
class="input executor-command"
id="input-command"
type="text"
spellcheck="false"
contenteditable
required></textarea>
</div>
</td>
<td class="is-flex is-justify-content-center is-flex-direction-column">
<button type="button"
class="button is-primary is-small my-2"
x-on:click="addManualCommand(manualCommand)">
Add Command
</button>
<button type="button"
class="button is-small"
x-on:click="addNewRow = false">
Cancel
</button>
</td>
</tr>
</template>
</tbody>
</template>
</table>
</div>
<!-- ADD COMMAND/LINK BUTTONS-->
<template x-if="isOperationRunning">
<div class="is-flex is-flex-direction-row is-justify-content-flex-end">
<div class="mb-6 mr-4 text-align-right">
<button title="add manual command" class="button is-small" x-on:click="addNewRow = true; getAgents(); setTimeout(() => {document.querySelector('#input-command').scrollIntoView()}, 2);">
<span class="icon"><em class="pr-1 fas fa-plus"></em></span>
<span>Manual Command</span>
</button>
</div>
<div class="mb-6 mr-4">
<button title="add potential link" class="button is-small" x-on:click="openPotentialLinkModal()">
<span class="icon"><em class="pr-1 fas fa-plus"></em></span>
<span>Potential Link</span>
</button>
</div>
</div>
</template>
</div>
</template>
</div>
<!-- MODALS -->
<template x-if="openModal">
<div class="modal is-active">
<div class="modal-background" @click="openModal = null; selectPotentialLink({})"></div>
<template x-if="openModal === 'showOutput'">
<div class="modal-card wide">
<header class="modal-card-head">
<p class="modal-card-title">Output</p>
</header>
<section class="modal-card-body">
<template x-if="selectedLinkFacts?.length > 0">
<div class="mb-2">
<p class="mt-3 mb-2">Facts:</p>
<pre class="has-text-left white-space-pre-line">
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<template x-for="(fact, index) in selectedLinkFacts" :key="fact+index">
<tr>
<td x-text="fact.name"></td>
<td x-text="fact.value"></td>
<td x-text="fact.score"></td>
</tr>
</template>
</tbody>
</table>
</pre>
</div>
</template>
<div>
<p class="mt-3 mb-2" x-text="`Exit Code: ${selectedLinkResults && selectedLinkResults.exit_code ? selectedLinkResults.exit_code : 'Nothing to show'}`"></p>
<p class="mt-3 mb-2" x-text="`Standard Output: ${selectedLinkResults && selectedLinkResults.stdout ? '' : 'Nothing to show'}`"></p>
<template x-if="selectedLinkResults != null && selectedLinkResults.stdout !== ''">
<pre class="has-text-left white-space-pre-line" x-text="selectedLinkResults.stdout"></pre>
</template>
</div>
<div>
<p class="mt-3 mb-2" x-text="`Standard Error: ${selectedLinkResults && selectedLinkResults.stderr ? '' : 'Nothing to show'}`"></p>
<template x-if="selectedLinkResults != null && selectedLinkResults.stderr !== ''">
<pre class="has-text-left white-space-pre-line has-text-danger" x-text="selectedLinkResults.stderr"></pre>
</template>
</div>
</section>
<footer class="modal-card-foot">
<nav class="level">
<div class="level-left">
</div>
<div class="level-right">
<div class="level-item">
<button class="button is-small" @click="openModal = null">Close</button>
</div>
</div>
</nav>
</footer>
</div>
</template>
<template x-if="openModal === 'showCommand'">
<div class="modal-card wide">
<header class="modal-card-head">
<p class="modal-card-title">Command</p>
</header>
<section class="modal-card-body">
<template x-if="isLinkEditable(selectedLink)">
<div>
<label x-show="false" htmlFor="edit-command">Edit command</label>
<textarea id="edit-command"
class="textarea is-small code"
contentEditable
x-model="editableCommand"
spellCheck="false"></textarea>
</div>
</template>
<template x-if="!isLinkEditable(selectedLink)">
<div>
<template x-if="!selectedLinkPlaintextCommand || selectedLinkPlaintextCommand == selectedLinkCommand">
<div>
<pre class="has-text-left white-space-pre-line" x-text="selectedLinkCommand"></pre>
</div>
</template>
<template x-if="selectedLinkPlaintextCommand && selectedLinkPlaintextCommand !== selectedLinkCommand">
<div>
<p class="mt-3 mb-2">Obfuscated:</p>
<pre class="has-text-left white-space-pre-line" x-text="selectedLinkCommand"></pre>
<p class="mt-3 mb-2">Plaintext:</p>
<pre class="has-text-left white-space-pre-line" x-text="selectedLinkPlaintextCommand"></pre>
</div>
</template>
</div>
</template>
</section>
<footer class="modal-card-foot">
<nav class="level">
<div class="level-left">
<template x-if="isLinkEditable(selectedLink)">
<button type="button"
class="button is-danger is-outlined is-small"
x-on:click="updateLink(-2); openModal = null;">
Discard
</button>
</template>
</div>
<div class="level-right">
<div class="level-item">
<button class="button is-small" @click="openModal = null">Close</button>
</div>
<template x-if="isLinkEditable(selectedLink)">
<div class="level-item">
<button type="button"
class="button is-primary is-small"
x-on:click="updateLink(-3, editableCommand); openModal = null;">
Approve
</button>
</div>
</template>
</div>
</nav>
</footer>
</div>
</template>
<template x-if="openModal === 'addOperation'">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Start New Operation</p>
</header>
<section class="modal-card-body">
<div class="modal-form">
<div>
<label for="op-name">Operation name</label>
<div class="modal-form-fields">
<input class="input is-small" id="op-name" type="text"
x-model="operationToStart.name" required>
</div>
</div>
<div>
<label for="basic-adversary"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Adversary']">Adversary</span></label>
<div class="modal-form-fields" id="basic-adversary">
<div class="select is-small my-2 ml-3">
<select x-model="operationToStart.adversary.adversary_id"
x-on:change="handleSelectAdversary()">
<option value="" selected>No adversary (manual)</option>
<template x-for="a in ADVERSARIES" :key="a.adversary_id">
<option :value="a.adversary_id" x-text="a.name"></option>
</template>
</select>
</div>
</div>
</div>
<div>
<label for="autonomous-source"><span class="has-tooltip-multiline has-tooltip-top" x-bind:data-tooltip="usage['Fact source']">Fact source</span></label>
<div class="modal-form-fields" id="autonomous-source">
<div class="select is-small my-2 ml-3">
<select x-model="operationToStart.source.id">
<template x-for="s in SOURCES" :key="s.id">
<option :value="s.id" x-text="s.name"
x-bind:selected="operationToStart.source.id === s.id"></option>
</template>
</select>
</div>
</div>
</div>
</div>
<div class="modal-form-group-header">
<button type="button" class="button no-style heading pt-2"
x-on:click="openModalForm.advanced = !openModalForm.advanced"
x-bind:class="openModalForm.advanced ? 'expanded' : 'collapsed'">
<span>Advanced</span>
<span class="navbar-divider"></span>
</button>
</div>
<div class="modal-form" x-show="openModalForm.advanced">
<div>
<label for="basic-groups"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Group']">Group</span></label>
<div class="modal-form-fields"
id="basic-groups">
<button type="button"
class="is-small button is-rounded is-primary m-1"
x-bind:class="(operationToStart.group === '') ? '' : 'is-outlined has-text-white'"
x-on:click="operationToStart.group = ''">
all groups
</button>
<template x-for="(group, index) in agentGroups" :key="group">
<button type="button"
class="is-small button is-rounded is-primary m-1"
x-bind:class="(operationToStart.group === group) ? '' : 'is-outlined has-text-white'"
x-on:click="operationToStart.group = group"
x-show="group !== ''"
x-text="group">
</button>
</template>
</div>
</div>
<div>
<label for="autonomous-planner"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Planner']">Planner</span></label>
<div class="modal-form-fields">
<div class="select is-small my-2 ml-3">
<select id="autonomous-planner" x-model="operationToStart.planner.id">
<template x-for="p in PLANNERS" :key="p.id">
<option :value="p.id"
x-bind:selected="operationToStart.planner.id === p.id"
x-bind:disabled="(!p.allow_repeatable_abilities && isSelectedAdversaryRepeatable)"
x-text="p.name"></option>
</template>
</select>
</div>
<span x-show="isSelectedAdversaryRepeatable" class="icon has-text-warning ml-2"
data-tooltip="The selected adversary has repeated abilities so some planners are unavailable.">
<em class="fas fa-exclamation-triangle"></em>
</span>
</div>
</div>
<div>
<label for="stealth-obfuscator"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Obfuscators']">Obfuscators</span></label>
<div class="modal-form-fields" id="stealth-obfuscator">
<template x-for="o in OBFUSCATORS" :key="o.name">
<button class="is-small button is-rounded is-primary m-1"
type="button"
x-bind:class="operationToStart.obfuscator === o.name ? '' : 'is-outlined has-text-white'"
x-on:click="operationToStart.obfuscator = o.name"
x-text="o.name">
</button>
</template>
</div>
</div>
<div>
<label for="autonomous-autonomous"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Autonomous']">Autonomous</span></label>
<div class="modal-form-fields" id="autonomous-autonomous">
<input type="radio" id="run-autonomously"
x-model="operationToStart.autonomous" value="1" checked>
<label for="run-autonomously">Run autonomously</label>
<input type="radio" id="manual-approval"
x-model="operationToStart.autonomous" value="0">
<label for="manual-approval">Require manual approval</label>
</div>
</div>
<div>
<label for="autonomous-use_learning_parsers"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Parser']">Parser</span></label>
<div class="modal-form-fields" id="autonomous-use_learning_parsers">
<input type="radio" id="parsers-default"
x-model="operationToStart.use_learning_parsers" value="1" checked>
<label for="parsers-default">Use default parsers</label>
<input type="radio" id="parsers-no_default"
x-model="operationToStart.use_learning_parsers" value="0">
<label for="parsers-no_default">Do not use default parsers</label>
</div>
</div>
<div>
<label for="basic-keep"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Auto-close']">Auto-close</span></label>
<div class="modal-form-fields" id="basic-keep">
<input type="radio" id="keep-open"
x-model="operationToStart.auto_close"
value="0" checked>
<label for="keep-open">Keep open forever</label>
<input type="radio" id="auto-close"
x-model="operationToStart.auto_close"
value="1">
<label for="auto-close">Auto close operation</label>
</div>
</div>
<div>
<label for="basic-run"><span
class="has-tooltip-multiline has-tooltip-bottom"
x-bind:data-tooltip="usage['Run immediately']">Run state</span></label>
<div class="modal-form-fields" id="basic-run">
<input type="radio" x-model="operationToStart.state"
id="run-immediately"
value="running" checked>
<label for="run-immediately">Run immediately</label>
<input type="radio" x-model="operationToStart.state"
id="pause-on-start"
value="paused">
<label for="pause-on-start">Pause on start</label>
</div>
</div>
<div>
<label for="stealth-jitter"><span
class="has-tooltip-multiline has-tooltip-top"
x-bind:data-tooltip="usage['Jitter']">Jitter (sec/sec)</span></label>
<div class="modal-form-fields" id="stealth-jitter">
<input class="input"
:class="{ 'is-danger': !isJitterValid() }"
x-model="jitterMin"
id="stealth-jitterMin"
type="number"
required
pattern="[0-9]*"
placeholder="2"/>
<label class="input-hover-label" for="stealth-jitterMin">min</label>
<span>/</span>
<input class="input"
:class="{ 'is-danger': !isJitterValid() }"
x-model="jitterMax"
id="stealth-jitterMax"
type="number"
required
pattern="[0-9]*"
placeholder="8"/>
<label class="input-hover-label" for="stealth-jitterMax">max</label>
<button type="button" class="button is-primary no-style reset"
title="reset to default"
x-on:click="jitterMin = 2; jitterMax = 8;">
Reset
</button>
<p x-show="!isJitterValid()" class="pl-3 help has-text-danger">
Min and Max need to be >= 0, and Min must be <= Max
</p>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<nav class="level">
<div class="level-left">
<div class="level-item">
<button class="button is-small" @click="openModal = null">Close</button>
</div>
</div>
<div class="level-right">
<div class="level-item">
<button type="button" class="button is-primary is-small"
x-on:click="addOperation()"
x-text="schedule ? 'Schedule' : 'Start'"></button>
</div>
</div>
</nav>
</footer>
</div>
</template>
<template x-if="openModal === 'showOperationDetails'">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Operation Details</p>
</header>
<section class="modal-card-body">
<table>
<col width="35%">
<col width="65%">
<tbody>
<tr>
<td>
<label class="label">Operation Name</label>
</td>
<td>
<span x-text="selectedOperation.name"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Adversary</label>
</td>
<td>
<span x-text="selectedOperation.adversary.name"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Fact Source</label>
</td>
<td>
<span x-text="selectedOperation.source.name"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Group</label>
</td>
<td>
<span x-text="(selectedOperation.group ? selectedOperation.group : 'all')"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Planner</label>
</td>
<td>
<span x-text="selectedOperation.planner.name"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Obfuscator</label>
</td>
<td>
<span x-text="selectedOperation.obfuscator"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Autonomous</label>
</td>
<td>
<span x-text="(selectedOperation.autonomous === 0 ? 'manual' : 'autonomous')"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Parser</label>
</td>
<td>
<span x-text="selectedOperation.use_learning_parsers"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Auto Close</label>
</td>
<td>
<span x-text="selectedOperation.auto_close"></span>
</td>
</tr>
<tr>
<td>
<label class="label">Jitter</label>
</td>
<td>
<span x-text="selectedOperation.jitter"></span>
</td>
</tr>
</tbody>
</table>
</section>
<footer class="modal-card-foot">
<nav class="level">
<div class="level-left"></div>
<div class="level-right">
<div class="level-item">
<button class="button is-small" @click="openModal = null">Close</button>
</div>
</div>
</nav>
</footer>
</div>
</template>
<template x-if="openModal === 'addLink'">
<div class="modal-card wide" x-init="getAbilities()">
<header class="modal-card-head">
<p class="modal-card-title">Add New Potential Link</p>
</header>
<section class="modal-card-body" x-show="!selectedPotentialLink.ability_id">
<form>
<div class="field is-horizontal">
<div class="field-label is-small">
<label class="label">Agent</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-small is-fullwidth">
<select id="link-agent" x-model="selectedAgentIndex" x-on:change="getAgent(selectedAgent.paw); potentialLink.agent = selectedAgent">
<option default disabled value="">Choose an agent</option>
<template x-for="(agent, index) in selectedOperation.host_group" :key="agent.paw">
<option :value="index" x-bind:selected="index === 0" x-text="agent.display_name + '-' + agent.paw"></option>
</template>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-small">
<label class="label">Executor</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-small is-fullwidth">
<select id="link-exec" x-model="potentialLink.executor" x-on:change="filterAbilities(abilities)">
<option value="" disabled>Select an executor</option>
<template x-for="(exec, index) of selectedAgentExecutors" :key="exec.executor">
<option x-text="exec.display" :value="exec.executor"></option>
</template>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="p-3"></div>
<div class="field is-horizontal">
<div class="field-label is-small">
<label class="label"><span class="icon is-small"><em class="fas fa-search"></em></span></label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input is-small" x-model="potentialLinkSearchQuery" placeholder="Search for a link..." x-on:keyup="filterAbilities(abilities)">
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-small">
<label class="label">Tactic</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-small is-fullwidth">
<select id="link-tactic" x-model="potentialLink.tactic" x-on:change="filterAbilities(abilities)">
<option default value="">All tactics</option>
<template x-for="tactic in tacticFiltersList" :key="tactic">
<option x-text="tactic"></option>
</template>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-small">
<label class="label">Technique</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select is-small is-fullwidth">
<select id="link-technique" x-model="potentialLink.technique" x-on:change="filterAbilities(abilities)">
<option default value="">All techniques</option>
<template x-for="technique in techniqueFiltersList" :key="technique">
<option x-text="technique"></option>
</template>
</select>
</div>
</div>
</div>
</div>
</div>
</form>
<hr>
<div class="is-flex is-justify-content-space-between mb-2">
<strong x-show="filteredAbilitiesList.length">Select a potential link</strong>
<p x-text="`${filteredAbilitiesList.length} potential link${((filteredAbilitiesList.length === 1) ? '' : 's')}`"></p>
</div>
<template x-for="ability in filteredAbilitiesList" :key="ability.ability_id">
<div class="box mb-2 mr-2 p-3 op-ability" @click="selectPotentialLink(ability)">
<p style="width: 90%">
<span class="has-text-weight-bold" x-text="ability.name"></span>
<span x-text="`(${ability.technique_id})`"></span>
</p>
<p class="help mb-0" x-text="ability.description" style="width: 90%; white-space: pre-line;"></p>
<span class="icon is-large ability-icon"><em class="fas fa-lg fa-arrow-right"></em></span>
</div>
</template>
<em x-show="showNotRepeatableText" class="has-text-grey-light is-pulled-right is-size-7">Note: Repeated links are not available for this operation</em>
</section>
<section class="modal-card-body " x-show="selectedPotentialLink.ability_id">
<strong x-text="selectedPotentialLink.name" class="mb-2 is-size-4"></strong>
<span x-text="`(${selectedPotentialLink.technique_id})`"></span>
<p x-text="selectedPotentialLink.description" style="white-space: pre-line;"></p>
<hr>
<p class="has-text-centered has-text-weight-bold">Fact Templates</p>
<p class="help" x-show="Object.keys(selectedPotentialLinkFacts).length">
This link needs facts in order to run properly. Select one value for each fact to add one potential link, otherwise, select multiple
values and a potential link will be added for each possible combination.
</p>
<p class="help" x-show="!Object.keys(selectedPotentialLinkFacts).length">
This link has no fact templates.
</p>
<template x-for="factName in Object.keys(selectedPotentialLinkFacts)" :key="factName">
<div class="block">
<span class="icon-text">
<label class="label" x-text="factName"></label>
<span class="icon has-text-warning"
x-show="selectedPotentialLinkFacts[factName] ? selectedPotentialLinkFacts[factName].every((fact) => !fact.selected) : false"
data-tooltip="There are currently empty fact templates. You can still add this link but it may not run properly.">
<em class="fas fa-exclamation-triangle"></em>
</span>
</span>
<template x-for="fact in selectedPotentialLinkFacts[factName] || null" :key="fact.value">
<div>
<label class="checkbox">
<input type="checkbox" x-model="fact.selected" x-on:change="setPotentialLinksToAdd()">
<span x-text="fact.value"></span>
<span class="tag" x-text="fact.origin" data-tooltip="The origin of this fact"></span>
</label>
</div>
</template>
</div>
</template>
<p class="has-text-centered has-text-weight-bold">Link Command</p>
<p class="help">
You can edit the link's command here. Editing the fact templates (<code>#{...}</code>) may result in unexpected behavior.
</p>
<textarea class="textarea is-family-monospace" x-model="potentialLinkCommand" x-on:change="setPotentialLinksToAdd()"></textarea>
</section>
<footer class="modal-card-foot" x-show="!selectedPotentialLink.ability_id">
<nav class="level">
<div class="level-left">
</div>
<div class="level-right">
<div class="level-item">
<button class="button is-small" @click="openModal = null; selectPotentialLink({})">Close</button>
</div>
</div>
</nav>
</footer>
<footer class="modal-card-foot" x-show="selectedPotentialLink.ability_id">
<nav class="level">
<div class="level-left">
<button class="button is-small" @click="selectPotentialLink({})">
<span class="icon"><em class="fas fa-arrow-left"></em></span>
<span>Back</span>
</button>
</div>
<div class="level-right">
<div class="level-item">
<button class="button is-small" @click="openModal = null; selectPotentialLink({})">Close</button>
</div>
<button class="button is-small" x-bind:class="{ 'is-primary': !isAddingBlankPotentialLink || !Object.keys(selectedPotentialLinkFacts).length, 'is-warning': isAddingBlankPotentialLink && Object.keys(selectedPotentialLinkFacts).length }" @click="addPotentialLinks()">
<span class="icon">
<em class="fas fa-plus" x-show="!isAddingBlankPotentialLink || !Object.keys(selectedPotentialLinkFacts).length"></em>
<em class="fas fa-exclamation-triangle" x-show="isAddingBlankPotentialLink && Object.keys(selectedPotentialLinkFacts).length"></em>
</span>
<span>
Add
<span x-text="potentialLinksToAdd.length"></span>
Potential Link<span x-show="potentialLinksToAdd.length > 1">s</span>
</span>
</button>
</div>
</nav>
</footer>
</div>
</template>
<template x-if="openModal === 'showDownloadMenu'">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Download Reports</p>
</header>
<section class="modal-card-body">
<div class="modal-form">
<div>
<label for="agent-output-field">Agent output</label>
<div class="modal-form-fields" id="agent-output-field">
<input id="agent-output"
x-bind:disabled="selectedReportType === 'csv'"
type="checkbox"
class="mr-1"
x-model="isAgentOutputSelected">
<label for="agent-output">include agent output</label>
</div>
</div>
<div>
<label for="report-type">Report type</label>
<div class="modal-form-fields download-buttons-container" id="report-type">
<input class="mr-1" type="radio" x-model="selectedReportType"
value="full-report" id="full-report">
<label for="full-report">Full Report</label>
<input class="mr-1" type="radio" x-model="selectedReportType"
value="event-logs" id="event-logs">
<label for="event-logs">Event Logs</label>
<input class="mr-1" type="radio" x-model="selectedReportType"
value="csv" id="csv">
<label for="csv">CSV</label>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<nav class="level">
<div class="level-left">
<div class="level-item">
<button class="button is-small" @click="openModal = null">Close</button>
</div>
</div>
<div class="level-right">
<div class="level-item">
<button type="button" class="button is-primary is-small"
x-on:click="handleDownload()">
Download
</button>
</div>
</div>
</nav>
</footer>
</div>
</template>
<template x-if="openModal === 'deleteOperationModal' && selectedOperation">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete operation?</p>
</header>
<section class="modal-card-body">
<div class="modal-form">
<div>
<p>Are you sure you want to delete the operation <em
x-text="selectedOperation.name"></em>?</p>
</div>
</div>
</section>
<footer class="modal-card-foot">
<nav class="level">
<div class="level-left">
<div class="level-item">
<button type="button" class="button ml-2 is-small"
x-on:click="openModal = null">
Cancel
</button>
</div>
</div>
<div class="level-right">
<div class="level-item">
<button type="button" class="button is-primary is-small"
x-on:click="deleteOperation(); openModal = null;">
Delete
</button>
</div>
</div>
</nav>
</footer>
</div>
</template>
<template x-if="openModal === 'deleteLinkModal'">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Delete link?</p>
</header>
<section class="modal-card-body">
<div class="modal-form">
<div>
<p>Are you sure you want to delete the link <em
x-text="selectedLink.ability.name"></em>?</p>
</div>
</div>
</section>
<footer class="modal-card-foot">
<nav class="level">
<div class="level-left">
<div class="level-item">
<button type="button" class="button ml-2 is-small"
x-on:click="openModal = null">
Cancel
</button>
</div>
</div>
<div class="level-right">
<div class="level-item">
<button type="button" class="button is-primary is-small"
x-on:click="updateLink(-2); openModal = null;">
Delete
</button>
</div>
</div>
</nav>
</footer>
</div>
</template>
</div>
</template>
</div>
</div>
<script>
function alpineOperations() {
return {
// CONSTANTS
ENDPOINT: '/api/v2/operations',
EXECUTOR_SHELLS: {
psh: 'PS',
sh: '$',
zsh: '$'
},
LINK_STATUSES: {
0: 'success',
'-1': 'paused',
1: 'failed',
'-2': 'discarded',
'-3': 'collect',
'-4': 'untrusted',
'-5': 'visible',
124: 'timeout'
},
OBFUSCATORS: [],
PLANNERS: [],
ADVERSARIES: [],
SOURCES: [],
docs: '{{ usage | tojson }}'.toString().replace(/["\[\]]/g, '').split('}'),
usage: {},
agentGroups: [],
selectedOperationID: '',
selectedOperation: null,
selectedAgentIndex: 0,
isSelectedAdversaryRepeatable: false,
selectedLink: null,
selectedLinkResults: null,
selectedLinkFacts: null,
selectedLinkPlaintextCommand: null,
selectedLinkCommand: null,
linkScrollHistory: {operationID: -1, shouldScroll: true, isEnabled: true},
selectedReportType: 'full-report',
isAgentOutputSelected: false,
openModal: null,
openModalForm: {
advanced: false
},
facts: [],
jitterMin: 2,
jitterMax: 8,
operations: [],
operationToStart: {},
selectedPotentialLink: {},
selectedPotentialLinkFacts: {},
potentialLinksToAdd: [],
potentialLinkSearchQuery: '',
isAddingBlankPotentialLink: false,
potentialLinkCommand: '',
potentialLink: {
agent: '',
executor: '',
tactic: '',
technique: '',
},
tacticFiltersList: [],
techniqueFiltersList: [],
abilities: [],
filteredAbilitiesList: [],
schedule: '',
editableCommand: null,
addNewRow: false,
addedTechniquesIDs: new Set(),
get EMPTY_MANUAL_COMMAND_OBJECT() {
return {
paw: '',
executor: {
name: '',
platform: '',
command: '',
}
};
},
getUsage() {
if (this.docs) {
this.docs.forEach((obj) => {
if (obj) {
let items = obj.split('{')[1].split(': ');
this.usage[items[0]] = items[1];
}
});
}
},
get showNotRepeatableText() {
return !this.selectedOperation?.adversary?.has_repeatable_abilities;
},
get selectedAgent() {
if (!this.selectedOperation || this.selectedAgentIndex >= this.selectedOperation.host_group.length) return null;
return this.selectedOperation.host_group[this.selectedAgentIndex];
},
get selectedAgentExecutors() {
if (!this.selectedAgent) return [];
return this.selectedAgent.executors.map((e) => {
let cmd = `(${e})`;
if (e in this.EXECUTOR_SHELLS) cmd += ` ${this.EXECUTOR_SHELLS[e]}`;
return {
executor: e,
display: `${cmd} > `
};
});
},
get isOperationRunning() {
if (!this.selectedOperation) return false;
return !(this.selectedOperation.state === 'finished' || this.selectedOperation.state === 'cleanup' || this.selectedOperation.state === 'out_of_time');
},
init() {
this.selectedOperation = { chain: [], host_group: [] };
this.agentGroups = [];
this.selectedOperationID = '';
this.selectedAgentIndex = 0;
this.isSelectedAdversaryRepeatable = false;
this.selectedLink = null;
this.selectedLinkResults = null;
this.selectedLinkFacts = null;
this.selectedLinkCommand = null;
this.selectedReportType = 'full-report';
this.isAgentOutputSelected = false;
this.openModal = null;
this.openModalForm = { advanced: false };
this.operations = [];
this.operationToStart = { planner: '', source: ''};
},
isLinkEditable(currentLink) {
if (!currentLink) return false;
// Link can only be editable if operation is running, and if link is paused, queued, or completed
return this.isOperationRunning && ((currentLink.status === -1 || !(currentLink.status in this.LINK_STATUSES))
&& !(currentLink.finish !== null || currentLink.output === 'True'));
},
getReadableTime(date) {
// i.e. format 2021-08-03 19:37:08
if (!date) return '';
date = date.replace('T', ' ').replace('Z', '');
let split = date.split('-');
let hMonth = Number(split[1]) - 1;
let hDate = Number(split[2].split(' ')[0]);
let hTime = split[2].split(' ')[1].split(':');
return new Date(Date.UTC(split[0], hMonth, hDate, hTime[0], hTime[1], hTime[2])).toLocaleString('en-US', { timeZoneName: 'short' });
},
getLastDecideTime() {
if (!this.selectedOperation || !this.selectedOperation.chain || this.selectedOperation.chain.length === 0) return '';
return getHumanFriendlyTimeISO8601(this.selectedOperation.chain[this.selectedOperation.chain.length - 1].decide);
},
// Controls whether user can add link or not--depending on whether adversary allows repeatable abilities, and ability has been added already or not
isLinkAvailable(ability) {
return this.selectedOperation.adversary.has_repeatable_abilities ? true : !this.addedTechniquesIDs.has(ability.technique_id);
},
generateAddedLinksList() {
this.addedTechniquesIDs = new Set();
this.selectedOperation.chain.forEach((link) => {
if (!this.addedTechniquesIDs.has(link.ability.technique_id)) this.addedTechniquesIDs.add(link.ability.technique_id);
});
},
scrollToLastLink(linkRow, index){
// Check if link is the last link and that we haven't just switched operations
if (index === this.selectedOperation.chain.length - 1 && this.linkScrollHistory.shouldScroll && this.linkScrollHistory.isEnabled){
linkRow.scrollIntoView();
}
},
//
// API CALLS
//
async initOperations() {
this.getOperations();
this.getFields();
this.getUsage();
this.getFacts();
while (this.$refs.operationsHeader) {
await sleep(3000);
this.getOperation();
this.getFacts();
this.getFields();
}
},
getFields() {
Promise.all([apiV2('GET', '/api/v2/obfuscators'), apiV2('GET', '/api/v2/planners'), apiV2('GET', '/api/v2/adversaries'), apiV2('GET', '/api/v2/sources')]).then(([o, p, a, s]) => {
this.OBFUSCATORS = o;
this.PLANNERS = p;
this.ADVERSARIES = a.sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase());
this.SOURCES = s;
}).catch((error) => {
console.error(error);
});
},
openOperationCreateModal() {
this.resetOperationToStart();
this.openModal = 'addOperation';
this.getAgents();
},
resetOperationToStart() {
let defaultSourceId = 'ed32b9c3-9593-4c33-b0db-e2007315096b' // basic fact source
if (!this.SOURCES.find((s) => s.id === defaultSourceId) && this.SOURCES[0]) {
defaultSourceId = this.SOURCES[0].id
}
this.operationToStart = {
name: '',
group: '',
adversary: { adversary_id: '' },
auto_close: 0,
state: 'running',
autonomous: 1,
planner: { id: this.PLANNERS[0] ? this.PLANNERS[0].id : 'aaa7c857-37a0-4c4a-85f7-4e9f7f30e31a' },
source: { id: defaultSourceId },
use_learning_parsers: 1,
obfuscator: 'plain-text',
jitter: '2/8',
visibility: '51',
};
},
getOperations() {
apiV2('GET', this.ENDPOINT).then((res) => {
this.operations = res;
}).catch(() => {
toast('Error getting operations');
});
},
getFacts() {
if (!this.selectedOperationID) return;
apiV2('GET', `/api/v2/facts/${this.selectedOperationID}`).then((facts) => {
this.facts = facts.found;
}).catch(() => {
toast('Error getting facts');
});
},
getOperation() {
if (!this.selectedOperationID) return;
apiV2('GET', `${this.ENDPOINT}/${this.selectedOperationID}`).then((res) => {
this.selectedOperation = res;
if (this.linkScrollHistory.operationID != this.selectedOperationID){
this.linkScrollHistory.shouldScroll = false;
}else{
this.linkScrollHistory.shouldScroll = true;
}
this.linkScrollHistory.operationID = this.selectedOperationID;
this.generateAddedLinksList();
}).catch(() => {
toast('Error getting operation');
});
},
addOperation() {
if (!this.operationToStart.name) {
toast('Error: Operation name field is empty.', false);
} else if (!this.isJitterValid()) {
toast('Jitter values are invalid', false);
} else {
this.operationToStart.name = sanitize(this.operationToStart.name);
this.operationToStart.autonomous = Number(this.operationToStart.autonomous);
this.operationToStart.use_learning_parsers = parseInt(this.operationToStart.use_learning_parsers, 10) === 1;
this.operationToStart.auto_close = parseInt(this.operationToStart.auto_close, 10) === 1;
this.operationToStart.jitter = `${parseInt(this.jitterMin, 10)}/${parseInt(this.jitterMax, 10)}`;
apiV2('POST', this.ENDPOINT, this.operationToStart).then((newOperation) => {
toast(`Started operation ${this.operationToStart.name}!`, true);
this.operations.push(newOperation);
this.selectedOperation = newOperation;
this.selectedOperationID = newOperation.id;
this.openModal = null;
}).catch(() => {
toast('Error adding operation');
});
}
},
rerunOperation() {
let { id, start, state, chain, host_group, ...newOp } = this.selectedOperation;
let dateMatches = newOp.name.match(/[(]\d{1,2}\/\d{1,2}\/\d{2,4}, \d{1,2}:\d{2}:\d{2} [A|P]M[)]$/g);
let stringToReplace = (dateMatches && dateMatches.length) ? dateMatches[dateMatches.length - 1] : '';
let date = `(${new Date().toLocaleString()})`;
newOp.name = stringToReplace ? (newOp.name.replace(stringToReplace, date)) : (`${newOp.name} ${date}`);
newOp.name = sanitize(newOp.name);
apiV2('POST', this.ENDPOINT, newOp).then((newOperation) => {
toast(`Started operation ${newOperation.name}!`, true);
this.operations.push(newOperation);
this.selectedOperation = newOperation;
this.selectedOperationID = newOperation.id;
}).catch((error) => {
toast('Error adding operation', false);
console.error(error);
});
},
deleteOperation() {
apiV2('DELETE', `${this.ENDPOINT}/${this.selectedOperationID}`)
.then(() => {
this.init();
this.getOperations();
toast('Deleted operation.', true);
})
.catch(() => {
toast('Error deleting operation.');
});
},
getAgent(agentPaw) {
apiV2('GET', `api/v2/agents/${agentPaw}`)
.then((res) => {
// Check if agent has been seen in the last hour
if ((new Date() - new Date(res.last_seen)) > 3600000) {
toast(`Command might not run. Is Agent #${res.paw} still alive?`);
} else {
this.potentialLink.agent = res;
}
})
.catch(() => {
toast('Error getting agent.');
});
},
getAgents() {
apiV2('GET', 'api/v2/agents')
.then((res) => {
this.updateAgentGroups(res);
this.selectedAgentIndex = 0;
})
.catch(() => {
toast('Error getting agents.');
});
},
getAbilities() {
apiV2('GET', '/api/v2/abilities').then((abilities) => {
this.abilities = abilities;
if (!this.potentialLink.agent && this.selectedOperation.host_group && this.selectedOperation.host_group.length > 0) {
this.potentialLink.agent = this.selectedOperation.host_group[0];
this.potentialLink.executor = this.potentialLink.agent.executors[0];
}
this.getAgent(this.potentialLink.agent.paw);
this.filterAbilities(abilities);
this.tacticFiltersList = sortAlphabetically([...new Set(abilities.flatMap((a) => a.tactic))]);
this.techniqueFiltersList = sortAlphabetically([...new Set(abilities.flatMap((a) => `${a.technique_id} | ${a.technique_name}`))]);
}).catch(() => {
toast('Error getting abilities', false);
});
},
addManualCommand(manualCommand) {
if (!manualCommand.executor.command) {
toast('Enter manual command.');
return;
}
manualCommand.paw = this.selectedAgent.paw;
manualCommand.executor.name = document.getElementById('manual-executors').selectedOptions[0].value;
manualCommand.executor.platform = this.selectedAgent.platform;
if (this.selectedOperation.state === 'paused') {
toast('Operation is currently paused, and new links might not be added.', true);
this.addNewRow = false;
}
apiV2('POST', `${this.ENDPOINT}/${this.selectedOperationID}/potential-links`, manualCommand)
.then((res) => {
toast('Added manual command.', true);
this.getOperation();
// Reset
this.addNewRow = false;
this.selectedAgentIndex = null;
})
.catch(() => toast('Error adding manual command.'));
},
openPotentialLinkModal() {
this.potentialLink.tactic = '';
this.potentialLink.technique = '';
this.potentialLinkSearchQuery = '';
this.filterAbilities(this.abilities);
this.getAgents();
this.openModal = 'addLink';
},
selectPotentialLink(link) {
this.selectedPotentialLink = link;
this.selectedPotentialLinkFacts = {};
this.setPotentialLinksToAdd();
if (!this.selectedPotentialLink.ability_id) return;
const executor = link.executors.find((ex) => ex.platform === this.potentialLink.agent.platform && this.potentialLink.executor === ex.name);
const fields = [...new Set([...executor.command.matchAll(/#{(.*?)}/gm)].map((field) => field[1]))];
const opSource = this.SOURCES.find((s) => s.id === this.selectedOperation.source.id);
const opSourceCollected = this.SOURCES.find((source) => source.name === this.selectedOperation.name);
const facts = (opSource.facts.concat(opSourceCollected ? opSourceCollected.facts : [])).concat(this.facts);
this.potentialLinkCommand = executor.command;
fields.filter((field) => facts.find((fact) => fact.name === field)).forEach((field) => {
this.selectedPotentialLinkFacts[field] = []
facts.filter((fact) => fact.name === field).forEach((fact) => {
this.selectedPotentialLinkFacts[field].push({
value: fact.value,
origin: fact.origin_type,
selected: false
});
});
});
this.setPotentialLinksToAdd();
},
setPotentialLinksToAdd() {
this.potentialLinksToAdd = [];
function cartesian(args) {
if (!args.length) return [];
let r = [], max = args.length - 1;
function helper(arr, i) {
for (let j = 0; j < args[i].length; j++) {
let a = arr.slice(0);
a.push(args[i][j]);
(i === max) ? r.push(a) : helper(a, i + 1);
}
}
helper([], 0);
return r;
}
let combinations = [];
Object.keys(this.selectedPotentialLinkFacts).forEach((factName) => {
combinations.push(this.selectedPotentialLinkFacts[factName].filter((fact) => fact.selected).map((fact) => `${factName}|${fact.value}`));
});
combinations = cartesian(combinations);
let executor;
if (this.selectedPotentialLink.executors) {
executor = this.selectedPotentialLink.executors.find((ex) => ex.platform === this.potentialLink.agent.platform && this.potentialLink.executor === ex.name);
}
if (!combinations.length) {
this.potentialLinksToAdd.push({
ability: this.selectedPotentialLink,
paw: this.potentialLink.agent.paw,
executor: {
...executor,
command: this.potentialLinkCommand
}
})
this.isAddingBlankPotentialLink = true;
return;
}
combinations.forEach((factGroup) => {
let command = this.potentialLinkCommand;
factGroup.forEach((fact) => {
let split = fact.split('|');
command = command.replaceAll(`#{${split[0]}}`, split[1])
});
this.potentialLinksToAdd.push({
ability: this.selectedPotentialLink,
paw: this.potentialLink.agent.paw,
executor: {
...executor,
command: command
}
});
});
this.isAddingBlankPotentialLink = false;
},
async addPotentialLinks() {
if (this.selectedOperation.state === 'paused') {
toast('Operation is currently paused, and new links might not be added.', true);
}
try {
for (let link of this.potentialLinksToAdd) {
await apiV2('POST', `${this.ENDPOINT}/${this.selectedOperationID}/potential-links`, link);
}
toast(`Added ${this.potentialLinksToAdd.length} links.`, true);
this.getOperation();
this.addedTechniquesIDs.add(this.potentialLinksToAdd.map((link) => link.ability.technique_id));
this.openModal = false;
this.selectPotentialLink({});
} catch (error) {
toast('Error adding potential link.');
console.error(error);
}
},
getLink(link) {
apiV2('GET', `${this.ENDPOINT}/${this.selectedOperationID}/links/${link.id}`)
.then((res) => {
this.getLinkResults(res.output, link);
if (res.facts) {
this.selectedLinkFacts = Array.from(new Set(res.facts)).sort((a, b) => b.score - a.score);
}
this.selectedLinkCommand = res.command;
this.selectedLinkPlaintextCommand = res.plaintext_command;
this.editableCommand = res.command;
})
.catch(() => {
this.selectedLinkResults = null;
this.selectedLinkFacts = null;
this.selectedLinkCommand = null;
this.selectedLinkPlaintextCommand = null;
toast('Error getting link results.');
});
},
getLinkResults(output, link) {
if (output !== 'True') {
this.selectedLinkResults = null;
} else {
apiV2('GET', `${this.ENDPOINT}/${this.selectedOperationID}/links/${link.id}/result`).then((res) => {
this.selectedLinkResults = JSON.parse(b64DecodeUnicode(res.result));
}).catch(() => console.error('Error getting link results.'));
}
},
updateLink(status, command = null) {
const updateLink = {
...this.selectedLink,
command: this.selectedLink.command
};
if (command) {
updateLink.command = command;
}
updateLink.status = status;
apiV2('PATCH', `${this.ENDPOINT}/${this.selectedOperationID}/links/${this.selectedLink.id}`, updateLink)
.then((res) => {
this.getOperation();
toast(`Updating link status to: ${this.LINK_STATUSES[status]}`, true);
})
.catch(() => toast('Error updating link state.'));
},
handleSelectAdversary(adversaryId) {
let adversaryMatch = this.ADVERSARIES.find((a) => a.adversary_id === this.operationToStart.adversary.adversary_id);
let lastAdversaryHadRepeatableAbilities = this.isSelectedAdversaryRepeatable;
this.isSelectedAdversaryRepeatable = adversaryMatch.has_repeatable_abilities;
if (this.isSelectedAdversaryRepeatable) {
let plannerMatch = this.PLANNERS.find((p) => p.id === this.operationToStart.planner.id);
if (plannerMatch && !plannerMatch.allow_repeatable_abilities) {
this.operationToStart.planner.id = this.PLANNERS.find((p) => p.allow_repeatable_abilities).id;
}
} else if (!this.isSelectedAdversaryRepeatable && lastAdversaryHadRepeatableAbilities) {
this.operationToStart.planner.id = this.PLANNERS[0].id;
}
},
updateOperation(field, updateValue) {
this.selectedOperation[field] = updateValue;
apiV2('PATCH', `${this.ENDPOINT}/${this.selectedOperationID}`, this.selectedOperation)
.then((res) => {
this.selectedOperation = res;
this.selectedLink = null;
})
.catch(() => {
toast(`Error updating operation ${field}. Operation may have already finished running.`);
});
},
updateOperationState() {
if (this.selectedOperationID) {
apiV2('GET', `${this.ENDPOINT}/${this.selectedOperationID}`).then((res) => {
this.selectedOperation.state = res.state;
// toast('Go to the GameBoard plugin to see if the blue team can detect your activities.', true);
}).catch(() => {
toast('Error getting operation state');
});
}
},
downloadInfo(formatType) {
const endpoint = formatType === 'full-report' ? 'report' : formatType;
apiV2('POST', `${this.ENDPOINT}/${this.selectedOperationID}/${endpoint}`, { enable_agent_output: this.isAgentOutputSelected })
.then((res) => {
this.createDownloadReport(res, `${this.selectedOperation.name}_${formatType}`);
})
.catch(() => toast(`Error generating operation ${formatType.replace('-', ' ')}`));
},
downloadCSV() {
let csv = [ 'Decide,Link/Ability Name,Agent #paw,Host,pid,Command,Plaintext Command' ];
this.selectedOperation.chain.forEach(async (link) => {
const rowItems = [];
rowItems.push(link.decide);
rowItems.push(link.ability.name);
rowItems.push(link.paw);
rowItems.push(link.host);
rowItems.push(link.pid);
rowItems.push(link.command);
rowItems.push(link.plaintext_command);
csv.push(rowItems.join(','));
});
this.createDownloadReport(new Blob([csv.join('\n')], { type: 'text/csv' }), this.selectedOperation.name, true);
},
handleDownload() {
if (this.selectedReportType === 'full-report') {
this.downloadInfo('full-report');
} else if (this.selectedReportType === 'event-logs') {
this.downloadInfo('event-logs');
} else if (this.selectedReportType === 'csv') this.downloadCSV();
},
//
// OBJECT MANIPULATION
//
createDownloadReport(data, fileName, isCSV = false) {
let dataURL;
if (!isCSV) {
dataURL = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(data, null, 2))}`;
fileName += '.json';
} else {
dataURL = window.URL.createObjectURL(data);
fileName += '.csv';
}
const elem = document.createElement('a');
elem.setAttribute('href', dataURL);
elem.setAttribute('download', fileName);
elem.click();
},
updateAgentGroups(agents) {
if (!agents) return;
this.agentGroups = [...new Set(agents.map((agent) => agent.group))];
},
isJitterValid() {
const jitterMin = parseInt(this.jitterMin, 10);
const jitterMax = parseInt(this.jitterMax, 10);
return (jitterMin >= 0 && jitterMax >= 0 && jitterMin <= jitterMax);
},
filterAbilities(abilities) {
this.filteredAbilitiesList = abilities.filter((ability) => {
let matchAgent = true,
matchTactic = true,
matchTechnique = true;
if (this.potentialLink.executor) matchAgent = ability.executors.some((ex) => ex.platform === this.potentialLink.agent.platform && this.potentialLink.executor === ex.name);
if (this.potentialLink.tactic) matchTactic = this.potentialLink.tactic === ability.tactic;
if (this.potentialLink.technique) matchTechnique = this.potentialLink.technique === `${ability.technique_id} | ${ability.technique_name}`;
matchQuery = ability.name.toLowerCase().includes(this.potentialLinkSearchQuery.toLowerCase()) || ability.description.toLowerCase().includes(this.potentialLinkSearchQuery.toLowerCase());
return matchAgent && matchTactic && matchTechnique && matchQuery;
});
},
};
}
</script>
<style>
#operationsPage #select-operation {
max-width: 800px;
margin: 0 auto;
}
#operationsPage .control-buttons > .button {
margin: 0 10px 10px 0;
}
#operationsPage .modal-card {
max-height: 90vh;
}
#operationsPage .toggleContainer label {
font-size: 1em !important;
}
#operationsPage .box.op-ability {
position: relative;
cursor: pointer;
border: 1px solid transparent;
background-color: #272727;
}
#operationsPage .box.op-ability:hover {
border: 1px solid #474747;
}
#operationsPage .ability-icon {
position: absolute;
right: 0;
top: 20%;
}
/*MODAL FORM STYLE*/
#operationsPage .modal-form-fields.download-buttons-container button {
width: fit-content;
}
/*END MODAL FORM STYLE*/
#operationsPage .timeline-table .modal-card code, table code, .modal-form code {
color: #ff8181;
background-color: initial;
white-space: pre-line;
}
#operationsPage .timeline-table {
padding-bottom: 5em;
}
#operationsPage .timeline-table table tr th {
font-weight: 400;
font-size: 0.75rem;
border-bottom: none;
color: gray;
}
#operationsPage .tag {
border-bottom: none;
}
#operationsPage .button:active, .button.is-active, .button:focus, .button.is-focused {
color: inherit;
}
#operationsPage button.is-larger {
font-size: 2.1rem;
}
#operationsPage button.no-style {
background-color: transparent;
border: none;
box-shadow: none;
}
#operationsPage button.no-style.reset {
padding: 0.5em;
width: fit-content;
font-size: .75em;
}
#operationsPage button.no-style[disabled] {
background-color: initial !important;
}
#operationsPage button.no-style:hover {
border: none;
color: white;
transform: scale(1.2);
}
#operationsPage em sub {
font-family: sans-serif;
}
#operationsPage .controls-container button {
margin: 0 2rem;
padding: 0;
max-height: 34px;
}
#operationsPage .timeline-table th:not(.decideColumn), th:not(.statusColumn) {
text-align: left;
}
#operationsPage .timeline-table .decideColumn {
color: gray;
width: 120px;
word-break: break-word;
font-size: 0.75rem;
text-align: center !important;
}
#operationsPage .timeline-table .infoColumn {
width: 200px;
}
#operationsPage .timeline-table table tbody td {
vertical-align: middle;
border: none;
height: 5.5em;
}
#operationsPage .timeline-table table tbody td.commandDetails textarea {
color: #ff8181;
background-color: #262626;
height: 150px;
max-width: 22ch;
word-break: break-word;
padding: 5px;
border-radius: 5px;
}
#operationsPage .timeline-table table tbody td textarea[contenteditable] {
width: 100%;
height: 5em;
margin-top: 1em;
}
#operationsPage .timeline-table table tbody td.commandDetails button {
width: max-content;
}
#operationsPage .timeline-table table tfoot td {
padding-top: 1em;
border: none !important;
}
/* white time-line */
#operationsPage span.linkStatus {
justify-content: flex-start;
display: flex;
}
#operationsPage span.linkStatus::before {
width: 2px;
height: 180px;
background-color: white;
content: "";
display: inline-block;
position: absolute;
margin-left: 25px;
margin-top: 40px;
}
/*circle for event*/
#operationsPage span.linkStatus::after {
position: relative;
display: block;
background: #161616;
border-radius: 50%;
height: 50px;
width: 50px;
content: attr(data-content);
padding-top: 18px;
text-align: center;
font-size: 0.5em;
}
#operationsPage span.status {
position: relative;
top: 55px;
z-index: 10;
font-size: .5em;
color: white;
left: 15px;
display: flex;
}
/*queued*/
#operationsPage span.status-queued {
color: #555;
}
#operationsPage span.status-queued::after {
box-shadow: 0 0 0 0.2em #555;
}
/*success*/
#operationsPage span.status-success {
color: #4a9;
}
#operationsPage span.status-success::after {
box-shadow: 0 0 0 0.2em #4a9;
}
/*failure*/
#operationsPage span.status-failed {
color: #c31;
}
#operationsPage span.status-failed::after {
box-shadow: 0 0 0 0.2em #c31;
}
/*paused*/
#operationsPage span.status-paused {
color: #ffc500;
}
#operationsPage span.status-paused::after {
box-shadow: 0 0 0 0.2em #ffc500;
}
/*removed*/
#operationsPage span.status-discarded {
color: #a05195;
}
#operationsPage span.status-discarded::after {
box-shadow: 0 0 0 0.2em #a05195;
}
/*collected*/
#operationsPage span.status-collect {
color: #ffb000;
}
#operationsPage span.status-collect::after {
box-shadow: 0 0 0 0.2em #ffb000;
}
/*untrusted*/
#operationsPage span.status-untrusted {
color: white;
}
#operationsPage span.status-untrusted::after {
box-shadow: 0 0 0 0.2em white;
}
/*visible*/
#operationsPage span.status-visible {
color: #f012be;
}
#operationsPage span.status-visible::after {
box-shadow: 0 0 0 0.2em #f012be;
}
/*timeout*/
#operationsPage span.status-timeout {
color: cornflowerblue;
}
#operationsPage span.status-timeout::after {
box-shadow: 0 0 0 0.2em cornflowerblue;
}
</style>