diff --git a/share/demo.kdbx b/share/demo.kdbx
index 71795676a..1f3727104 100644
Binary files a/share/demo.kdbx and b/share/demo.kdbx differ
diff --git a/share/icons/application/scalable/actions/health.svg b/share/icons/application/scalable/actions/health.svg
new file mode 100644
index 000000000..4cd5fa091
--- /dev/null
+++ b/share/icons/application/scalable/actions/health.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index af9b9bb58..6b3d9abfa 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -48,6 +48,7 @@ set(keepassx_SOURCES
core/Merger.cpp
core/Metadata.cpp
core/PasswordGenerator.cpp
+ core/PasswordHealth.cpp
core/PassphraseGenerator.cpp
core/SignalMultiplexer.cpp
core/ScreenLockListener.cpp
@@ -149,8 +150,12 @@ set(keepassx_SOURCES
gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp
gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
- gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp
- gui/dbsettings/DatabaseSettingsPageStatistics.cpp
+ gui/reports/ReportsWidget.cpp
+ gui/reports/ReportsDialog.cpp
+ gui/reports/ReportsWidgetHealthcheck.cpp
+ gui/reports/ReportsPageHealthcheck.cpp
+ gui/reports/ReportsWidgetStatistics.cpp
+ gui/reports/ReportsPageStatistics.cpp
gui/settings/SettingsWidget.cpp
gui/widgets/ElidedLabel.cpp
gui/widgets/PopupHelpWidget.cpp
diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp
index 9cb4e0735..b49af7005 100644
--- a/src/browser/BrowserSettings.cpp
+++ b/src/browser/BrowserSettings.cpp
@@ -19,6 +19,7 @@
#include "BrowserSettings.h"
#include "core/Config.h"
+#include "core/PasswordHealth.h"
BrowserSettings* BrowserSettings::m_instance(nullptr);
@@ -541,7 +542,7 @@ QJsonObject BrowserSettings::generatePassword()
m_passwordGenerator.setCharClasses(passwordCharClasses());
m_passwordGenerator.setFlags(passwordGeneratorFlags());
const QString pw = m_passwordGenerator.generatePassword();
- password["entropy"] = m_passwordGenerator.estimateEntropy(pw);
+ password["entropy"] = PasswordHealth(pw).entropy();
password["password"] = pw;
} else {
m_passPhraseGenerator.setWordCount(passPhraseWordCount());
diff --git a/src/cli/Estimate.cpp b/src/cli/Estimate.cpp
index a84e23963..3b7509057 100644
--- a/src/cli/Estimate.cpp
+++ b/src/cli/Estimate.cpp
@@ -19,6 +19,7 @@
#include "cli/Utils.h"
#include "cli/TextStream.h"
+#include "core/PasswordHealth.h"
#include
#include
#include
@@ -49,10 +50,9 @@ static void estimate(const char* pwd, bool advanced)
{
TextStream out(Utils::STDOUT, QIODevice::WriteOnly);
- double e = 0.0;
int len = static_cast(strlen(pwd));
if (!advanced) {
- e = ZxcvbnMatch(pwd, nullptr, nullptr);
+ const auto e = PasswordHealth(pwd).entropy();
// clang-format off
out << QObject::tr("Length %1").arg(len, 0) << '\t'
<< QObject::tr("Entropy %1").arg(e, 0, 'f', 3) << '\t'
@@ -62,7 +62,7 @@ static void estimate(const char* pwd, bool advanced)
int ChkLen = 0;
ZxcMatch_t *info, *p;
double m = 0.0;
- e = ZxcvbnMatch(pwd, nullptr, &info);
+ const auto e = ZxcvbnMatch(pwd, nullptr, &info);
for (p = info; p; p = p->Next) {
m += p->Entrpy;
}
diff --git a/src/core/PasswordGenerator.cpp b/src/core/PasswordGenerator.cpp
index e203af672..ff271a453 100644
--- a/src/core/PasswordGenerator.cpp
+++ b/src/core/PasswordGenerator.cpp
@@ -19,7 +19,6 @@
#include "PasswordGenerator.h"
#include "crypto/Random.h"
-#include
const char* PasswordGenerator::DefaultExcludedChars = "";
@@ -31,11 +30,6 @@ PasswordGenerator::PasswordGenerator()
{
}
-double PasswordGenerator::estimateEntropy(const QString& password)
-{
- return ZxcvbnMatch(password.toLatin1(), nullptr, nullptr);
-}
-
void PasswordGenerator::setLength(int length)
{
if (length <= 0) {
diff --git a/src/core/PasswordGenerator.h b/src/core/PasswordGenerator.h
index 22627d25b..55418b4ba 100644
--- a/src/core/PasswordGenerator.h
+++ b/src/core/PasswordGenerator.h
@@ -57,7 +57,6 @@ public:
public:
PasswordGenerator();
- double estimateEntropy(const QString& password);
void setLength(int length);
void setCharClasses(const CharClasses& classes);
void setFlags(const GeneratorFlags& flags);
diff --git a/src/core/PasswordHealth.cpp b/src/core/PasswordHealth.cpp
new file mode 100644
index 000000000..58e4e42af
--- /dev/null
+++ b/src/core/PasswordHealth.cpp
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include
+#include
+
+#include "Database.h"
+#include "Entry.h"
+#include "Group.h"
+#include "PasswordHealth.h"
+#include "zxcvbn.h"
+
+PasswordHealth::PasswordHealth(double entropy)
+ : m_score(entropy)
+ , m_entropy(entropy)
+{
+ switch (quality()) {
+ case Quality::Bad:
+ case Quality::Poor:
+ m_scoreReasons << QApplication::tr("Very weak password");
+ m_scoreDetails << QApplication::tr("Password entropy is %1 bits").arg(QString::number(m_entropy, 'f', 2));
+ break;
+
+ case Quality::Weak:
+ m_scoreReasons << QApplication::tr("Weak password");
+ m_scoreDetails << QApplication::tr("Password entropy is %1 bits").arg(QString::number(m_entropy, 'f', 2));
+ break;
+
+ default:
+ // No reason or details for good and excellent passwords
+ break;
+ }
+}
+
+PasswordHealth::PasswordHealth(QString pwd)
+ : PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr))
+{
+}
+
+void PasswordHealth::setScore(int score)
+{
+ m_score = score;
+}
+
+void PasswordHealth::adjustScore(int amount)
+{
+ m_score += amount;
+}
+
+QString PasswordHealth::scoreReason() const
+{
+ return m_scoreReasons.join("\n");
+}
+
+void PasswordHealth::addScoreReason(QString reason)
+{
+ m_scoreReasons << reason;
+}
+
+QString PasswordHealth::scoreDetails() const
+{
+ return m_scoreDetails.join("\n");
+}
+
+void PasswordHealth::addScoreDetails(QString details)
+{
+ m_scoreDetails.append(details);
+}
+
+PasswordHealth::Quality PasswordHealth::quality() const
+{
+ if (m_score <= 0) {
+ return Quality::Bad;
+ } else if (m_score < 40) {
+ return Quality::Poor;
+ } else if (m_score < 65) {
+ return Quality::Weak;
+ } else if (m_score < 100) {
+ return Quality::Good;
+ }
+ return Quality::Excellent;
+}
+
+/**
+ * This class provides additional information about password health
+ * than can be derived from the password itself (re-use, expiry).
+ */
+HealthChecker::HealthChecker(QSharedPointer db)
+{
+ // Build the cache of re-used passwords
+ for (const auto* entry : db->rootGroup()->entriesRecursive()) {
+ if (!entry->isRecycled()) {
+ m_reuse[entry->password()]
+ << QApplication::tr("Used in %1/%2").arg(entry->group()->hierarchy().join('/'), entry->title());
+ }
+ }
+}
+
+/**
+ * Call operator of the Health Checker class.
+ *
+ * Returns the health of the password in `entry`, considering
+ * password entropy, re-use, expiration, etc.
+ */
+QSharedPointer HealthChecker::evaluate(const Entry* entry)
+{
+ if (!entry) {
+ return {};
+ }
+
+ // Return from cache if we saw it before
+ if (m_cache.contains(entry->uuid())) {
+ return m_cache[entry->uuid()];
+ }
+
+ // First analyse the password itself
+ const auto pwd = entry->password();
+ auto health = QSharedPointer(new PasswordHealth(pwd));
+
+ // Second, if the password is in the database more than once,
+ // reduce the score accordingly
+ const auto& used = m_reuse[pwd];
+ const auto count = used.size();
+ if (count > 1) {
+ constexpr auto penalty = 15;
+ health->adjustScore(-penalty * (count - 1));
+ health->addScoreReason(QApplication::tr("Password is used %1 times").arg(QString::number(count)));
+ // Add the first 20 uses of the password to prevent the details display from growing too large
+ for (int i = 0; i < used.size(); ++i) {
+ health->addScoreDetails(used[i]);
+ if (i == 19) {
+ health->addScoreDetails(QStringLiteral("..."));
+ break;
+ }
+ }
+
+ // Don't allow re-used passwords to be considered "good"
+ // no matter how great their entropy is.
+ if (health->score() > 64) {
+ health->setScore(64);
+ }
+ }
+
+ // Third, if the password has already expired, reduce score to 0;
+ // or, if the password is going to expire in the next 30 days,
+ // reduce score by 2 points per day.
+ if (entry->isExpired()) {
+ health->setScore(0);
+ health->addScoreReason(QApplication::tr("Password has expired"));
+ health->addScoreDetails(QApplication::tr("Password expiry was %1")
+ .arg(entry->timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate)));
+ } else if (entry->timeInfo().expires()) {
+ const auto days = QDateTime::currentDateTime().daysTo(entry->timeInfo().expiryTime());
+ if (days <= 30) {
+ // First bring the score down into the "weak" range
+ // so that the entry appears in Health Check. Then
+ // reduce the score by 2 points for every day that
+ // we get closer to expiry. days<=0 has already
+ // been handled above ("isExpired()").
+ if (health->score() > 60) {
+ health->setScore(60);
+ }
+ health->adjustScore((30 - days) * -2);
+ health->addScoreReason(days <= 2 ? QApplication::tr("Password is about to expire")
+ : days <= 10 ? QApplication::tr("Password expires in %1 days").arg(days)
+ : QApplication::tr("Password will expire soon"));
+ health->addScoreDetails(QApplication::tr("Password expires on %1")
+ .arg(entry->timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate)));
+ }
+ }
+
+ // Return the result
+ return m_cache.insert(entry->uuid(), health).value();
+}
diff --git a/src/core/PasswordHealth.h b/src/core/PasswordHealth.h
new file mode 100644
index 000000000..ca7f0236e
--- /dev/null
+++ b/src/core/PasswordHealth.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSX_PASSWORDHEALTH_H
+#define KEEPASSX_PASSWORDHEALTH_H
+
+#include
+#include
+#include
+
+class Database;
+class Entry;
+
+/**
+ * Health status of a single password.
+ *
+ * @see HealthChecker
+ */
+class PasswordHealth
+{
+public:
+ explicit PasswordHealth(double entropy);
+ explicit PasswordHealth(QString pwd);
+
+ /*
+ * The password score is defined to be the greater the better
+ * (more secure) the password is. It doesn't have a dimension,
+ * there are no defined maximum or minimum values, and score
+ * values may change with different versions of the software.
+ */
+ int score() const
+ {
+ return m_score;
+ }
+
+ void setScore(int score);
+ void adjustScore(int amount);
+
+ /*
+ * A text description for the password's quality assessment
+ * (translated into the application language), and additional
+ * information. Empty if nothing is wrong with the password.
+ * May contain more than line, separated by '\n'.
+ */
+ QString scoreReason() const;
+ void addScoreReason(QString reason);
+
+ QString scoreDetails() const;
+ void addScoreDetails(QString details);
+
+ /*
+ * The password quality assessment (based on the score).
+ */
+ enum class Quality
+ {
+ Bad,
+ Poor,
+ Weak,
+ Good,
+ Excellent
+ };
+ Quality quality() const;
+
+ /*
+ * The password's raw entropy value, in bits.
+ */
+ double entropy() const
+ {
+ return m_entropy;
+ }
+
+private:
+ int m_score = 0;
+ double m_entropy = 0.0;
+ QStringList m_scoreReasons;
+ QStringList m_scoreDetails;
+};
+
+/**
+ * Password health check for all entries of a database.
+ *
+ * @see PasswordHealth
+ */
+class HealthChecker
+{
+public:
+ explicit HealthChecker(QSharedPointer);
+
+ // Get the health status of an entry in the database
+ QSharedPointer evaluate(const Entry* entry);
+
+private:
+ // Result cache (first=entry UUID)
+ QHash> m_cache;
+ // first = password, second = entries that use it
+ QHash m_reuse;
+};
+
+#endif // KEEPASSX_PASSWORDHEALTH_H
diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp
index 4b9fe5f85..bd24cf165 100644
--- a/src/gui/AboutDialog.cpp
+++ b/src/gui/AboutDialog.cpp
@@ -76,7 +76,7 @@ static const QString aboutContributors = R"(
fonic (Entry Table View)
kylemanna (YubiKey)
c4rlo (Offline HIBP Checker)
- wolframroesler (HTML Exporter)
+ wolframroesler (HTML Export, Statistics, Password Health)
mdaniel (OpVault Importer)
keithbennett (KeePassHTTP)
Typz (KeePassHTTP)
diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
index c37e6c5ea..7e158406b 100644
--- a/src/gui/DatabaseTabWidget.cpp
+++ b/src/gui/DatabaseTabWidget.cpp
@@ -457,6 +457,11 @@ void DatabaseTabWidget::changeMasterKey()
currentDatabaseWidget()->switchToMasterKeyChange();
}
+void DatabaseTabWidget::changeReports()
+{
+ currentDatabaseWidget()->switchToReports();
+}
+
void DatabaseTabWidget::changeDatabaseSettings()
{
currentDatabaseWidget()->switchToDatabaseSettings();
diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h
index 5c55bc63c..29019a2d2 100644
--- a/src/gui/DatabaseTabWidget.h
+++ b/src/gui/DatabaseTabWidget.h
@@ -78,6 +78,7 @@ public slots:
void relockPendingDatabase();
void changeMasterKey();
+ void changeReports();
void changeDatabaseSettings();
void performGlobalAutoType();
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index eb33c09c0..fd579b04a 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -59,6 +59,7 @@
#include "gui/entry/EntryView.h"
#include "gui/group/EditGroupWidget.h"
#include "gui/group/GroupView.h"
+#include "gui/reports/ReportsDialog.h"
#include "keeshare/KeeShare.h"
#include "touchid/TouchID.h"
@@ -88,6 +89,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent)
, m_editEntryWidget(new EditEntryWidget(this))
, m_editGroupWidget(new EditGroupWidget(this))
, m_historyEditEntryWidget(new EditEntryWidget(this))
+ , m_reportsDialog(new ReportsDialog(this))
, m_databaseSettingDialog(new DatabaseSettingsDialog(this))
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
@@ -165,6 +167,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent)
m_editEntryWidget->setObjectName("editEntryWidget");
m_editGroupWidget->setObjectName("editGroupWidget");
m_csvImportWizard->setObjectName("csvImportWizard");
+ m_reportsDialog->setObjectName("reportsDialog");
m_databaseSettingDialog->setObjectName("databaseSettingsDialog");
m_databaseOpenWidget->setObjectName("databaseOpenWidget");
m_keepass1OpenWidget->setObjectName("keepass1OpenWidget");
@@ -173,6 +176,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent)
addChildWidget(m_mainWidget);
addChildWidget(m_editEntryWidget);
addChildWidget(m_editGroupWidget);
+ addChildWidget(m_reportsDialog);
addChildWidget(m_databaseSettingDialog);
addChildWidget(m_historyEditEntryWidget);
addChildWidget(m_databaseOpenWidget);
@@ -196,6 +200,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent)
connect(m_editEntryWidget, SIGNAL(historyEntryActivated(Entry*)), SLOT(switchToHistoryView(Entry*)));
connect(m_historyEditEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchBackToEntryEdit()));
connect(m_editGroupWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
+ connect(m_reportsDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
@@ -1105,6 +1110,12 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
}
}
+void DatabaseWidget::switchToReports()
+{
+ m_reportsDialog->load(m_db);
+ setCurrentWidget(m_reportsDialog);
+}
+
void DatabaseWidget::switchToDatabaseSettings()
{
m_databaseSettingDialog->load(m_db);
diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h
index 9f0c5c976..6420a3b24 100644
--- a/src/gui/DatabaseWidget.h
+++ b/src/gui/DatabaseWidget.h
@@ -34,6 +34,7 @@ class DatabaseOpenWidget;
class KeePass1OpenWidget;
class OpVaultOpenWidget;
class DatabaseSettingsDialog;
+class ReportsDialog;
class Database;
class FileWatcher;
class EditEntryWidget;
@@ -181,6 +182,7 @@ public slots:
void sortGroupsAsc();
void sortGroupsDesc();
void switchToMasterKeyChange();
+ void switchToReports();
void switchToDatabaseSettings();
void switchToOpenDatabase();
void switchToOpenDatabase(const QString& filePath);
@@ -251,6 +253,7 @@ private:
QPointer m_editEntryWidget;
QPointer m_editGroupWidget;
QPointer m_historyEditEntryWidget;
+ QPointer m_reportsDialog;
QPointer m_databaseSettingDialog;
QPointer m_databaseOpenWidget;
QPointer m_keepass1OpenWidget;
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index e9c150dd5..2d52331ff 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -332,6 +332,7 @@ MainWindow::MainWindow()
m_ui->actionDatabaseSave->setIcon(filePath()->icon("actions", "document-save"));
m_ui->actionDatabaseSaveAs->setIcon(filePath()->icon("actions", "document-save-as"));
m_ui->actionDatabaseClose->setIcon(filePath()->icon("actions", "document-close"));
+ m_ui->actionReports->setIcon(filePath()->icon("actions", "help-about"));
m_ui->actionChangeDatabaseSettings->setIcon(filePath()->icon("actions", "document-edit"));
m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key"));
m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "database-lock"));
@@ -403,6 +404,7 @@ MainWindow::MainWindow()
connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeCurrentDatabaseTab()));
connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, SLOT(mergeDatabase()));
connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey()));
+ connect(m_ui->actionReports, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeReports()));
connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings()));
connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv()));
connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database()));
@@ -673,6 +675,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && currentGroupHasEntries
&& !recycleBinSelected);
m_ui->actionChangeMasterKey->setEnabled(true);
+ m_ui->actionReports->setEnabled(true);
m_ui->actionChangeDatabaseSettings->setEnabled(true);
m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave());
m_ui->actionDatabaseSaveAs->setEnabled(true);
@@ -719,6 +722,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
}
m_ui->actionChangeMasterKey->setEnabled(false);
+ m_ui->actionReports->setEnabled(false);
m_ui->actionChangeDatabaseSettings->setEnabled(false);
m_ui->actionDatabaseSave->setEnabled(false);
m_ui->actionDatabaseSaveAs->setEnabled(false);
@@ -746,6 +750,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
}
m_ui->actionChangeMasterKey->setEnabled(false);
+ m_ui->actionReports->setEnabled(false);
m_ui->actionChangeDatabaseSettings->setEnabled(false);
m_ui->actionDatabaseSave->setEnabled(false);
m_ui->actionDatabaseSaveAs->setEnabled(false);
diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui
index e09c91dd7..aec0efb37 100644
--- a/src/gui/MainWindow.ui
+++ b/src/gui/MainWindow.ui
@@ -236,6 +236,7 @@
+
@@ -532,6 +533,20 @@
Change master &key...
+
+
+ false
+
+
+ &Reports...
+
+
+ Statistics, health check, etc.
+
+
+ QAction::NoRole
+
+
false
diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp
index e0f8fbe5f..c04487c0e 100644
--- a/src/gui/PasswordGeneratorWidget.cpp
+++ b/src/gui/PasswordGeneratorWidget.cpp
@@ -26,6 +26,7 @@
#include "core/Config.h"
#include "core/FilePath.h"
#include "core/PasswordGenerator.h"
+#include "core/PasswordHealth.h"
#include "gui/Clipboard.h"
PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
@@ -261,21 +262,17 @@ void PasswordGeneratorWidget::updateButtonsEnabled(const QString& password)
void PasswordGeneratorWidget::updatePasswordStrength(const QString& password)
{
- double entropy = 0.0;
- if (m_ui->tabWidget->currentIndex() == Password) {
- entropy = m_passwordGenerator->estimateEntropy(password);
- } else {
- entropy = m_dicewareGenerator->estimateEntropy();
+ PasswordHealth health(password);
+ if (m_ui->tabWidget->currentIndex() == Diceware) {
+ // Diceware estimates entropy differently
+ health = PasswordHealth(m_dicewareGenerator->estimateEntropy());
}
- m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(entropy, 'f', 2)));
+ m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(health.entropy(), 'f', 2)));
- if (entropy > m_ui->entropyProgressBar->maximum()) {
- entropy = m_ui->entropyProgressBar->maximum();
- }
- m_ui->entropyProgressBar->setValue(entropy);
+ m_ui->entropyProgressBar->setValue(std::min(int(health.entropy()), m_ui->entropyProgressBar->maximum()));
- colorStrengthIndicator(entropy);
+ colorStrengthIndicator(health);
}
void PasswordGeneratorWidget::applyPassword()
@@ -384,7 +381,7 @@ void PasswordGeneratorWidget::excludeHexChars()
m_ui->editExcludedChars->setText("GHIJKLMNOPQRSTUVWXYZghijklmnopqrstuvwxyz");
}
-void PasswordGeneratorWidget::colorStrengthIndicator(double entropy)
+void PasswordGeneratorWidget::colorStrengthIndicator(const PasswordHealth& health)
{
// Take the existing stylesheet and convert the text and background color to arguments
QString style = m_ui->entropyProgressBar->styleSheet();
@@ -395,18 +392,27 @@ void PasswordGeneratorWidget::colorStrengthIndicator(double entropy)
// Set the color and background based on entropy
// colors are taking from the KDE breeze palette
//
- if (entropy < 40) {
+ switch (health.quality()) {
+ case PasswordHealth::Quality::Bad:
+ case PasswordHealth::Quality::Poor:
m_ui->entropyProgressBar->setStyleSheet(style.arg("#c0392b"));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Poor", "Password quality")));
- } else if (entropy >= 40 && entropy < 65) {
+ break;
+
+ case PasswordHealth::Quality::Weak:
m_ui->entropyProgressBar->setStyleSheet(style.arg("#f39c1f"));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Weak", "Password quality")));
- } else if (entropy >= 65 && entropy < 100) {
+ break;
+
+ case PasswordHealth::Quality::Good:
m_ui->entropyProgressBar->setStyleSheet(style.arg("#11d116"));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Good", "Password quality")));
- } else {
+ break;
+
+ case PasswordHealth::Quality::Excellent:
m_ui->entropyProgressBar->setStyleSheet(style.arg("#27ae60"));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Excellent", "Password quality")));
+ break;
}
}
diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h
index b39a2f10f..eba7f815f 100644
--- a/src/gui/PasswordGeneratorWidget.h
+++ b/src/gui/PasswordGeneratorWidget.h
@@ -32,6 +32,7 @@ namespace Ui
}
class PasswordGenerator;
+class PasswordHealth;
class PassphraseGenerator;
class PasswordGeneratorWidget : public QWidget
@@ -77,7 +78,7 @@ private slots:
void passwordSpinBoxChanged();
void dicewareSliderMoved();
void dicewareSpinBoxChanged();
- void colorStrengthIndicator(double entropy);
+ void colorStrengthIndicator(const PasswordHealth& health);
void updateGenerator();
diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp
index 33c4df2c4..e0e6765a4 100644
--- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp
@@ -19,7 +19,6 @@
#include "DatabaseSettingsDialog.h"
#include "ui_DatabaseSettingsDialog.h"
-#include "DatabaseSettingsPageStatistics.h"
#include "DatabaseSettingsWidgetEncryption.h"
#include "DatabaseSettingsWidgetGeneral.h"
#include "DatabaseSettingsWidgetMasterKey.h"
@@ -85,8 +84,6 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key"));
m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings"));
- addSettingsPage(new DatabaseSettingsPageStatistics());
-
#if defined(WITH_XC_KEESHARE)
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp
new file mode 100644
index 000000000..22ebab41a
--- /dev/null
+++ b/src/gui/reports/ReportsDialog.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "ReportsDialog.h"
+#include "ui_ReportsDialog.h"
+
+#include "ReportsPageHealthcheck.h"
+#include "ReportsPageStatistics.h"
+#include "ReportsWidgetHealthcheck.h"
+
+#include "core/Global.h"
+#include "touchid/TouchID.h"
+#include
+#include
+
+class ReportsDialog::ExtraPage
+{
+public:
+ ExtraPage(QSharedPointer p, QWidget* w)
+ : page(p)
+ , widget(w)
+ {
+ }
+ void loadSettings(QSharedPointer db) const
+ {
+ page->loadSettings(widget, db);
+ }
+ void saveSettings() const
+ {
+ page->saveSettings(widget);
+ }
+
+private:
+ QSharedPointer page;
+ QWidget* widget;
+};
+
+ReportsDialog::ReportsDialog(QWidget* parent)
+ : DialogyWidget(parent)
+ , m_ui(new Ui::ReportsDialog())
+ , m_healthPage(new ReportsPageHealthcheck())
+ , m_statPage(new ReportsPageStatistics())
+ , m_editEntryWidget(new EditEntryWidget(this))
+{
+ m_ui->setupUi(this);
+
+ connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
+ addPage(m_healthPage);
+ addPage(m_statPage);
+
+ m_ui->stackedWidget->setCurrentIndex(0);
+
+ m_editEntryWidget->setObjectName("editEntryWidget");
+ m_editEntryWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
+ m_ui->stackedWidget->addWidget(m_editEntryWidget);
+ adjustSize();
+
+ connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int)));
+ connect(m_healthPage->m_healthWidget,
+ SIGNAL(entryActivated(const Group*, Entry*)),
+ SLOT(entryActivationSignalReceived(const Group*, Entry*)));
+ connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
+}
+
+ReportsDialog::~ReportsDialog()
+{
+}
+
+void ReportsDialog::load(const QSharedPointer& db)
+{
+ m_ui->categoryList->setCurrentCategory(0);
+ for (const ExtraPage& page : asConst(m_extraPages)) {
+ page.loadSettings(db);
+ }
+ m_db = db;
+}
+
+void ReportsDialog::addPage(QSharedPointer page)
+{
+ const auto category = m_ui->categoryList->currentCategory();
+ const auto widget = page->createWidget();
+ widget->setParent(this);
+ m_extraPages.append(ExtraPage(page, widget));
+ m_ui->stackedWidget->addWidget(widget);
+ m_ui->categoryList->addCategory(page->name(), page->icon());
+ m_ui->categoryList->setCurrentCategory(category);
+}
+
+void ReportsDialog::reject()
+{
+ for (const ExtraPage& extraPage : asConst(m_extraPages)) {
+ extraPage.saveSettings();
+ }
+
+#ifdef WITH_XC_TOUCHID
+ TouchID::getInstance().reset(m_db ? m_db->filePath() : "");
+#endif
+
+ emit editFinished(true);
+}
+
+void ReportsDialog::entryActivationSignalReceived(const Group* group, Entry* entry)
+{
+ m_editEntryWidget->loadEntry(entry, false, false, group->hierarchy().join(" > "), m_db);
+ m_ui->stackedWidget->setCurrentWidget(m_editEntryWidget);
+}
+
+void ReportsDialog::switchToMainView(bool previousDialogAccepted)
+{
+ m_ui->stackedWidget->setCurrentWidget(m_healthPage->m_healthWidget);
+ if (previousDialogAccepted) {
+ m_healthPage->m_healthWidget->calculateHealth();
+ }
+}
diff --git a/src/gui/reports/ReportsDialog.h b/src/gui/reports/ReportsDialog.h
new file mode 100644
index 000000000..7a53623c3
--- /dev/null
+++ b/src/gui/reports/ReportsDialog.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSX_REPORTSWIDGET_H
+#define KEEPASSX_REPORTSWIDGET_H
+
+#include "config-keepassx.h"
+#include "gui/DialogyWidget.h"
+#include "gui/entry/EditEntryWidget.h"
+
+#include
+#include
+#include
+
+class Database;
+class Entry;
+class Group;
+class QTabWidget;
+class ReportsPageHealthcheck;
+class ReportsPageStatistics;
+
+namespace Ui
+{
+ class ReportsDialog;
+}
+
+class IReportsPage
+{
+public:
+ virtual ~IReportsPage()
+ {
+ }
+ virtual QString name() = 0;
+ virtual QIcon icon() = 0;
+ virtual QWidget* createWidget() = 0;
+ virtual void loadSettings(QWidget* widget, QSharedPointer db) = 0;
+ virtual void saveSettings(QWidget* widget) = 0;
+};
+
+class ReportsDialog : public DialogyWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ReportsDialog(QWidget* parent = nullptr);
+ ~ReportsDialog() override;
+ Q_DISABLE_COPY(ReportsDialog);
+
+ void load(const QSharedPointer& db);
+ void addPage(QSharedPointer page);
+
+signals:
+ void editFinished(bool accepted);
+
+private slots:
+ void reject();
+ void entryActivationSignalReceived(const Group*, Entry* entry);
+ void switchToMainView(bool previousDialogAccepted);
+
+private:
+ QSharedPointer m_db;
+ const QScopedPointer m_ui;
+ const QSharedPointer m_healthPage;
+ const QSharedPointer m_statPage;
+ QPointer m_editEntryWidget;
+
+ class ExtraPage;
+ QList m_extraPages;
+};
+
+#endif // KEEPASSX_REPORTSWIDGET_H
diff --git a/src/gui/reports/ReportsDialog.ui b/src/gui/reports/ReportsDialog.ui
new file mode 100644
index 000000000..773981a10
--- /dev/null
+++ b/src/gui/reports/ReportsDialog.ui
@@ -0,0 +1,43 @@
+
+
+ ReportsDialog
+
+
+ -
+
+ -
+
+
+ -
+
+
+ -1
+
+
+
+
+
+ -
+
+ -
+
+
+ QDialogButtonBox::Close
+
+
+
+
+
+
+
+
+
+ CategoryListWidget
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/src/gui/reports/ReportsPageHealthcheck.cpp b/src/gui/reports/ReportsPageHealthcheck.cpp
new file mode 100644
index 000000000..41fa40625
--- /dev/null
+++ b/src/gui/reports/ReportsPageHealthcheck.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "ReportsPageHealthcheck.h"
+
+#include "ReportsWidgetHealthcheck.h"
+#include "core/FilePath.h"
+
+#include
+
+ReportsPageHealthcheck::ReportsPageHealthcheck()
+ : m_healthWidget(new ReportsWidgetHealthcheck())
+{
+}
+
+QString ReportsPageHealthcheck::name()
+{
+ return QApplication::tr("Health Check");
+}
+
+QIcon ReportsPageHealthcheck::icon()
+{
+ return FilePath::instance()->icon("actions", "health");
+}
+
+QWidget* ReportsPageHealthcheck::createWidget()
+{
+ return m_healthWidget;
+}
+
+void ReportsPageHealthcheck::loadSettings(QWidget* widget, QSharedPointer db)
+{
+ const auto settingsWidget = reinterpret_cast(widget);
+ settingsWidget->loadSettings(db);
+}
+
+void ReportsPageHealthcheck::saveSettings(QWidget* widget)
+{
+ const auto settingsWidget = reinterpret_cast(widget);
+ settingsWidget->saveSettings();
+}
diff --git a/src/gui/reports/ReportsPageHealthcheck.h b/src/gui/reports/ReportsPageHealthcheck.h
new file mode 100644
index 000000000..8a85b2d20
--- /dev/null
+++ b/src/gui/reports/ReportsPageHealthcheck.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
+#define KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
+
+#include
+
+#include "ReportsDialog.h"
+
+class ReportsWidgetHealthcheck;
+
+class ReportsPageHealthcheck : public IReportsPage
+{
+public:
+ ReportsWidgetHealthcheck* m_healthWidget;
+
+ ReportsPageHealthcheck();
+
+ QString name() override;
+ QIcon icon() override;
+ QWidget* createWidget() override;
+ void loadSettings(QWidget* widget, QSharedPointer db) override;
+ void saveSettings(QWidget* widget) override;
+};
+
+#endif // KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp b/src/gui/reports/ReportsPageStatistics.cpp
similarity index 57%
rename from src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp
rename to src/gui/reports/ReportsPageStatistics.cpp
index 6fe24ff0f..e4570e172 100644
--- a/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp
+++ b/src/gui/reports/ReportsPageStatistics.cpp
@@ -15,38 +15,36 @@
* along with this program. If not, see .
*/
-#include "DatabaseSettingsPageStatistics.h"
+#include "ReportsPageStatistics.h"
-#include "DatabaseSettingsWidgetStatistics.h"
-#include "core/Database.h"
+#include "ReportsWidgetStatistics.h"
#include "core/FilePath.h"
-#include "core/Group.h"
#include
-QString DatabaseSettingsPageStatistics::name()
+QString ReportsPageStatistics::name()
{
return QApplication::tr("Statistics");
}
-QIcon DatabaseSettingsPageStatistics::icon()
+QIcon ReportsPageStatistics::icon()
{
return FilePath::instance()->icon("actions", "statistics");
}
-QWidget* DatabaseSettingsPageStatistics::createWidget()
+QWidget* ReportsPageStatistics::createWidget()
{
- return new DatabaseSettingsWidgetStatistics();
+ return new ReportsWidgetStatistics();
}
-void DatabaseSettingsPageStatistics::loadSettings(QWidget* widget, QSharedPointer db)
+void ReportsPageStatistics::loadSettings(QWidget* widget, QSharedPointer db)
{
- DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast(widget);
+ ReportsWidgetStatistics* settingsWidget = reinterpret_cast(widget);
settingsWidget->loadSettings(db);
}
-void DatabaseSettingsPageStatistics::saveSettings(QWidget* widget)
+void ReportsPageStatistics::saveSettings(QWidget* widget)
{
- DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast(widget);
+ ReportsWidgetStatistics* settingsWidget = reinterpret_cast(widget);
settingsWidget->saveSettings();
}
diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.h b/src/gui/reports/ReportsPageStatistics.h
similarity index 78%
rename from src/gui/dbsettings/DatabaseSettingsPageStatistics.h
rename to src/gui/reports/ReportsPageStatistics.h
index c890f3b81..00d611ee3 100644
--- a/src/gui/dbsettings/DatabaseSettingsPageStatistics.h
+++ b/src/gui/reports/ReportsPageStatistics.h
@@ -15,14 +15,14 @@
* along with this program. If not, see .
*/
-#ifndef KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
-#define KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
+#ifndef KEEPASSXC_REPORTSPAGESTATISTICS_H
+#define KEEPASSXC_REPORTSPAGESTATISTICS_H
#include
-#include "DatabaseSettingsDialog.h"
+#include "ReportsDialog.h"
-class DatabaseSettingsPageStatistics : public IDatabaseSettingsPage
+class ReportsPageStatistics : public IReportsPage
{
public:
QString name() override;
@@ -32,4 +32,4 @@ public:
void saveSettings(QWidget* widget) override;
};
-#endif // KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
+#endif // KEEPASSXC_REPORTSPAGESTATISTICS_H
diff --git a/src/gui/reports/ReportsWidget.cpp b/src/gui/reports/ReportsWidget.cpp
new file mode 100644
index 000000000..184434116
--- /dev/null
+++ b/src/gui/reports/ReportsWidget.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2018 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "ReportsWidget.h"
+
+ReportsWidget::ReportsWidget(QWidget* parent)
+ : SettingsWidget(parent)
+{
+}
+
+ReportsWidget::~ReportsWidget()
+{
+}
+
+/**
+ * Load the database to be configured by this page and initialize the page.
+ * The page will NOT take ownership of the database.
+ *
+ * @param db database object to be configured
+ */
+void ReportsWidget::load(QSharedPointer db)
+{
+ m_db = std::move(db);
+ initialize();
+}
+
+const QSharedPointer ReportsWidget::getDatabase() const
+{
+ return m_db;
+}
diff --git a/src/gui/reports/ReportsWidget.h b/src/gui/reports/ReportsWidget.h
new file mode 100644
index 000000000..631490405
--- /dev/null
+++ b/src/gui/reports/ReportsWidget.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_REPORTSWIDGET_H
+#define KEEPASSXC_REPORTSWIDGET_H
+
+#include "gui/settings/SettingsWidget.h"
+
+#include
+
+class Database;
+
+/**
+ * Pure-virtual base class for KeePassXC database settings widgets.
+ */
+class ReportsWidget : public SettingsWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ReportsWidget(QWidget* parent = nullptr);
+ Q_DISABLE_COPY(ReportsWidget);
+ ~ReportsWidget() override;
+
+ virtual void load(QSharedPointer db);
+
+ const QSharedPointer getDatabase() const;
+
+signals:
+ /**
+ * Can be emitted to indicate size changes and allow parents widgets to adjust properly.
+ */
+ void sizeChanged();
+
+protected:
+ QSharedPointer m_db;
+};
+
+#endif // KEEPASSXC_REPORTSWIDGET_H
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp
new file mode 100644
index 000000000..c668b3495
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "ReportsWidgetHealthcheck.h"
+#include "ui_ReportsWidgetHealthcheck.h"
+
+#include "core/AsyncTask.h"
+#include "core/Database.h"
+#include "core/FilePath.h"
+#include "core/Group.h"
+#include "core/PasswordHealth.h"
+
+#include
+#include
+#include
+
+namespace
+{
+ class Health
+ {
+ public:
+ struct Item
+ {
+ QPointer group;
+ QPointer entry;
+ QSharedPointer health;
+
+ Item(const Group* g, const Entry* e, QSharedPointer h)
+ : group(g)
+ , entry(e)
+ , health(h)
+ {
+ }
+
+ bool operator<(const Item& rhs) const
+ {
+ return health->score() < rhs.health->score();
+ }
+ };
+
+ explicit Health(QSharedPointer);
+
+ const QList>& items() const
+ {
+ return m_items;
+ }
+
+ private:
+ QSharedPointer m_db;
+ HealthChecker m_checker;
+ QList> m_items;
+ };
+} // namespace
+
+Health::Health(QSharedPointer db)
+ : m_db(db)
+ , m_checker(db)
+{
+ for (const auto* group : db->rootGroup()->groupsRecursive(true)) {
+ // Skip recycle bin
+ if (group->isRecycled()) {
+ continue;
+ }
+
+ for (const auto* entry : group->entries()) {
+ if (entry->isRecycled()) {
+ continue;
+ }
+
+ // Skip entries with empty password
+ if (entry->password().isEmpty()) {
+ continue;
+ }
+
+ // Add entry if its password isn't at least "good"
+ const auto item = QSharedPointer- (new Item(group, entry, m_checker.evaluate(entry)));
+ if (item->health->quality() < PasswordHealth::Quality::Good) {
+ m_items.append(item);
+ }
+ }
+ }
+
+ // Sort the result so that the worst passwords (least score)
+ // are at the top
+ std::sort(m_items.begin(), m_items.end(), [](QSharedPointer
- x, QSharedPointer
- y) { return *x < *y; });
+}
+
+ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
+ : QWidget(parent)
+ , m_ui(new Ui::ReportsWidgetHealthcheck())
+ , m_errorIcon(FilePath::instance()->icon("status", "dialog-error"))
+{
+ m_ui->setupUi(this);
+
+ m_referencesModel.reset(new QStandardItemModel());
+ m_ui->healthcheckTableView->setModel(m_referencesModel.data());
+ m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection);
+ m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+
+ connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
+}
+
+ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck()
+{
+}
+
+void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer
health,
+ const Group* group,
+ const Entry* entry)
+{
+ QString descr, tip;
+ QColor qualityColor;
+ const auto quality = health->quality();
+ switch (quality) {
+ case PasswordHealth::Quality::Bad:
+ descr = tr("Bad", "Password quality");
+ tip = tr("Bad — password must be changed");
+ qualityColor.setNamedColor("red");
+ break;
+
+ case PasswordHealth::Quality::Poor:
+ descr = tr("Poor", "Password quality");
+ tip = tr("Poor — password should be changed");
+ qualityColor.setNamedColor("orange");
+ break;
+
+ case PasswordHealth::Quality::Weak:
+ descr = tr("Weak", "Password quality");
+ tip = tr("Weak — consider changing the password");
+ qualityColor.setNamedColor("yellow");
+ break;
+
+ case PasswordHealth::Quality::Good:
+ case PasswordHealth::Quality::Excellent:
+ qualityColor.setNamedColor("green");
+ break;
+ }
+
+ auto row = QList();
+ row << new QStandardItem(descr);
+ row << new QStandardItem(entry->iconPixmap(), entry->title());
+ row << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"));
+ row << new QStandardItem(QString::number(health->score()));
+ row << new QStandardItem(health->scoreReason());
+
+ // Set background color of first column according to password quality.
+ // Set the same as foreground color so the description is usually
+ // invisible, it's just for screen readers etc.
+ QBrush brush(qualityColor);
+ row[0]->setForeground(brush);
+ row[0]->setBackground(brush);
+
+ // Set tooltips
+ row[0]->setToolTip(tip);
+ row[4]->setToolTip(health->scoreDetails());
+
+ // Store entry pointer per table row (used in double click handler)
+ m_referencesModel->appendRow(row);
+ m_rowToEntry.append({group, entry});
+}
+
+void ReportsWidgetHealthcheck::loadSettings(QSharedPointer db)
+{
+ m_db = std::move(db);
+ m_healthCalculated = false;
+ m_referencesModel->clear();
+ m_rowToEntry.clear();
+
+ auto row = QList();
+ row << new QStandardItem(tr("Please wait, health data is being calculated..."));
+ m_referencesModel->appendRow(row);
+}
+
+void ReportsWidgetHealthcheck::showEvent(QShowEvent* event)
+{
+ QWidget::showEvent(event);
+
+ if (!m_healthCalculated) {
+ // Perform stats calculation on next event loop to allow widget to appear
+ m_healthCalculated = true;
+ QTimer::singleShot(0, this, SLOT(calculateHealth()));
+ }
+}
+
+void ReportsWidgetHealthcheck::calculateHealth()
+{
+ m_referencesModel->clear();
+
+ const QScopedPointer health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); }));
+ if (health->items().empty()) {
+ // No findings
+ m_referencesModel->clear();
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, everything is healthy!"));
+ } else {
+ // Show our findings
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("") << tr("Title") << tr("Path") << tr("Score")
+ << tr("Reason"));
+ for (const auto& item : health->items()) {
+ addHealthRow(item->health, item->group, item->entry);
+ }
+ }
+
+ m_ui->healthcheckTableView->resizeRowsToContents();
+}
+
+void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ const auto row = m_rowToEntry[index.row()];
+ const auto group = row.first;
+ const auto entry = row.second;
+ if (group && entry) {
+ emit entryActivated(group, const_cast(entry));
+ }
+}
+
+void ReportsWidgetHealthcheck::saveSettings()
+{
+ // nothing to do - the tab is passive
+}
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h
new file mode 100644
index 000000000..bf0cf531e
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHealthcheck.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
+#define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
+
+#include "gui/entry/EntryModel.h"
+#include
+#include
+#include
+#include
+
+class Database;
+class Entry;
+class Group;
+class PasswordHealth;
+class QStandardItemModel;
+
+namespace Ui
+{
+ class ReportsWidgetHealthcheck;
+}
+
+class ReportsWidgetHealthcheck : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr);
+ ~ReportsWidgetHealthcheck();
+
+ void loadSettings(QSharedPointer db);
+ void saveSettings();
+
+protected:
+ void showEvent(QShowEvent* event) override;
+
+signals:
+ void entryActivated(const Group* group, Entry* entry);
+
+public slots:
+ void calculateHealth();
+ void emitEntryActivated(const QModelIndex& index);
+
+private:
+ void addHealthRow(QSharedPointer, const Group*, const Entry*);
+
+ QScopedPointer m_ui;
+
+ bool m_healthCalculated = false;
+ QIcon m_errorIcon;
+ QScopedPointer m_referencesModel;
+ QSharedPointer m_db;
+ QList> m_rowToEntry;
+};
+
+#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.ui b/src/gui/reports/ReportsWidgetHealthcheck.ui
new file mode 100644
index 000000000..48d8df07f
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHealthcheck.ui
@@ -0,0 +1,79 @@
+
+
+ ReportsWidgetHealthcheck
+
+
+
+ 0
+ 0
+ 327
+ 379
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Health Check
+
+
+ -
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ true
+
+
+ Qt::ElideMiddle
+
+
+ false
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
+
+ true
+
+
+
+ Hover over reason to show additional details. Double-click entries to edit.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp
similarity index 86%
rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp
rename to src/gui/reports/ReportsWidgetStatistics.cpp
index b02741adb..bc642af78 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp
+++ b/src/gui/reports/ReportsWidgetStatistics.cpp
@@ -15,15 +15,15 @@
* along with this program. If not, see .
*/
-#include "DatabaseSettingsWidgetStatistics.h"
-#include "ui_DatabaseSettingsWidgetStatistics.h"
+#include "ReportsWidgetStatistics.h"
+#include "ui_ReportsWidgetStatistics.h"
#include "core/AsyncTask.h"
#include "core/Database.h"
#include "core/FilePath.h"
#include "core/Group.h"
#include "core/Metadata.h"
-#include "zxcvbn.h"
+#include "core/PasswordHealth.h"
#include
#include
@@ -48,6 +48,7 @@ namespace
// Ctor does all the work
explicit Stats(QSharedPointer db)
: modified(QFileInfo(db->filePath()).lastModified())
+ , m_db(db)
{
gatherStats(db->rootGroup()->groupsRecursive(true));
}
@@ -92,19 +93,27 @@ namespace
}
private:
+ QSharedPointer m_db;
QHash m_passwords;
void gatherStats(const QList& groups)
{
+ auto checker = HealthChecker(m_db);
+
for (const auto* group : groups) {
// Don't count anything in the recycle bin
- if (group == group->database()->metadata()->recycleBin()) {
+ if (group->isRecycled()) {
continue;
}
++nGroups;
for (const auto* entry : group->entries()) {
+ // Don't count anything in the recycle bin
+ if (entry->isRecycled()) {
+ continue;
+ }
+
++nEntries;
if (entry->isExpired()) {
@@ -125,7 +134,7 @@ namespace
}
// Speed up Zxcvbn process by excluding very long passwords and most passphrases
- if (pwd.size() < 25 && ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr) < 65) {
+ if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
++nPwdsWeak;
}
@@ -138,9 +147,9 @@ namespace
};
} // namespace
-DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* parent)
+ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent)
: QWidget(parent)
- , m_ui(new Ui::DatabaseSettingsWidgetStatistics())
+ , m_ui(new Ui::ReportsWidgetStatistics())
, m_errIcon(FilePath::instance()->icon("status", "dialog-error"))
{
m_ui->setupUi(this);
@@ -148,14 +157,15 @@ DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* pare
m_referencesModel.reset(new QStandardItemModel());
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Name") << tr("Value"));
m_ui->statisticsTableView->setModel(m_referencesModel.data());
+ m_ui->statisticsTableView->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->statisticsTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
}
-DatabaseSettingsWidgetStatistics::~DatabaseSettingsWidgetStatistics()
+ReportsWidgetStatistics::~ReportsWidgetStatistics()
{
}
-void DatabaseSettingsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg)
+void ReportsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg)
{
auto row = QList();
row << new QStandardItem(name);
@@ -170,7 +180,7 @@ void DatabaseSettingsWidgetStatistics::addStatsRow(QString name, QString value,
}
};
-void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer db)
+void ReportsWidgetStatistics::loadSettings(QSharedPointer db)
{
m_db = std::move(db);
m_statsCalculated = false;
@@ -178,7 +188,7 @@ void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer db)
addStatsRow(tr("Please wait, database statistics are being calculated..."), "");
}
-void DatabaseSettingsWidgetStatistics::showEvent(QShowEvent* event)
+void ReportsWidgetStatistics::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
@@ -189,9 +199,9 @@ void DatabaseSettingsWidgetStatistics::showEvent(QShowEvent* event)
}
}
-void DatabaseSettingsWidgetStatistics::calculateStats()
+void ReportsWidgetStatistics::calculateStats()
{
- const auto stats = AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); });
+ const QScopedPointer stats(AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); }));
m_referencesModel->clear();
addStatsRow(tr("Database name"), m_db->metadata()->name());
@@ -231,7 +241,7 @@ void DatabaseSettingsWidgetStatistics::calculateStats()
tr("Average password length is less than ten characters. Longer passwords provide more security."));
}
-void DatabaseSettingsWidgetStatistics::saveSettings()
+void ReportsWidgetStatistics::saveSettings()
{
// nothing to do - the tab is passive
}
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h b/src/gui/reports/ReportsWidgetStatistics.h
similarity index 74%
rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h
rename to src/gui/reports/ReportsWidgetStatistics.h
index 2bd42f13d..cc11a75f5 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h
+++ b/src/gui/reports/ReportsWidgetStatistics.h
@@ -15,8 +15,8 @@
* along with this program. If not, see .
*/
-#ifndef KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
-#define KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
+#ifndef KEEPASSXC_REPORTSWIDGETSTATISTICS_H
+#define KEEPASSXC_REPORTSWIDGETSTATISTICS_H
#include
#include
@@ -26,15 +26,15 @@ class QStandardItemModel;
namespace Ui
{
- class DatabaseSettingsWidgetStatistics;
+ class ReportsWidgetStatistics;
}
-class DatabaseSettingsWidgetStatistics : public QWidget
+class ReportsWidgetStatistics : public QWidget
{
Q_OBJECT
public:
- explicit DatabaseSettingsWidgetStatistics(QWidget* parent = nullptr);
- ~DatabaseSettingsWidgetStatistics();
+ explicit ReportsWidgetStatistics(QWidget* parent = nullptr);
+ ~ReportsWidgetStatistics();
void loadSettings(QSharedPointer db);
void saveSettings();
@@ -46,7 +46,7 @@ private slots:
void calculateStats();
private:
- QScopedPointer m_ui;
+ QScopedPointer m_ui;
bool m_statsCalculated = false;
QIcon m_errIcon;
@@ -56,4 +56,4 @@ private:
void addStatsRow(QString name, QString value, bool bad = false, QString badMsg = "");
};
-#endif // KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
+#endif // KEEPASSXC_REPORTSWIDGETSTATISTICS_H
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui b/src/gui/reports/ReportsWidgetStatistics.ui
similarity index 94%
rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui
rename to src/gui/reports/ReportsWidgetStatistics.ui
index ed9d6346e..1f3bf5fea 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui
+++ b/src/gui/reports/ReportsWidgetStatistics.ui
@@ -1,7 +1,7 @@
- DatabaseSettingsWidgetStatistics
-
+ ReportsWidgetStatistics
+
0
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index fc27f48d3..c3f1c0e22 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -176,6 +176,9 @@ add_unit_test(NAME testmerge SOURCES TestMerge.cpp
add_unit_test(NAME testpasswordgenerator SOURCES TestPasswordGenerator.cpp
LIBS ${TEST_LIBRARIES})
+add_unit_test(NAME testpasswordhealth SOURCES TestPasswordHealth.cpp
+ LIBS ${TEST_LIBRARIES})
+
add_unit_test(NAME testpassphrasegenerator SOURCES TestPassphraseGenerator.cpp
LIBS ${TEST_LIBRARIES})
diff --git a/tests/TestPasswordHealth.cpp b/tests/TestPasswordHealth.cpp
new file mode 100644
index 000000000..238b78b92
--- /dev/null
+++ b/tests/TestPasswordHealth.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "TestPasswordHealth.h"
+#include "TestGlobal.h"
+
+#include "core/PasswordHealth.h"
+
+QTEST_GUILESS_MAIN(TestPasswordHealth)
+
+void TestPasswordHealth::initTestCase()
+{
+}
+
+void TestPasswordHealth::testNoDb()
+{
+ const auto empty = PasswordHealth("");
+ QCOMPARE(empty.score(), 0);
+ QCOMPARE(empty.entropy(), 0.0);
+ QCOMPARE(empty.quality(), PasswordHealth::Quality::Bad);
+ QVERIFY(!empty.scoreReason().isEmpty());
+ QVERIFY(!empty.scoreDetails().isEmpty());
+
+ const auto poor = PasswordHealth("secret");
+ QCOMPARE(poor.score(), 6);
+ QCOMPARE(int(poor.entropy()), 6);
+ QCOMPARE(poor.quality(), PasswordHealth::Quality::Poor);
+ QVERIFY(!poor.scoreReason().isEmpty());
+ QVERIFY(!poor.scoreDetails().isEmpty());
+
+ const auto weak = PasswordHealth("Yohb2ChR4");
+ QCOMPARE(weak.score(), 47);
+ QCOMPARE(int(weak.entropy()), 47);
+ QCOMPARE(weak.quality(), PasswordHealth::Quality::Weak);
+ QVERIFY(!weak.scoreReason().isEmpty());
+ QVERIFY(!weak.scoreDetails().isEmpty());
+
+ const auto good = PasswordHealth("MIhIN9UKrgtPL2hp");
+ QCOMPARE(good.score(), 78);
+ QCOMPARE(int(good.entropy()), 78);
+ QCOMPARE(good.quality(), PasswordHealth::Quality::Good);
+ QVERIFY(good.scoreReason().isEmpty());
+ QVERIFY(good.scoreDetails().isEmpty());
+
+ const auto excellent = PasswordHealth("prompter-ream-oversleep-step-extortion-quarrel-reflected-prefix");
+ QCOMPARE(excellent.score(), 164);
+ QCOMPARE(int(excellent.entropy()), 164);
+ QCOMPARE(excellent.quality(), PasswordHealth::Quality::Excellent);
+ QVERIFY(excellent.scoreReason().isEmpty());
+ QVERIFY(excellent.scoreDetails().isEmpty());
+}
diff --git a/tests/TestPasswordHealth.h b/tests/TestPasswordHealth.h
new file mode 100644
index 000000000..2d887a7de
--- /dev/null
+++ b/tests/TestPasswordHealth.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSX_TESTPASSWORDHEALTH_H
+#define KEEPASSX_TESTPASSWORDHEALTH_H
+
+#include
+
+class TestPasswordHealth : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void testNoDb();
+};
+
+#endif // KEEPASSX_TESTPASSWORDHEALTH_H
diff --git a/utils/makeicons.sh b/utils/makeicons.sh
index 6efc608ee..887874161 100644
--- a/utils/makeicons.sh
+++ b/utils/makeicons.sh
@@ -99,6 +99,7 @@ map() {
group-edit) echo folder-edit-outline ;;
group-empty-trash) echo trash-can-outline ;;
group-new) echo folder-plus-outline ;;
+ health) echo heart-pulse ;;
help-about) echo information-outline ;;
internet-web-browser) echo web ;;
key-enter) echo keyboard-variant ;;