parent
240ecdc3d5
commit
4727200bc9
|
@ -31,7 +31,7 @@ void TagCache::requestTags(QString operationSlug) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto reply = NetMan::getInstance().getOperationTags(operationSlug);
|
auto reply = NetMan::getOperationTags(operationSlug);
|
||||||
tagRequests.insert(operationSlug, reply);
|
tagRequests.insert(operationSlug, reply);
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply, operationSlug]() {
|
connect(reply, &QNetworkReply::finished, this, [this, reply, operationSlug]() {
|
||||||
onGetTagsComplete(reply, operationSlug);
|
onGetTagsComplete(reply, operationSlug);
|
||||||
|
|
|
@ -179,7 +179,7 @@ void TagEditor::createTag(QString tagName) {
|
||||||
tagCompleteTextBox->setEnabled(false);
|
tagCompleteTextBox->setEnabled(false);
|
||||||
|
|
||||||
dto::Tag newTag(newText, TagWidget::randomColor());
|
dto::Tag newTag(newText, TagWidget::randomColor());
|
||||||
createTagReply = NetMan::getInstance().createTag(newTag, operationSlug);
|
createTagReply = NetMan::createTag(newTag, operationSlug);
|
||||||
connect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
|
connect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ void CreateOperation::submitButtonClicked() {
|
||||||
|
|
||||||
submitButton->startAnimation();
|
submitButton->startAnimation();
|
||||||
submitButton->setEnabled(false);
|
submitButton->setEnabled(false);
|
||||||
createOpReply = NetMan::getInstance().createOperation(name, slug);
|
createOpReply = NetMan::createOperation(name, slug);
|
||||||
connect(createOpReply, &QNetworkReply::finished, this, &CreateOperation::onRequestComplete);
|
connect(createOpReply, &QNetworkReply::finished, this, &CreateOperation::onRequestComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ void CreateOperation::onRequestComplete() {
|
||||||
dto::Operation op = dto::Operation::parseData(data);
|
dto::Operation op = dto::Operation::parseData(data);
|
||||||
AppSettings::getInstance().setOperationDetails(op.slug, op.name);
|
AppSettings::getInstance().setOperationDetails(op.slug, op.name);
|
||||||
operationNameTextBox->clear();
|
operationNameTextBox->clear();
|
||||||
NetMan::getInstance().refreshOperationsList();
|
NetMan::refreshOperationsList();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -17,7 +17,7 @@ Credits::Credits(QWidget* parent)
|
||||||
, updateLabel(new QLabel(this))
|
, updateLabel(new QLabel(this))
|
||||||
{
|
{
|
||||||
setWindowTitle("About");
|
setWindowTitle("About");
|
||||||
connect(&NetMan::getInstance(), &NetMan::releasesChecked, this, &Credits::onReleasesUpdate);
|
connect(NetMan::get(), &NetMan::releasesChecked, this, &Credits::onReleasesUpdate);
|
||||||
|
|
||||||
updateLabel->setVisible(false);
|
updateLabel->setVisible(false);
|
||||||
updateLabel->setOpenExternalLinks(true);
|
updateLabel->setOpenExternalLinks(true);
|
||||||
|
|
|
@ -201,7 +201,7 @@ void EvidenceManager::submitEvidenceTriggered() {
|
||||||
evidenceIDForRequest = selectedRowEvidenceID();
|
evidenceIDForRequest = selectedRowEvidenceID();
|
||||||
try {
|
try {
|
||||||
model::Evidence evi = db->getEvidenceDetails(evidenceIDForRequest);
|
model::Evidence evi = db->getEvidenceDetails(evidenceIDForRequest);
|
||||||
uploadAssetReply = NetMan::getInstance().uploadAsset(evi);
|
uploadAssetReply = NetMan::uploadAsset(evi);
|
||||||
connect(uploadAssetReply, &QNetworkReply::finished, this, &EvidenceManager::onUploadComplete);
|
connect(uploadAssetReply, &QNetworkReply::finished, this, &EvidenceManager::onUploadComplete);
|
||||||
}
|
}
|
||||||
catch (QSqlError& e) {
|
catch (QSqlError& e) {
|
||||||
|
|
|
@ -123,8 +123,7 @@ void EvidenceFilterForm::wireUi() {
|
||||||
submittedComboBox->installEventFilter(this);
|
submittedComboBox->installEventFilter(this);
|
||||||
contentTypeComboBox->installEventFilter(this);
|
contentTypeComboBox->installEventFilter(this);
|
||||||
|
|
||||||
connect(&NetMan::getInstance(), &NetMan::operationListUpdated, this,
|
connect(NetMan::get(), &NetMan::operationListUpdated, this, &EvidenceFilterForm::onOperationListUpdated);
|
||||||
&EvidenceFilterForm::onOperationListUpdated);
|
|
||||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &EvidenceFilterForm::writeAndClose);
|
connect(buttonBox, &QDialogButtonBox::accepted, this, &EvidenceFilterForm::writeAndClose);
|
||||||
|
|
||||||
connect(includeStartDateCheckBox, &QCheckBox::stateChanged, fromDateEdit, &QDateEdit::setEnabled);
|
connect(includeStartDateCheckBox, &QCheckBox::stateChanged, fromDateEdit, &QDateEdit::setEnabled);
|
||||||
|
|
|
@ -91,7 +91,7 @@ void GetInfo::submitButtonClicked() {
|
||||||
if (saveData()) {
|
if (saveData()) {
|
||||||
try {
|
try {
|
||||||
model::Evidence evi = db->getEvidenceDetails(evidenceID);
|
model::Evidence evi = db->getEvidenceDetails(evidenceID);
|
||||||
uploadAssetReply = NetMan::getInstance().uploadAsset(evi);
|
uploadAssetReply = NetMan::uploadAsset(evi);
|
||||||
connect(uploadAssetReply, &QNetworkReply::finished, this, &GetInfo::onUploadComplete);
|
connect(uploadAssetReply, &QNetworkReply::finished, this, &GetInfo::onUploadComplete);
|
||||||
}
|
}
|
||||||
catch (QSqlError& e) {
|
catch (QSqlError& e) {
|
||||||
|
|
|
@ -220,7 +220,7 @@ void Settings::onSaveClicked() {
|
||||||
QString originalApiUrl = inst.apiURL;
|
QString originalApiUrl = inst.apiURL;
|
||||||
inst.apiURL = hostPathTextBox->text();
|
inst.apiURL = hostPathTextBox->text();
|
||||||
if (originalApiUrl != hostPathTextBox->text()) {
|
if (originalApiUrl != hostPathTextBox->text()) {
|
||||||
NetMan::getInstance().refreshOperationsList();
|
NetMan::refreshOperationsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
inst.screenshotExec = captureAreaCmdTextBox->text();
|
inst.screenshotExec = captureAreaCmdTextBox->text();
|
||||||
|
@ -261,7 +261,7 @@ void Settings::onTestConnectionClicked() {
|
||||||
}
|
}
|
||||||
testConnectionButton->startAnimation();
|
testConnectionButton->startAnimation();
|
||||||
testConnectionButton->setEnabled(false);
|
testConnectionButton->setEnabled(false);
|
||||||
currentTestReply = NetMan::getInstance().testConnection(
|
currentTestReply = NetMan::testConnection(
|
||||||
hostPathTextBox->text(), accessKeyTextBox->text(), secretKeyTextBox->text());
|
hostPathTextBox->text(), accessKeyTextBox->text(), secretKeyTextBox->text());
|
||||||
connect(currentTestReply, &QNetworkReply::finished, this, &Settings::onTestRequestComplete);
|
connect(currentTestReply, &QNetworkReply::finished, this, &Settings::onTestRequestComplete);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
public:
|
public:
|
||||||
inline static const auto unknownValue = QStringLiteral("???");
|
|
||||||
inline static const auto dbLocation = QStringLiteral("%1/evidence.sqlite").arg(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
|
inline static const auto dbLocation = QStringLiteral("%1/evidence.sqlite").arg(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
|
||||||
inline static const auto defaultEvidenceRepo = QStringLiteral("%1/evidence").arg(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
|
inline static const auto defaultEvidenceRepo = QStringLiteral("%1/evidence").arg(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
|
||||||
inline static const auto commitHash = QStringLiteral("%1").arg(COMMIT_HASH);
|
inline static const auto commitHash = QStringLiteral("%1").arg(COMMIT_HASH);
|
||||||
|
@ -57,8 +56,8 @@ class Constants {
|
||||||
QRegularExpressionMatch match = ownerRegex.match(rawRepo);
|
QRegularExpressionMatch match = ownerRegex.match(rawRepo);
|
||||||
// Note that the specific values for the error cases below don't matter
|
// Note that the specific values for the error cases below don't matter
|
||||||
// They are set to avoid rerunning the parsing (since these values won't change mid-run)
|
// They are set to avoid rerunning the parsing (since these values won't change mid-run)
|
||||||
parsedOwner = match.hasMatch() ? match.captured(1) : unknownValue;
|
parsedOwner = match.hasMatch() ? match.captured(1) : QString();
|
||||||
parsedRepo = match.hasMatch() ? match.captured(2) : unknownValue;
|
parsedRepo = match.hasMatch() ? match.captured(2) : QString();
|
||||||
}
|
}
|
||||||
return field == RepoField::owner ? parsedOwner : parsedRepo;
|
return field == RepoField::owner ? parsedOwner : parsedRepo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,228 +13,98 @@
|
||||||
#include "dtos/operation.h"
|
#include "dtos/operation.h"
|
||||||
#include "dtos/tag.h"
|
#include "dtos/tag.h"
|
||||||
#include "dtos/github_release.h"
|
#include "dtos/github_release.h"
|
||||||
#include "helpers/multipartparser.h"
|
#include "helpers/constants.h"
|
||||||
|
#include <helpers/multipartparser.h>
|
||||||
#include "helpers/stopreply.h"
|
#include "helpers/stopreply.h"
|
||||||
#include "models/evidence.h"
|
#include "models/evidence.h"
|
||||||
|
|
||||||
|
|
||||||
class NetMan : public QObject {
|
class NetMan : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
public:
|
||||||
public:
|
|
||||||
static NetMan &getInstance() {
|
|
||||||
static NetMan instance;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
NetMan(NetMan const &) = delete;
|
|
||||||
void operator=(NetMan const &) = delete;
|
|
||||||
// type alias QList<dto::Operation> to provide shorter lines
|
// type alias QList<dto::Operation> to provide shorter lines
|
||||||
using OperationVector = QList<dto::Operation>;
|
using OperationVector = QList<dto::Operation>;
|
||||||
|
static NetMan* get() {
|
||||||
signals:
|
static NetMan i;
|
||||||
void operationListUpdated(bool success, NetMan::OperationVector operations = NetMan::OperationVector());
|
return &i;
|
||||||
void releasesChecked(bool success, QList<dto::GithubRelease> releases = QList<dto::GithubRelease>());
|
|
||||||
void testConnectionComplete(bool connected, int statusCode);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QNetworkAccessManager *nam;
|
|
||||||
|
|
||||||
NetMan(QObject * parent = nullptr)
|
|
||||||
: QObject(parent)
|
|
||||||
, nam(new QNetworkAccessManager(this))
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
}
|
||||||
~NetMan() = default;
|
|
||||||
|
|
||||||
/// ashirtGet generates a basic GET request to the ashirt API server. No authentication is
|
|
||||||
/// provided (use addASHIRTAuth to do this)
|
|
||||||
/// Allows for an optional altHost parameter, in order to check for ashirt servers.
|
|
||||||
/// Normal usage should provide no value for this parameter.
|
|
||||||
RequestBuilder* ashirtGet(QString endpoint, const QString & altHost= QString()) {
|
|
||||||
QString base = (altHost.isEmpty()) ? AppConfig::getInstance().apiURL : altHost;
|
|
||||||
return RequestBuilder::newGet()
|
|
||||||
->setHost(base)
|
|
||||||
->setEndpoint(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ashirtJSONPost generates a basic POST request with content type application/json. No
|
|
||||||
/// authentication is provided (use addASHIRTAuth to do this)
|
|
||||||
RequestBuilder* ashirtJSONPost(QString endpoint, QByteArray body) {
|
|
||||||
return RequestBuilder::newJSONPost()
|
|
||||||
->setHost(AppConfig::getInstance().apiURL)
|
|
||||||
->setEndpoint(endpoint)
|
|
||||||
->setBody(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ashirtFormPost generates a basic POST request with content type multipart/form-data.
|
|
||||||
/// No authentication is provided (use addASHIRTAuth to do this)
|
|
||||||
RequestBuilder* ashirtFormPost(QString endpoint, QByteArray body, QString boundary) {
|
|
||||||
return RequestBuilder::newFormPost(boundary)
|
|
||||||
->setHost(AppConfig::getInstance().apiURL)
|
|
||||||
->setEndpoint(endpoint)
|
|
||||||
->setBody(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// addASHIRTAuth takes the provided RequestBuilder and adds on Authorization and Date headers
|
|
||||||
/// in order to properly authenticate with ASHIRT servers. Note that this should not be used for
|
|
||||||
/// non-ashirt requests
|
|
||||||
void addASHIRTAuth(RequestBuilder* reqBuilder, const QString& altApiKey = QString(),
|
|
||||||
const QString& altSecretKey = QString()) {
|
|
||||||
auto now = QDateTime::currentDateTimeUtc().toString(QStringLiteral("ddd, dd MMM yyyy hh:mm:ss 'GMT'"));
|
|
||||||
reqBuilder->addRawHeader(QStringLiteral("Date"), now);
|
|
||||||
|
|
||||||
// load default key if not present
|
|
||||||
QString apiKeyCopy = QString(altApiKey);
|
|
||||||
if (apiKeyCopy.isEmpty()) {
|
|
||||||
apiKeyCopy = AppConfig::getInstance().accessKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto code = generateHash(RequestMethodToString(reqBuilder->getMethod()),
|
|
||||||
reqBuilder->getEndpoint(), now, reqBuilder->getBody(), altSecretKey);
|
|
||||||
|
|
||||||
auto authValue = QStringLiteral("%1:%2").arg(apiKeyCopy, code);
|
|
||||||
reqBuilder->addRawHeader(QStringLiteral("Authorization"), authValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// generateHash provides a cryptographic hash for ASHIRT api server communication
|
|
||||||
QString generateHash(QString method, QString path, QString date, QByteArray body = NO_BODY,
|
|
||||||
const QString &secretKey = QString()) {
|
|
||||||
auto hashedBody = QCryptographicHash::hash(body, QCryptographicHash::Sha256);
|
|
||||||
std::string msg = (method + "\n" + path + "\n" + date + "\n").toStdString();
|
|
||||||
msg += hashedBody.toStdString();
|
|
||||||
|
|
||||||
QString secretKeyCopy = QString(secretKey);
|
|
||||||
if (secretKeyCopy.isEmpty()) {
|
|
||||||
secretKeyCopy = AppConfig::getInstance().secretKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMessageAuthenticationCode code(QCryptographicHash::Sha256);
|
|
||||||
QByteArray key = QByteArray::fromBase64(secretKeyCopy.toUtf8());
|
|
||||||
|
|
||||||
code.setKey(key);
|
|
||||||
code.addData(QByteArray::fromStdString(msg));
|
|
||||||
return code.result().toBase64();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// onGetOpsComplete is called when the network request associated with the method refreshOperationsList
|
|
||||||
/// completes. This will emit an operationListUpdated signal.
|
|
||||||
void onGetOpsComplete() {
|
|
||||||
bool isValid;
|
|
||||||
auto data = extractResponse(allOpsReply, isValid);
|
|
||||||
if (isValid) {
|
|
||||||
OperationVector ops = dto::Operation::parseDataAsList(data);
|
|
||||||
std::sort(ops.begin(), ops.end(),
|
|
||||||
[](dto::Operation i, dto::Operation j) { return i.name < j.name; });
|
|
||||||
|
|
||||||
Q_EMIT operationListUpdated(true, ops);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Q_EMIT operationListUpdated(false);
|
|
||||||
}
|
|
||||||
tidyReply(&allOpsReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// onGithubReleasesComplete is called when the network request associated with the method checkForNewRelease
|
|
||||||
/// completes. This will emit a releasesChecked signal
|
|
||||||
void onGithubReleasesComplete() {
|
|
||||||
bool isValid;
|
|
||||||
auto data = extractResponse(githubReleaseReply, isValid);
|
|
||||||
if (isValid) {
|
|
||||||
auto releases = dto::GithubRelease::parseDataAsList(data);
|
|
||||||
Q_EMIT releasesChecked(true, releases);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Q_EMIT releasesChecked(false);
|
|
||||||
}
|
|
||||||
tidyReply(&githubReleaseReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
|
|
||||||
/// uploadAsset takes the given Evidence model, encodes it (and the file), and uploads this
|
/// uploadAsset takes the given Evidence model, encodes it (and the file), and uploads this
|
||||||
/// to the configured ASHIRT API server. Returns a QNetworkReply to track the request
|
/// to the configured ASHIRT API server. Returns a QNetworkReply to track the request
|
||||||
/// Note: does not specify the occurred_at field, so occurred_at will reflect the time of upload,
|
/// Note: does not specify the occurred_at field, so occurred_at will reflect the time of upload,
|
||||||
/// rather than the time of capture.
|
/// rather than the time of capture.
|
||||||
QNetworkReply *uploadAsset(model::Evidence evidence) {
|
static QNetworkReply* uploadAsset(model::Evidence evidence) {
|
||||||
MultipartParser parser;
|
MultipartParser parser;
|
||||||
parser.addParameter(QStringLiteral("notes"), evidence.description);
|
parser.addParameter(QStringLiteral("notes"), evidence.description);
|
||||||
parser.addParameter(QStringLiteral("contentType"), evidence.contentType);
|
parser.addParameter(QStringLiteral("contentType"), evidence.contentType);
|
||||||
|
|
||||||
// TODO: convert this time below into a proper unix timestamp (mSecSinceEpoch and secsSinceEpoch
|
// TODO: convert this time below into a proper unix timestamp (mSecSinceEpoch and secsSinceEpoch
|
||||||
// produce invalid times)
|
// produce invalid times)
|
||||||
// parser.AddParameter("occurred_at", std::to_string(evidence.recordedDate);
|
// parser.AddParameter("occurred_at", std::to_string(evidence.recordedDate);
|
||||||
|
|
||||||
QStringList list;
|
QStringList list;
|
||||||
for (const auto& tag : evidence.tags) {
|
for (const auto& tag : evidence.tags)
|
||||||
list << QString::number(tag.serverTagId);
|
list.append(QString::number(tag.serverTagId));
|
||||||
}
|
|
||||||
parser.addParameter(QStringLiteral("tagIds"), QStringLiteral("[%1]").arg(list.join(QStringLiteral(","))));
|
parser.addParameter(QStringLiteral("tagIds"), QStringLiteral("[%1]").arg(list.join(QStringLiteral(","))));
|
||||||
|
|
||||||
parser.addFile(QStringLiteral("file"), evidence.path);
|
parser.addFile(QStringLiteral("file"), evidence.path);
|
||||||
|
auto builder = ashirtFormPost(QStringLiteral("/api/operations/%1/evidence").arg(evidence.operationSlug), parser.generateBody(), parser.boundary());
|
||||||
auto body = parser.generateBody();
|
|
||||||
|
|
||||||
auto builder = ashirtFormPost(QStringLiteral("/api/operations/%1/evidence").arg(evidence.operationSlug), body, parser.boundary());
|
|
||||||
addASHIRTAuth(builder);
|
addASHIRTAuth(builder);
|
||||||
return builder->execute(nam);
|
return builder->execute(get()->nam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// testConnection provides a mechanism to validate a given host, apikey and secret key, to test
|
/// testConnection provides a mechanism to validate a given host, apikey and secret key, to test
|
||||||
/// a connection to the ASHIRT API server
|
/// a connection to the ASHIRT API server
|
||||||
QNetworkReply *testConnection(QString host, QString apiKey, QString secretKey) {
|
static QNetworkReply *testConnection(QString host, QString apiKey, QString secretKey) {
|
||||||
auto builder = ashirtGet(QStringLiteral("/api/checkconnection"), host);
|
auto builder = ashirtGet(QStringLiteral("/api/checkconnection"), host);
|
||||||
addASHIRTAuth(builder, apiKey, secretKey);
|
addASHIRTAuth(builder, apiKey, secretKey);
|
||||||
return builder->execute(nam);
|
return builder->execute(get()->nam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// getAllOperations retrieves all (user-visble) operations from the configured ASHIRT API server.
|
/// getAllOperations retrieves all (user-visble) operations from the configured ASHIRT API server.
|
||||||
/// Note: normally you should opt to use refreshOperationsList and retrieve the results by listening
|
/// Note: normally you should opt to use refreshOperationsList and retrieve the results by listening
|
||||||
/// for the operationListUpdated signal.
|
/// for the operationListUpdated signal.
|
||||||
QNetworkReply *getAllOperations() {
|
static QNetworkReply *getAllOperations() {
|
||||||
auto builder = ashirtGet(QStringLiteral("/api/operations"));
|
auto builder = ashirtGet(QStringLiteral("/api/operations"));
|
||||||
addASHIRTAuth(builder);
|
addASHIRTAuth(builder);
|
||||||
return builder->execute(nam);
|
return builder->execute(get()->nam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// getGithubReleases retrieves the recent releases from github for the provided owner and repo.
|
/// getGithubReleases retrieves the recent releases from github for the provided owner and repo.
|
||||||
/// Note that normally you should call checkForNewRelease
|
/// Note that normally you should call checkForNewRelease
|
||||||
QNetworkReply *getGithubReleases(QString owner, QString repo) {
|
static QNetworkReply *getGithubReleases(QString owner, QString repo) {
|
||||||
return RequestBuilder::newGet()
|
return RequestBuilder::newGet()
|
||||||
->setHost(QStringLiteral("https://api.github.com"))
|
->setHost(QStringLiteral("https://api.github.com"))
|
||||||
->setEndpoint(QStringLiteral("/repos/%1/%2/releases").arg(owner, repo))
|
->setEndpoint(QStringLiteral("/repos/%1/%2/releases").arg(owner, repo))
|
||||||
->execute(nam);
|
->execute(get()->nam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// refreshOperationsList retrieves the operations currently visible to the user. Results should be
|
/// refreshOperationsList retrieves the operations currently visible to the user. Results should be
|
||||||
/// retrieved by listening for the operationListUpdated signal
|
/// retrieved by listening for the operationListUpdated signal
|
||||||
void refreshOperationsList() {
|
static void refreshOperationsList() {
|
||||||
if (allOpsReply == nullptr) {
|
if (get()->allOpsReply)
|
||||||
allOpsReply = getAllOperations();
|
return;
|
||||||
connect(allOpsReply, &QNetworkReply::finished, this, &NetMan::onGetOpsComplete);
|
get()->allOpsReply = get()->getAllOperations();
|
||||||
}
|
connect(get()->allOpsReply, &QNetworkReply::finished, get(), &NetMan::onGetOpsComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// getOperationTags retrieves the tags for specified operation from the ASHIRT API server
|
/// getOperationTags retrieves the tags for specified operation from the ASHIRT API server
|
||||||
QNetworkReply *getOperationTags(QString operationSlug) {
|
static QNetworkReply *getOperationTags(QString operationSlug) {
|
||||||
auto builder = ashirtGet(QStringLiteral("/api/operations/%1/tags").arg(operationSlug));
|
auto builder = ashirtGet(QStringLiteral("/api/operations/%1/tags").arg(operationSlug));
|
||||||
addASHIRTAuth(builder);
|
addASHIRTAuth(builder);
|
||||||
return builder->execute(nam);
|
return builder->execute(get()->nam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// createTag attempts to create a new tag for specified operation from the ASHIRT API server.
|
/// createTag attempts to create a new tag for specified operation from the ASHIRT API server.
|
||||||
QNetworkReply *createTag(dto::Tag tag, QString operationSlug) {
|
static QNetworkReply *createTag(dto::Tag tag, QString operationSlug) {
|
||||||
auto builder = ashirtJSONPost(QStringLiteral("/api/operations/%1/tags").arg(operationSlug), dto::Tag::toJson(tag));
|
auto builder = ashirtJSONPost(QStringLiteral("/api/operations/%1/tags").arg(operationSlug), dto::Tag::toJson(tag));
|
||||||
addASHIRTAuth(builder);
|
addASHIRTAuth(builder);
|
||||||
return builder->execute(nam);
|
return builder->execute(get()->nam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// createOperation attempts to create a new operation with the given name and slug
|
/// createOperation attempts to create a new operation with the given name and slug
|
||||||
QNetworkReply *createOperation(QString name, QString slug) {
|
static QNetworkReply *createOperation(QString name, QString slug) {
|
||||||
auto builder = ashirtJSONPost(QStringLiteral("/api/operations"), dto::Operation::createOperationJson(name, slug));
|
auto builder = ashirtJSONPost(QStringLiteral("/api/operations"), dto::Operation::createOperationJson(name, slug));
|
||||||
addASHIRTAuth(builder);
|
addASHIRTAuth(builder);
|
||||||
return builder->execute(nam);
|
return builder->execute(get()->nam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// extractResponse inspects the provided QNetworkReply and returns back the contents of the reply.
|
/// extractResponse inspects the provided QNetworkReply and returns back the contents of the reply.
|
||||||
|
@ -251,16 +121,118 @@ class NetMan : public QObject {
|
||||||
|
|
||||||
/// checkForNewRelease retrieves the recent releases from github for the provided owner/repo project.
|
/// checkForNewRelease retrieves the recent releases from github for the provided owner/repo project.
|
||||||
/// Callers should retrieve the result by listening for the releasesChecked signal
|
/// Callers should retrieve the result by listening for the releasesChecked signal
|
||||||
void checkForNewRelease(QString owner, QString repo) {
|
static void checkForNewRelease(QString owner, QString repo) {
|
||||||
if (owner == Constants::unknownValue || repo == Constants::unknownValue) {
|
if (owner.isEmpty() || repo.isEmpty()) {
|
||||||
QTextStream(stderr) << "Skipping release check: no owner or repo set." << Qt::endl;
|
QTextStream(stderr) << "Skipping release check: no owner or repo set." << Qt::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
githubReleaseReply = getGithubReleases(owner, repo);
|
get()->githubReleaseReply = get()->getGithubReleases(owner, repo);
|
||||||
connect(githubReleaseReply, &QNetworkReply::finished, this, &NetMan::onGithubReleasesComplete);
|
connect(get()->githubReleaseReply, &QNetworkReply::finished, get(), &NetMan::onGithubReleasesComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
signals:
|
||||||
QNetworkReply *allOpsReply = nullptr;
|
void operationListUpdated(bool success, NetMan::OperationVector operations = NetMan::OperationVector());
|
||||||
QNetworkReply *githubReleaseReply = nullptr;
|
void releasesChecked(bool success, QList<dto::GithubRelease> releases = QList<dto::GithubRelease>());
|
||||||
|
|
||||||
|
private:
|
||||||
|
NetMan(QObject * parent = nullptr) : QObject(parent), nam(new QNetworkAccessManager(this)) { }
|
||||||
|
NetMan(NetMan const &) = delete;
|
||||||
|
void operator=(NetMan const &) = delete;
|
||||||
|
~NetMan() = default;
|
||||||
|
|
||||||
|
/// ashirtGet generates a basic GET request to the ashirt API server. No authentication is
|
||||||
|
/// provided (use addASHIRTAuth to do this)
|
||||||
|
/// Allows for an optional altHost parameter, in order to check for ashirt servers.
|
||||||
|
/// Normal usage should provide no value for this parameter.
|
||||||
|
static RequestBuilder* ashirtGet(QString endpoint, const QString & altHost= QString()) {
|
||||||
|
QString base = (altHost.isEmpty()) ? AppConfig::getInstance().apiURL : altHost;
|
||||||
|
return RequestBuilder::newGet()
|
||||||
|
->setHost(base)
|
||||||
|
->setEndpoint(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ashirtJSONPost generates a basic POST request with content type application/json. No
|
||||||
|
/// authentication is provided (use addASHIRTAuth to do this)
|
||||||
|
static RequestBuilder* ashirtJSONPost(QString endpoint, QByteArray body) {
|
||||||
|
return RequestBuilder::newJSONPost()
|
||||||
|
->setHost(AppConfig::getInstance().apiURL)
|
||||||
|
->setEndpoint(endpoint)
|
||||||
|
->setBody(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ashirtFormPost generates a basic POST request with content type multipart/form-data.
|
||||||
|
/// No authentication is provided (use addASHIRTAuth to do this)
|
||||||
|
static RequestBuilder* ashirtFormPost(QString endpoint, QByteArray body, QString boundry) {
|
||||||
|
return RequestBuilder::newFormPost(boundry)
|
||||||
|
->setHost(AppConfig::getInstance().apiURL)
|
||||||
|
->setEndpoint(endpoint)
|
||||||
|
->setBody(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// addASHIRTAuth takes the provided RequestBuilder and adds on Authorization and Date headers
|
||||||
|
/// in order to properly authenticate with ASHIRT servers. Note that this should not be used for
|
||||||
|
/// non-ashirt requests
|
||||||
|
static void addASHIRTAuth(RequestBuilder* reqBuilder, const QString& altApiKey = QString(),
|
||||||
|
const QString& altSecretKey = QString()) {
|
||||||
|
auto now = QDateTime::currentDateTimeUtc().toString(QStringLiteral("ddd, dd MMM yyyy hh:mm:ss 'GMT'"));
|
||||||
|
reqBuilder->addRawHeader(QStringLiteral("Date"), now);
|
||||||
|
|
||||||
|
// load default key if not present
|
||||||
|
QString apiKeyCopy = altApiKey.isEmpty() ? AppConfig::getInstance().accessKey : QString(altApiKey);
|
||||||
|
|
||||||
|
auto code = generateHash(RequestMethodToString(reqBuilder->getMethod()),
|
||||||
|
reqBuilder->getEndpoint(), now, reqBuilder->getBody(), altSecretKey);
|
||||||
|
|
||||||
|
auto authValue = QStringLiteral("%1:%2").arg(apiKeyCopy, code);
|
||||||
|
reqBuilder->addRawHeader(QStringLiteral("Authorization"), authValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// generateHash provides a cryptographic hash for ASHIRT api server communication
|
||||||
|
static QString generateHash(QString method, QString path, QString date, QByteArray body = NO_BODY,
|
||||||
|
const QString &secretKey = QString()) {
|
||||||
|
|
||||||
|
QString msg = QStringLiteral("%1\n%2\n%3\n").arg(method, path, date);
|
||||||
|
QString secretKeyCopy = secretKey.isEmpty() ? AppConfig::getInstance().secretKey : QString(secretKey);
|
||||||
|
|
||||||
|
QMessageAuthenticationCode code(QCryptographicHash::Sha256);
|
||||||
|
code.setKey(QByteArray::fromBase64(secretKeyCopy.toUtf8()));
|
||||||
|
code.addData(msg.toLatin1());
|
||||||
|
code.addData(QCryptographicHash::hash(body, QCryptographicHash::Sha256));
|
||||||
|
return code.result().toBase64();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// onGetOpsComplete is called when the network request associated with the method refreshOperationsList
|
||||||
|
/// completes. This will emit an operationListUpdated signal.
|
||||||
|
static void onGetOpsComplete() {
|
||||||
|
bool isValid;
|
||||||
|
auto data = get()->extractResponse(get()->allOpsReply, isValid);
|
||||||
|
if (isValid) {
|
||||||
|
OperationVector ops = dto::Operation::parseDataAsList(data);
|
||||||
|
std::sort(ops.begin(), ops.end(),
|
||||||
|
[](dto::Operation i, dto::Operation j) { return i.name < j.name; });
|
||||||
|
|
||||||
|
Q_EMIT get()->operationListUpdated(true, ops);
|
||||||
|
} else {
|
||||||
|
Q_EMIT get()->operationListUpdated(false);
|
||||||
|
}
|
||||||
|
tidyReply(&get()->allOpsReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// onGithubReleasesComplete is called when the network request associated with the method checkForNewRelease
|
||||||
|
/// completes. This will emit a releasesChecked signal
|
||||||
|
static void onGithubReleasesComplete() {
|
||||||
|
bool isValid;
|
||||||
|
auto data = get()->extractResponse(get()->githubReleaseReply, isValid);
|
||||||
|
if (isValid) {
|
||||||
|
auto releases = dto::GithubRelease::parseDataAsList(data);
|
||||||
|
Q_EMIT get()->releasesChecked(true, releases);
|
||||||
|
} else {
|
||||||
|
Q_EMIT get()->releasesChecked(false);
|
||||||
|
}
|
||||||
|
tidyReply(&get()->githubReleaseReply);
|
||||||
|
}
|
||||||
|
QNetworkReply *allOpsReply = nullptr;
|
||||||
|
QNetworkReply *githubReleaseReply = nullptr;
|
||||||
|
QNetworkAccessManager *nam = nullptr;
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,7 +56,7 @@ TrayManager::TrayManager(QWidget * parent, DatabaseConnection* db)
|
||||||
wireUi();
|
wireUi();
|
||||||
|
|
||||||
// delayed so that windows can listen for get all ops signal
|
// delayed so that windows can listen for get all ops signal
|
||||||
NetMan::getInstance().refreshOperationsList();
|
NetMan::refreshOperationsList();
|
||||||
QTimer::singleShot(5000, this, &TrayManager::checkForUpdate);
|
QTimer::singleShot(5000, this, &TrayManager::checkForUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,16 +120,14 @@ void TrayManager::wireUi() {
|
||||||
&TrayManager::captureWindowActionTriggered);
|
&TrayManager::captureWindowActionTriggered);
|
||||||
|
|
||||||
// connect to network signals
|
// connect to network signals
|
||||||
connect(&NetMan::getInstance(), &NetMan::operationListUpdated, this,
|
connect(NetMan::get(), &NetMan::operationListUpdated, this, &TrayManager::onOperationListUpdated);
|
||||||
&TrayManager::onOperationListUpdated);
|
connect(NetMan::get(), &NetMan::releasesChecked, this, &TrayManager::onReleaseCheck);
|
||||||
connect(&NetMan::getInstance(), &NetMan::releasesChecked, this, &TrayManager::onReleaseCheck);
|
connect(&AppSettings::getInstance(), &AppSettings::onOperationUpdated, this, &TrayManager::setActiveOperationLabel);
|
||||||
connect(&AppSettings::getInstance(), &AppSettings::onOperationUpdated, this,
|
|
||||||
&TrayManager::setActiveOperationLabel);
|
|
||||||
|
|
||||||
connect(trayIcon, &QSystemTrayIcon::messageClicked, this, &TrayManager::onTrayMessageClicked);
|
connect(trayIcon, &QSystemTrayIcon::messageClicked, this, &TrayManager::onTrayMessageClicked);
|
||||||
connect(trayIcon, &QSystemTrayIcon::activated, this, [this] {
|
connect(trayIcon, &QSystemTrayIcon::activated, this, [this] {
|
||||||
newOperationAction->setEnabled(false);
|
newOperationAction->setEnabled(false);
|
||||||
NetMan::getInstance().refreshOperationsList();
|
NetMan::refreshOperationsList();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(updateCheckTimer, &QTimer::timeout, this, &TrayManager::checkForUpdate);
|
connect(updateCheckTimer, &QTimer::timeout, this, &TrayManager::checkForUpdate);
|
||||||
|
@ -297,7 +295,7 @@ void TrayManager::onOperationListUpdated(bool success,
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrayManager::checkForUpdate() {
|
void TrayManager::checkForUpdate() {
|
||||||
NetMan::getInstance().checkForNewRelease(Constants::releaseOwner(), Constants::releaseRepo());
|
NetMan::checkForNewRelease(Constants::releaseOwner(), Constants::releaseRepo());
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrayManager::onReleaseCheck(bool success, const QList<dto::GithubRelease>& releases) {
|
void TrayManager::onReleaseCheck(bool success, const QList<dto::GithubRelease>& releases) {
|
||||||
|
|
Loading…
Reference in New Issue