From 76a3f4790f2ca7d4f901d5dc682311aca8ad50eb Mon Sep 17 00:00:00 2001 From: Joel Smith Date: Wed, 4 Nov 2020 10:58:21 -0800 Subject: [PATCH] Add create operation tray menu option (#66) Closes #66 --- ashirt.pro | 2 + src/dtos/ashirt_error.h | 27 +++++ src/dtos/operation.h | 15 +++ src/forms/add_operation/createoperation.cpp | 125 ++++++++++++++++++++ src/forms/add_operation/createoperation.h | 46 +++++++ src/helpers/netman.h | 7 ++ src/traymanager.cpp | 7 ++ src/traymanager.h | 3 + 8 files changed, 232 insertions(+) create mode 100644 src/dtos/ashirt_error.h create mode 100644 src/forms/add_operation/createoperation.cpp create mode 100644 src/forms/add_operation/createoperation.h diff --git a/ashirt.pro b/ashirt.pro index ecec47b..f08f01a 100644 --- a/ashirt.pro +++ b/ashirt.pro @@ -61,6 +61,7 @@ SOURCES += \ src/components/tagging/tagview.cpp \ src/components/tagging/tagwidget.cpp \ src/db/databaseconnection.cpp \ + src/forms/add_operation/createoperation.cpp \ src/forms/evidence_filter/evidencefilter.cpp \ src/forms/evidence_filter/evidencefilterform.cpp \ src/forms/getinfo/getinfo.cpp \ @@ -101,6 +102,7 @@ HEADERS += \ src/dtos/checkConnection.h \ src/exceptions/databaseerr.h \ src/exceptions/fileerror.h \ + src/forms/add_operation/createoperation.h \ src/forms/evidence_filter/evidencefilter.h \ src/forms/evidence_filter/evidencefilterform.h \ src/forms/getinfo/getinfo.h \ diff --git a/src/dtos/ashirt_error.h b/src/dtos/ashirt_error.h new file mode 100644 index 0000000..4fad775 --- /dev/null +++ b/src/dtos/ashirt_error.h @@ -0,0 +1,27 @@ +#ifndef DTO_ASHIRT_ERROR_H +#define DTO_ASHIRT_ERROR_H + +#include + +#include "helpers/jsonhelpers.h" + +namespace dto { +class AShirtError { + + public: + QString error = ""; + + static AShirtError parseData(QByteArray data) { + return parseJSONItem(data, AShirtError::fromJson); + } + + private: + static AShirtError fromJson(QJsonObject obj) { + AShirtError e; + e.error = obj["error"].toString(); + + return e; + } +}; +} +#endif // DTO_ASHIRT_ERROR_H diff --git a/src/dtos/operation.h b/src/dtos/operation.h index 09a025b..1f99e10 100644 --- a/src/dtos/operation.h +++ b/src/dtos/operation.h @@ -13,6 +13,10 @@ namespace dto { class Operation { public: Operation() {} + Operation(QString name, QString slug) { + this->name = name; + this->slug = slug; + } enum OperationStatus { OperationStatusPlanning = 0, @@ -34,6 +38,17 @@ class Operation { return parseJSONList(data, Operation::fromJson); } + static QByteArray createOperationJson(QString name, QString slug) { + return createOperationJson(Operation(name, slug)); + } + + static QByteArray createOperationJson(Operation o) { + QJsonObject obj; + obj.insert("slug", o.slug); + obj.insert("name", o.name); + return QJsonDocument(obj).toJson(); + } + private: // provides a Operation from a given QJsonObject static Operation fromJson(QJsonObject obj) { diff --git a/src/forms/add_operation/createoperation.cpp b/src/forms/add_operation/createoperation.cpp new file mode 100644 index 0000000..820d906 --- /dev/null +++ b/src/forms/add_operation/createoperation.cpp @@ -0,0 +1,125 @@ +#include "createoperation.h" + +#include + +#include "helpers/netman.h" +#include "helpers/stopreply.h" +#include "dtos/ashirt_error.h" + +CreateOperation::CreateOperation(QWidget* parent) : QDialog(parent) { + buildUi(); + wireUi(); +} + +CreateOperation::~CreateOperation() { + delete closeWindowAction; + delete submitButton; + delete _operationLabel; + delete responseLabel; + delete operationNameTextBox; + + delete gridLayout; + stopReply(&createOpReply); +} + +void CreateOperation::buildUi() { + gridLayout = new QGridLayout(this); + + submitButton = new LoadingButton("Submit", this); + submitButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + _operationLabel = new QLabel("Operation Name", this); + _operationLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + responseLabel = new QLabel(this); + operationNameTextBox = new QLineEdit(this); + + // Layout + /* 0 1 2 + +---------------+-------------+------------+ + 0 | Op Lbl | [Operation TB] | + +---------------+-------------+------------+ + 1 | Error Lbl | + +---------------+-------------+------------+ + 2 | | | Submit Btn | + +---------------+-------------+------------+ + */ + + // row 0 + gridLayout->addWidget(_operationLabel, 0, 0); + gridLayout->addWidget(operationNameTextBox, 0, 1, 1, 2); + + // row 1 + gridLayout->addWidget(responseLabel, 1, 0, 1, 3); + + // row 2 + gridLayout->addWidget(submitButton, 2, 2); + + closeWindowAction = new QAction(this); + closeWindowAction->setShortcut(QKeySequence::Close); + this->addAction(closeWindowAction); + + this->setLayout(gridLayout); + this->resize(400, 1); + this->setWindowTitle("Create Operation"); + + Qt::WindowFlags flags = this->windowFlags(); + flags |= Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowMinMaxButtonsHint | + Qt::WindowCloseButtonHint; + this->setWindowFlags(flags); +} + +void CreateOperation::wireUi() { + connect(submitButton, &QPushButton::clicked, this, &CreateOperation::submitButtonClicked); +} + +void CreateOperation::submitButtonClicked() { + responseLabel->setText(""); + auto name = operationNameTextBox->text().trimmed(); + auto slug = makeSlugFromName(name); + + if (slug == "") { + responseLabel->setText( + (name == "") + ? "The Operation Name must not be empty" + : "The Operation Name must include letters or numbers" + ); + return; + } + + submitButton->startAnimation(); + submitButton->setEnabled(false); + createOpReply = NetMan::getInstance().createOperation(name, slug); + connect(createOpReply, &QNetworkReply::finished, this, &CreateOperation::onRequestComplete); +} + +QString CreateOperation::makeSlugFromName(QString name) { + static QRegularExpression invalidCharsRegex("[^A-Za-z0-9]+"); + static QRegularExpression startOrEndDash("^-|-$"); + + return name.toLower().replace(invalidCharsRegex, "-").replace(startOrEndDash, ""); +} + +void CreateOperation::onRequestComplete() { + bool isValid; + auto data = NetMan::extractResponse(createOpReply, isValid); + if (isValid) { + dto::Operation op = dto::Operation::parseData(data); + AppSettings::getInstance().setOperationDetails(op.slug, op.name); + operationNameTextBox->clear(); + this->close(); + } + else { + dto::AShirtError err = dto::AShirtError::parseData(data); + if (err.error.contains("slug already exists")) { + responseLabel->setText("A similar operation name already exists. Please try a new name."); + } + else { + responseLabel->setText("Got an unexpected error: " + err.error); + } + } + + submitButton->stopAnimation(); + submitButton->setEnabled(true); + + tidyReply(&createOpReply); +} diff --git a/src/forms/add_operation/createoperation.h b/src/forms/add_operation/createoperation.h new file mode 100644 index 0000000..a311c52 --- /dev/null +++ b/src/forms/add_operation/createoperation.h @@ -0,0 +1,46 @@ +#ifndef FORM_CREATEOPERATION_H +#define FORM_CREATEOPERATION_H + +#include +#include +#include +#include +#include +#include + +#include "components/loading_button/loadingbutton.h" + +class CreateOperation : public QDialog { + Q_OBJECT + + public: + explicit CreateOperation(QWidget *parent = nullptr); + ~CreateOperation(); + + private: + void buildUi(); + void wireUi(); + + void submitButtonClicked(); + + private slots: + void onRequestComplete(); + + + QString makeSlugFromName(QString name); +// void showEvent(QShowEvent *evt) override; + + private: + + QNetworkReply* createOpReply = nullptr; + + // ui elements + QGridLayout* gridLayout = nullptr; + QAction* closeWindowAction = nullptr; + LoadingButton* submitButton = nullptr; + QLabel* _operationLabel = nullptr; + QLabel* responseLabel = nullptr; + QLineEdit* operationNameTextBox = nullptr; +}; + +#endif // FORM_CREATEOPERATION_H diff --git a/src/helpers/netman.h b/src/helpers/netman.h index ecb3f20..3d6ad13 100644 --- a/src/helpers/netman.h +++ b/src/helpers/netman.h @@ -234,6 +234,13 @@ class NetMan : public QObject { return builder->execute(nam); } + /// createOperation attempts to create a new operation with the given name and slug + QNetworkReply *createOperation(QString name, QString slug) { + auto builder = ashirtJSONPost("/api/operations", dto::Operation::createOperationJson(name, slug)); + addASHIRTAuth(builder); + return builder->execute(nam); + } + /// extractResponse inspects the provided QNetworkReply and returns back the contents of the reply. /// In addition, it will also indicated, via the provided valid flag, if the response was valid. /// A Valid response is one that has a 200 or 201 response AND had no errors flaged from Qt diff --git a/src/traymanager.cpp b/src/traymanager.cpp index ecd764d..9264696 100644 --- a/src/traymanager.cpp +++ b/src/traymanager.cpp @@ -99,6 +99,7 @@ void TrayManager::buildUi() { settingsWindow = new Settings(hotkeyManager, this); evidenceManagerWindow = new EvidenceManager(db, this); creditsWindow = new Credits(this); + createOperationWindow = new CreateOperation(this); trayIconMenu = new QMenu(this); chooseOpSubmenu = new QMenu(tr("Select Operation")); @@ -126,7 +127,10 @@ void TrayManager::buildUi() { currentOperationMenuAction->setEnabled(false); chooseOpStatusAction = new QAction("Loading operations...", chooseOpSubmenu); chooseOpStatusAction->setEnabled(false); + newOperationAction = new QAction("New Operation", chooseOpSubmenu); + newOperationAction->setEnabled(false); // only enable when we have an internet connection chooseOpSubmenu->addAction(chooseOpStatusAction); + chooseOpSubmenu->addAction(newOperationAction); chooseOpSubmenu->addSeparator(); setActiveOperationLabel(); @@ -158,6 +162,7 @@ void TrayManager::wireUi() { connect(showEvidenceManagerAction, actTriggered, [this, toTop](){toTop(evidenceManagerWindow);}); connect(showCreditsAction, actTriggered, [this, toTop](){toTop(creditsWindow);}); connect(addCodeblockAction, actTriggered, this, &TrayManager::captureCodeblockActionTriggered); + connect(newOperationAction, actTriggered, [this, toTop](){toTop(createOperationWindow);}); connect(screenshotTool, &Screenshot::onScreenshotCaptured, this, &TrayManager::onScreenshotCaptured); @@ -180,6 +185,7 @@ void TrayManager::wireUi() { connect(trayIcon, &QSystemTrayIcon::messageClicked, [](){QDesktopServices::openUrl(Constants::releasePageUrl());}); connect(trayIcon, &QSystemTrayIcon::activated, [this] { chooseOpStatusAction->setText("Loading operations..."); + newOperationAction->setEnabled(false); NetMan::getInstance().refreshOperationsList(); }); @@ -296,6 +302,7 @@ void TrayManager::onOperationListUpdated(bool success, if (success) { chooseOpStatusAction->setText(tr("Operations loaded")); + newOperationAction->setEnabled(true); cleanChooseOpSubmenu(); for (const auto& op : operations) { diff --git a/src/traymanager.h b/src/traymanager.h index b661e17..eb6f110 100644 --- a/src/traymanager.h +++ b/src/traymanager.h @@ -16,6 +16,7 @@ #include "helpers/screenshot.h" #include "hotkeymanager.h" #include "tools/UGlobalHotkey/uglobalhotkeys.h" +#include "forms/add_operation/createoperation.h" #ifndef QT_NO_SYSTEMTRAYICON @@ -75,6 +76,7 @@ class TrayManager : public QDialog { Settings *settingsWindow = nullptr; EvidenceManager *evidenceManagerWindow = nullptr; Credits *creditsWindow = nullptr; + CreateOperation *createOperationWindow = nullptr; // UI Elements QSystemTrayIcon *trayIcon = nullptr; @@ -91,6 +93,7 @@ class TrayManager : public QDialog { QMenu *chooseOpSubmenu = nullptr; QAction *chooseOpStatusAction = nullptr; + QAction *newOperationAction = nullptr; QAction *selectedAction = nullptr; // note: do not delete; for reference only std::vector allOperationActions; };