| <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 </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> |