Netman Clean up (#189)

Co-authored-by: Chris Rizzitello <crizzitello@ics.com>
main
crizzitello 2022-07-11 13:50:58 -04:00 committed by GitHub
parent 240ecdc3d5
commit 4727200bc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 156 additions and 188 deletions

View File

@ -31,7 +31,7 @@ void TagCache::requestTags(QString operationSlug) {
return;
}
auto reply = NetMan::getInstance().getOperationTags(operationSlug);
auto reply = NetMan::getOperationTags(operationSlug);
tagRequests.insert(operationSlug, reply);
connect(reply, &QNetworkReply::finished, this, [this, reply, operationSlug]() {
onGetTagsComplete(reply, operationSlug);

View File

@ -179,7 +179,7 @@ void TagEditor::createTag(QString tagName) {
tagCompleteTextBox->setEnabled(false);
dto::Tag newTag(newText, TagWidget::randomColor());
createTagReply = NetMan::getInstance().createTag(newTag, operationSlug);
createTagReply = NetMan::createTag(newTag, operationSlug);
connect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
}

View File

@ -63,7 +63,7 @@ void CreateOperation::submitButtonClicked() {
submitButton->startAnimation();
submitButton->setEnabled(false);
createOpReply = NetMan::getInstance().createOperation(name, slug);
createOpReply = NetMan::createOperation(name, slug);
connect(createOpReply, &QNetworkReply::finished, this, &CreateOperation::onRequestComplete);
}
@ -81,7 +81,7 @@ void CreateOperation::onRequestComplete() {
dto::Operation op = dto::Operation::parseData(data);
AppSettings::getInstance().setOperationDetails(op.slug, op.name);
operationNameTextBox->clear();
NetMan::getInstance().refreshOperationsList();
NetMan::refreshOperationsList();
close();
}
else {

View File

@ -17,7 +17,7 @@ Credits::Credits(QWidget* parent)
, updateLabel(new QLabel(this))
{
setWindowTitle("About");
connect(&NetMan::getInstance(), &NetMan::releasesChecked, this, &Credits::onReleasesUpdate);
connect(NetMan::get(), &NetMan::releasesChecked, this, &Credits::onReleasesUpdate);
updateLabel->setVisible(false);
updateLabel->setOpenExternalLinks(true);

View File

@ -201,7 +201,7 @@ void EvidenceManager::submitEvidenceTriggered() {
evidenceIDForRequest = selectedRowEvidenceID();
try {
model::Evidence evi = db->getEvidenceDetails(evidenceIDForRequest);
uploadAssetReply = NetMan::getInstance().uploadAsset(evi);
uploadAssetReply = NetMan::uploadAsset(evi);
connect(uploadAssetReply, &QNetworkReply::finished, this, &EvidenceManager::onUploadComplete);
}
catch (QSqlError& e) {

View File

@ -123,8 +123,7 @@ void EvidenceFilterForm::wireUi() {
submittedComboBox->installEventFilter(this);
contentTypeComboBox->installEventFilter(this);
connect(&NetMan::getInstance(), &NetMan::operationListUpdated, this,
&EvidenceFilterForm::onOperationListUpdated);
connect(NetMan::get(), &NetMan::operationListUpdated, this, &EvidenceFilterForm::onOperationListUpdated);
connect(buttonBox, &QDialogButtonBox::accepted, this, &EvidenceFilterForm::writeAndClose);
connect(includeStartDateCheckBox, &QCheckBox::stateChanged, fromDateEdit, &QDateEdit::setEnabled);

View File

@ -91,7 +91,7 @@ void GetInfo::submitButtonClicked() {
if (saveData()) {
try {
model::Evidence evi = db->getEvidenceDetails(evidenceID);
uploadAssetReply = NetMan::getInstance().uploadAsset(evi);
uploadAssetReply = NetMan::uploadAsset(evi);
connect(uploadAssetReply, &QNetworkReply::finished, this, &GetInfo::onUploadComplete);
}
catch (QSqlError& e) {

View File

@ -220,7 +220,7 @@ void Settings::onSaveClicked() {
QString originalApiUrl = inst.apiURL;
inst.apiURL = hostPathTextBox->text();
if (originalApiUrl != hostPathTextBox->text()) {
NetMan::getInstance().refreshOperationsList();
NetMan::refreshOperationsList();
}
inst.screenshotExec = captureAreaCmdTextBox->text();
@ -261,7 +261,7 @@ void Settings::onTestConnectionClicked() {
}
testConnectionButton->startAnimation();
testConnectionButton->setEnabled(false);
currentTestReply = NetMan::getInstance().testConnection(
currentTestReply = NetMan::testConnection(
hostPathTextBox->text(), accessKeyTextBox->text(), secretKeyTextBox->text());
connect(currentTestReply, &QNetworkReply::finished, this, &Settings::onTestRequestComplete);
}

View File

@ -5,7 +5,6 @@
class Constants {
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 defaultEvidenceRepo = QStringLiteral("%1/evidence").arg(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
inline static const auto commitHash = QStringLiteral("%1").arg(COMMIT_HASH);
@ -57,8 +56,8 @@ class Constants {
QRegularExpressionMatch match = ownerRegex.match(rawRepo);
// 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)
parsedOwner = match.hasMatch() ? match.captured(1) : unknownValue;
parsedRepo = match.hasMatch() ? match.captured(2) : unknownValue;
parsedOwner = match.hasMatch() ? match.captured(1) : QString();
parsedRepo = match.hasMatch() ? match.captured(2) : QString();
}
return field == RepoField::owner ? parsedOwner : parsedRepo;
}

View File

@ -13,228 +13,98 @@
#include "dtos/operation.h"
#include "dtos/tag.h"
#include "dtos/github_release.h"
#include "helpers/multipartparser.h"
#include "helpers/constants.h"
#include <helpers/multipartparser.h>
#include "helpers/stopreply.h"
#include "models/evidence.h"
class NetMan : public QObject {
Q_OBJECT
public:
static NetMan &getInstance() {
static NetMan instance;
return instance;
}
NetMan(NetMan const &) = delete;
void operator=(NetMan const &) = delete;
public:
// type alias QList<dto::Operation> to provide shorter lines
using OperationVector = QList<dto::Operation>;
signals:
void operationListUpdated(bool success, NetMan::OperationVector operations = NetMan::OperationVector());
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))
{
static NetMan* get() {
static NetMan i;
return &i;
}
~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
/// 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,
/// rather than the time of capture.
QNetworkReply *uploadAsset(model::Evidence evidence) {
static QNetworkReply* uploadAsset(model::Evidence evidence) {
MultipartParser parser;
parser.addParameter(QStringLiteral("notes"), evidence.description);
parser.addParameter(QStringLiteral("contentType"), evidence.contentType);
// TODO: convert this time below into a proper unix timestamp (mSecSinceEpoch and secsSinceEpoch
// produce invalid times)
// parser.AddParameter("occurred_at", std::to_string(evidence.recordedDate);
QStringList list;
for (const auto& tag : evidence.tags) {
list << QString::number(tag.serverTagId);
}
for (const auto& tag : evidence.tags)
list.append(QString::number(tag.serverTagId));
parser.addParameter(QStringLiteral("tagIds"), QStringLiteral("[%1]").arg(list.join(QStringLiteral(","))));
parser.addFile(QStringLiteral("file"), evidence.path);
auto body = parser.generateBody();
auto builder = ashirtFormPost(QStringLiteral("/api/operations/%1/evidence").arg(evidence.operationSlug), body, parser.boundary());
auto builder = ashirtFormPost(QStringLiteral("/api/operations/%1/evidence").arg(evidence.operationSlug), parser.generateBody(), parser.boundary());
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
/// 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);
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.
/// Note: normally you should opt to use refreshOperationsList and retrieve the results by listening
/// for the operationListUpdated signal.
QNetworkReply *getAllOperations() {
static QNetworkReply *getAllOperations() {
auto builder = ashirtGet(QStringLiteral("/api/operations"));
addASHIRTAuth(builder);
return builder->execute(nam);
return builder->execute(get()->nam);
}
/// getGithubReleases retrieves the recent releases from github for the provided owner and repo.
/// Note that normally you should call checkForNewRelease
QNetworkReply *getGithubReleases(QString owner, QString repo) {
static QNetworkReply *getGithubReleases(QString owner, QString repo) {
return RequestBuilder::newGet()
->setHost(QStringLiteral("https://api.github.com"))
->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
/// retrieved by listening for the operationListUpdated signal
void refreshOperationsList() {
if (allOpsReply == nullptr) {
allOpsReply = getAllOperations();
connect(allOpsReply, &QNetworkReply::finished, this, &NetMan::onGetOpsComplete);
}
static void refreshOperationsList() {
if (get()->allOpsReply)
return;
get()->allOpsReply = get()->getAllOperations();
connect(get()->allOpsReply, &QNetworkReply::finished, get(), &NetMan::onGetOpsComplete);
}
/// 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));
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.
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));
addASHIRTAuth(builder);
return builder->execute(nam);
return builder->execute(get()->nam);
}
/// 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));
addASHIRTAuth(builder);
return builder->execute(nam);
return builder->execute(get()->nam);
}
/// 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.
/// Callers should retrieve the result by listening for the releasesChecked signal
void checkForNewRelease(QString owner, QString repo) {
if (owner == Constants::unknownValue || repo == Constants::unknownValue) {
static void checkForNewRelease(QString owner, QString repo) {
if (owner.isEmpty() || repo.isEmpty()) {
QTextStream(stderr) << "Skipping release check: no owner or repo set." << Qt::endl;
return;
}
githubReleaseReply = getGithubReleases(owner, repo);
connect(githubReleaseReply, &QNetworkReply::finished, this, &NetMan::onGithubReleasesComplete);
get()->githubReleaseReply = get()->getGithubReleases(owner, repo);
connect(get()->githubReleaseReply, &QNetworkReply::finished, get(), &NetMan::onGithubReleasesComplete);
}
private:
QNetworkReply *allOpsReply = nullptr;
QNetworkReply *githubReleaseReply = nullptr;
signals:
void operationListUpdated(bool success, NetMan::OperationVector operations = NetMan::OperationVector());
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;
};

View File

@ -56,7 +56,7 @@ TrayManager::TrayManager(QWidget * parent, DatabaseConnection* db)
wireUi();
// delayed so that windows can listen for get all ops signal
NetMan::getInstance().refreshOperationsList();
NetMan::refreshOperationsList();
QTimer::singleShot(5000, this, &TrayManager::checkForUpdate);
}
@ -120,16 +120,14 @@ void TrayManager::wireUi() {
&TrayManager::captureWindowActionTriggered);
// connect to network signals
connect(&NetMan::getInstance(), &NetMan::operationListUpdated, this,
&TrayManager::onOperationListUpdated);
connect(&NetMan::getInstance(), &NetMan::releasesChecked, this, &TrayManager::onReleaseCheck);
connect(&AppSettings::getInstance(), &AppSettings::onOperationUpdated, this,
&TrayManager::setActiveOperationLabel);
connect(NetMan::get(), &NetMan::operationListUpdated, this, &TrayManager::onOperationListUpdated);
connect(NetMan::get(), &NetMan::releasesChecked, this, &TrayManager::onReleaseCheck);
connect(&AppSettings::getInstance(), &AppSettings::onOperationUpdated, this, &TrayManager::setActiveOperationLabel);
connect(trayIcon, &QSystemTrayIcon::messageClicked, this, &TrayManager::onTrayMessageClicked);
connect(trayIcon, &QSystemTrayIcon::activated, this, [this] {
newOperationAction->setEnabled(false);
NetMan::getInstance().refreshOperationsList();
NetMan::refreshOperationsList();
});
connect(updateCheckTimer, &QTimer::timeout, this, &TrayManager::checkForUpdate);
@ -297,7 +295,7 @@ void TrayManager::onOperationListUpdated(bool success,
}
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) {