mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
* Fixes #4168 * Introduce a custom data element stored with an entry to indicate that it is a "Known Bad" entry. This flag causes database reports to skip these entries. * The current number of known bad entries is displayed in the statistics report. * Add context menu to reports to easily exclude entries.
260 lines
9.3 KiB
C++
260 lines
9.3 KiB
C++
/*
|
|
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "ReportsWidgetStatistics.h"
|
|
#include "ui_ReportsWidgetStatistics.h"
|
|
|
|
#include "core/AsyncTask.h"
|
|
#include "core/Database.h"
|
|
#include "core/Global.h"
|
|
#include "core/Group.h"
|
|
#include "core/Metadata.h"
|
|
#include "core/PasswordHealth.h"
|
|
#include "core/Resources.h"
|
|
|
|
#include <QFileInfo>
|
|
#include <QHash>
|
|
#include <QStandardItemModel>
|
|
|
|
namespace
|
|
{
|
|
class Stats
|
|
{
|
|
public:
|
|
// The statistics we collect:
|
|
QDateTime modified; // File modification time
|
|
int nGroups = 0; // Number of groups in the database
|
|
int nEntries = 0; // Number of entries (across all groups)
|
|
int nExpired = 0; // Number of expired entries
|
|
int nPwdsWeak = 0; // Number of weak or poor passwords
|
|
int nPwdsShort = 0; // Number of passwords 8 characters or less in size
|
|
int nPwdsUnique = 0; // Number of unique passwords
|
|
int nPwdsReused = 0; // Number of non-unique passwords
|
|
int nKnownBad = 0; // Number of known bad entries
|
|
int pwdTotalLen = 0; // Total length of all passwords
|
|
|
|
// Ctor does all the work
|
|
explicit Stats(QSharedPointer<Database> db)
|
|
: modified(QFileInfo(db->filePath()).lastModified())
|
|
, m_db(db)
|
|
{
|
|
gatherStats(db->rootGroup()->groupsRecursive(true));
|
|
}
|
|
|
|
// Get average password length
|
|
int averagePwdLength() const
|
|
{
|
|
return m_passwords.empty() ? 0 : pwdTotalLen / m_passwords.size();
|
|
}
|
|
|
|
// Get max number of password reuse (=how many entries
|
|
// share the same password)
|
|
int maxPwdReuse() const
|
|
{
|
|
int ret = 0;
|
|
for (const auto& count : m_passwords) {
|
|
ret = std::max(ret, count);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// A warning sign is displayed if one of the
|
|
// following returns true.
|
|
bool isAnyExpired() const
|
|
{
|
|
return nExpired > 0;
|
|
}
|
|
|
|
bool areTooManyPwdsReused() const
|
|
{
|
|
return nPwdsReused > nPwdsUnique / 10;
|
|
}
|
|
|
|
bool arePwdsReusedTooOften() const
|
|
{
|
|
return maxPwdReuse() > 3;
|
|
}
|
|
|
|
bool isAvgPwdTooShort() const
|
|
{
|
|
return averagePwdLength() < 10;
|
|
}
|
|
|
|
private:
|
|
QSharedPointer<Database> m_db;
|
|
QHash<QString, int> m_passwords;
|
|
|
|
void gatherStats(const QList<Group*>& groups)
|
|
{
|
|
auto checker = HealthChecker(m_db);
|
|
|
|
for (const auto* group : groups) {
|
|
// Don't count anything in the recycle bin
|
|
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()) {
|
|
++nExpired;
|
|
}
|
|
|
|
// Get password statistics
|
|
const auto pwd = entry->password();
|
|
if (!pwd.isEmpty()) {
|
|
if (!m_passwords.contains(pwd)) {
|
|
++nPwdsUnique;
|
|
} else {
|
|
++nPwdsReused;
|
|
}
|
|
|
|
if (pwd.size() < 8) {
|
|
++nPwdsShort;
|
|
}
|
|
|
|
// Speed up Zxcvbn process by excluding very long passwords and most passphrases
|
|
if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
|
|
++nPwdsWeak;
|
|
}
|
|
|
|
if (entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
|
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR) {
|
|
++nKnownBad;
|
|
}
|
|
|
|
pwdTotalLen += pwd.size();
|
|
m_passwords[pwd]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_ui(new Ui::ReportsWidgetStatistics())
|
|
, m_errIcon(Resources::instance()->icon("dialog-error"))
|
|
{
|
|
m_ui->setupUi(this);
|
|
|
|
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);
|
|
}
|
|
|
|
ReportsWidgetStatistics::~ReportsWidgetStatistics()
|
|
{
|
|
}
|
|
|
|
void ReportsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg)
|
|
{
|
|
auto row = QList<QStandardItem*>();
|
|
row << new QStandardItem(name);
|
|
row << new QStandardItem(value);
|
|
m_referencesModel->appendRow(row);
|
|
|
|
if (bad) {
|
|
m_referencesModel->item(m_referencesModel->rowCount() - 1, 1)->setIcon(m_errIcon);
|
|
if (!badMsg.isEmpty()) {
|
|
m_referencesModel->item(m_referencesModel->rowCount() - 1, 1)->setToolTip(badMsg);
|
|
}
|
|
}
|
|
};
|
|
|
|
void ReportsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
|
|
{
|
|
m_db = std::move(db);
|
|
m_statsCalculated = false;
|
|
m_referencesModel->clear();
|
|
addStatsRow(tr("Please wait, database statistics are being calculated..."), "");
|
|
}
|
|
|
|
void ReportsWidgetStatistics::showEvent(QShowEvent* event)
|
|
{
|
|
QWidget::showEvent(event);
|
|
|
|
if (!m_statsCalculated) {
|
|
// Perform stats calculation on next event loop to allow widget to appear
|
|
m_statsCalculated = true;
|
|
QTimer::singleShot(0, this, SLOT(calculateStats()));
|
|
}
|
|
}
|
|
|
|
void ReportsWidgetStatistics::calculateStats()
|
|
{
|
|
const QScopedPointer<Stats> stats(AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); }));
|
|
|
|
m_referencesModel->clear();
|
|
addStatsRow(tr("Database name"), m_db->metadata()->name());
|
|
addStatsRow(tr("Description"), m_db->metadata()->description());
|
|
addStatsRow(tr("Location"), m_db->filePath());
|
|
addStatsRow(tr("Last saved"), stats->modified.toString(Qt::DefaultLocaleShortDate));
|
|
addStatsRow(tr("Unsaved changes"),
|
|
m_db->isModified() ? tr("yes") : tr("no"),
|
|
m_db->isModified(),
|
|
tr("The database was modified, but the changes have not yet been saved to disk."));
|
|
addStatsRow(tr("Number of groups"), QString::number(stats->nGroups));
|
|
addStatsRow(tr("Number of entries"), QString::number(stats->nEntries));
|
|
addStatsRow(tr("Number of expired entries"),
|
|
QString::number(stats->nExpired),
|
|
stats->isAnyExpired(),
|
|
tr("The database contains entries that have expired."));
|
|
addStatsRow(tr("Unique passwords"), QString::number(stats->nPwdsUnique));
|
|
addStatsRow(tr("Non-unique passwords"),
|
|
QString::number(stats->nPwdsReused),
|
|
stats->areTooManyPwdsReused(),
|
|
tr("More than 10% of passwords are reused. Use unique passwords when possible."));
|
|
addStatsRow(tr("Maximum password reuse"),
|
|
QString::number(stats->maxPwdReuse()),
|
|
stats->arePwdsReusedTooOften(),
|
|
tr("Some passwords are used more than three times. Use unique passwords when possible."));
|
|
addStatsRow(tr("Number of short passwords"),
|
|
QString::number(stats->nPwdsShort),
|
|
stats->nPwdsShort > 0,
|
|
tr("Recommended minimum password length is at least 8 characters."));
|
|
addStatsRow(tr("Number of weak passwords"),
|
|
QString::number(stats->nPwdsWeak),
|
|
stats->nPwdsWeak > 0,
|
|
tr("Recommend using long, randomized passwords with a rating of 'good' or 'excellent'."));
|
|
addStatsRow(tr("Entries excluded from reports"),
|
|
QString::number(stats->nKnownBad),
|
|
stats->nKnownBad > 0,
|
|
tr("Excluding entries from reports, e. g. because they are known to have a poor password, isn't "
|
|
"necessarily a problem but you should keep an eye on them."));
|
|
addStatsRow(tr("Average password length"),
|
|
tr("%1 characters").arg(stats->averagePwdLength()),
|
|
stats->isAvgPwdTooShort(),
|
|
tr("Average password length is less than ten characters. Longer passwords provide more security."));
|
|
}
|
|
|
|
void ReportsWidgetStatistics::saveSettings()
|
|
{
|
|
// nothing to do - the tab is passive
|
|
}
|