Add "Statistics" page to Database Settings dialog (#2034)

Added new page "Statistics" to the Database Settings dialog that shows information like number of groups and entries, number of unique and re-used passwords, average password length, etc.
Show warnings for problematic values with explainations for the user in tooltips.

Fixes #2034

Database statistics icon:
Downloaded from: https://www.flaticon.com/authors/freepik
Original source: https://www.flaticon.com/free-icon/bars-chart_265733
This commit is contained in:
Wolfram Rösler 2019-10-13 11:45:58 -04:00 committed by Jonathan White
parent ca0c4f5a3d
commit 8afb1f17b4
12 changed files with 448 additions and 7 deletions

View File

@ -13,6 +13,7 @@
- CLI: Add group commands (mv, mkdir and rmdir) [#3313]. - CLI: Add group commands (mv, mkdir and rmdir) [#3313].
- CLI: Add interactive shell mode command `open` [#3224](https://github.com/keepassxreboot/keepassxc/issues/3224) - CLI: Add interactive shell mode command `open` [#3224](https://github.com/keepassxreboot/keepassxc/issues/3224)
- Add "Paper Backup" aka "Export to HTML file" to the "Database" menu [#3277](https://github.com/keepassxreboot/keepassxc/pull/3277) - Add "Paper Backup" aka "Export to HTML file" to the "Database" menu [#3277](https://github.com/keepassxreboot/keepassxc/pull/3277)
- Add statistics panel with information about the database (number of entries, number of unique passwords, etc.) to the Database Settings dialog [#2034](https://github.com/keepassxreboot/keepassxc/issues/2034)
### Changed ### Changed

View File

@ -245,3 +245,6 @@ License: MIT
Files: share/icons/application/scalable/apps/freedesktop.svg Files: share/icons/application/scalable/apps/freedesktop.svg
Copyright: GPL-2+ Copyright: GPL-2+
Comment: from Freedesktop.org website Comment: from Freedesktop.org website
Files: share/icons/application/32x32/actions/statistics.png
Copyright: Icon made by Freepik from https://www.flaticon.com/free-icon/bars-chart_265733

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -148,6 +148,8 @@ set(keepassx_SOURCES
gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp
gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp
gui/dbsettings/DatabaseSettingsPageStatistics.cpp
gui/settings/SettingsWidget.cpp gui/settings/SettingsWidget.cpp
gui/widgets/ElidedLabel.cpp gui/widgets/ElidedLabel.cpp
gui/widgets/PopupHelpWidget.cpp gui/widgets/PopupHelpWidget.cpp

View File

@ -19,6 +19,7 @@
#include "DatabaseSettingsDialog.h" #include "DatabaseSettingsDialog.h"
#include "ui_DatabaseSettingsDialog.h" #include "ui_DatabaseSettingsDialog.h"
#include "DatabaseSettingsPageStatistics.h"
#include "DatabaseSettingsWidgetEncryption.h" #include "DatabaseSettingsWidgetEncryption.h"
#include "DatabaseSettingsWidgetGeneral.h" #include "DatabaseSettingsWidgetGeneral.h"
#include "DatabaseSettingsWidgetMasterKey.h" #include "DatabaseSettingsWidgetMasterKey.h"
@ -84,6 +85,8 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key")); m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key"));
m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings")); m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings"));
addSettingsPage(new DatabaseSettingsPageStatistics());
#if defined(WITH_XC_KEESHARE) #if defined(WITH_XC_KEESHARE)
addSettingsPage(new DatabaseSettingsPageKeeShare()); addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif #endif

View File

@ -0,0 +1,52 @@
/*
* 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 "DatabaseSettingsPageStatistics.h"
#include "DatabaseSettingsWidgetStatistics.h"
#include "core/Database.h"
#include "core/FilePath.h"
#include "core/Group.h"
#include <QApplication>
QString DatabaseSettingsPageStatistics::name()
{
return QApplication::tr("Statistics");
}
QIcon DatabaseSettingsPageStatistics::icon()
{
return FilePath::instance()->icon("actions", "statistics");
}
QWidget* DatabaseSettingsPageStatistics::createWidget()
{
return new DatabaseSettingsWidgetStatistics();
}
void DatabaseSettingsPageStatistics::loadSettings(QWidget* widget, QSharedPointer<Database> db)
{
DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetStatistics*>(widget);
settingsWidget->loadSettings(db);
}
void DatabaseSettingsPageStatistics::saveSettings(QWidget* widget)
{
DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetStatistics*>(widget);
settingsWidget->saveSettings();
}

View File

@ -0,0 +1,35 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
#define KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
#include <QWidget>
#include "DatabaseSettingsDialog.h"
class DatabaseSettingsPageStatistics : public IDatabaseSettingsPage
{
public:
QString name() override;
QIcon icon() override;
QWidget* createWidget() override;
void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
void saveSettings(QWidget* widget) override;
};
#endif // KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H

View File

@ -0,0 +1,215 @@
/*
* 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 "DatabaseSettingsWidgetStatistics.h"
#include "ui_DatabaseSettingsWidgetStatistics.h"
#include "core/Database.h"
#include "core/FilePath.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "zxcvbn/zxcvbn.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 pwdTotalLen = 0; // Total length of all passwords
// Ctor does all the work
explicit Stats(QSharedPointer<Database> db)
: modified(QFileInfo(db->filePath()).lastModified())
{
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:
QHash<QString, int> m_passwords;
void gatherStats(const QList<Group*>& groups)
{
for (const auto* group : groups) {
// Don't count anything in the recycle bin
if (group == group->database()->metadata()->recycleBin()) {
continue;
}
++nGroups;
for (const auto* entry : group->entries()) {
++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;
}
if (ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr) < 65) {
++nPwdsWeak;
}
pwdTotalLen += pwd.size();
m_passwords[pwd]++;
}
}
}
}
};
} // namespace
DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::DatabaseSettingsWidgetStatistics())
, m_errIcon(FilePath::instance()->icon("status", "dialog-error"))
{
m_ui->setupUi(this);
}
DatabaseSettingsWidgetStatistics::~DatabaseSettingsWidgetStatistics()
{
}
void DatabaseSettingsWidgetStatistics::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 DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
{
m_referencesModel.reset(new QStandardItemModel());
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Name") << tr("Value"));
const auto stats = Stats(db);
addStatsRow(tr("Database name"), db->metadata()->name());
addStatsRow(tr("Description"), db->metadata()->description());
addStatsRow(tr("Location"), db->filePath());
addStatsRow(tr("Last saved"), stats.modified.toString(Qt::DefaultLocaleShortDate));
addStatsRow(tr("Unsaved changes"),
db->isModified() ? tr("yes") : tr("no"),
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("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."));
m_ui->sharedGroupsView->setModel(m_referencesModel.data());
m_ui->sharedGroupsView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
}
void DatabaseSettingsWidgetStatistics::saveSettings()
{
// nothing to do - the tab is passive
}

View File

@ -0,0 +1,51 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
#define KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
#include <QIcon>
#include <QWidget>
class Database;
class QStandardItemModel;
namespace Ui
{
class DatabaseSettingsWidgetStatistics;
}
class DatabaseSettingsWidgetStatistics : public QWidget
{
Q_OBJECT
public:
explicit DatabaseSettingsWidgetStatistics(QWidget* parent = nullptr);
~DatabaseSettingsWidgetStatistics();
void loadSettings(QSharedPointer<Database> db);
void saveSettings();
private:
QScopedPointer<Ui::DatabaseSettingsWidgetStatistics> m_ui;
QIcon m_errIcon;
QScopedPointer<QStandardItemModel> m_referencesModel;
void addStatsRow(QString name, QString value, bool bad = false, QString badMsg = "");
};
#endif // KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DatabaseSettingsWidgetStatistics</class>
<widget class="QWidget" name="DatabaseSettingsWidgetStatistics">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>327</width>
<height>379</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="enableGroupBox">
<property name="title">
<string>Statistics</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="sharedGroupsView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Hover over lines with error icons for further information.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>