Files
keepassxc/src/gui/reports/ReportsWidgetStatistics.cpp
Wolfram Rösler 3c19fdd193 Reports: Add "Known Bad" flag for entries
* 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.
2020-05-08 20:51:11 -04:00

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
}