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 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 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

View File

@ -245,3 +245,6 @@ License: MIT
Files: share/icons/application/scalable/apps/freedesktop.svg
Copyright: GPL-2+
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/DatabaseSettingsWidgetEncryption.cpp
gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp
gui/dbsettings/DatabaseSettingsPageStatistics.cpp
gui/settings/SettingsWidget.cpp
gui/widgets/ElidedLabel.cpp
gui/widgets/PopupHelpWidget.cpp

View File

@ -257,7 +257,7 @@ namespace Bootstrap
nullptr, // do not change owner or group
pACL, // DACL specified
nullptr // do not change SACL
);
);
Cleanup:

View File

@ -19,6 +19,7 @@
#include "DatabaseSettingsDialog.h"
#include "ui_DatabaseSettingsDialog.h"
#include "DatabaseSettingsPageStatistics.h"
#include "DatabaseSettingsWidgetEncryption.h"
#include "DatabaseSettingsWidgetGeneral.h"
#include "DatabaseSettingsWidgetMasterKey.h"
@ -84,6 +85,8 @@ 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

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

@ -242,12 +242,12 @@ void DatabaseSettingsWidgetBrowser::convertAttributesToCustomData()
{
if (MessageBox::Yes
!= MessageBox::question(
this,
tr("Move KeePassHTTP attributes to custom data"),
tr("Do you really want to move all legacy browser integration data to the latest standard?\n"
"This is necessary to maintain compatibility with the browser plugin."),
MessageBox::Yes | MessageBox::Cancel,
MessageBox::Cancel)) {
this,
tr("Move KeePassHTTP attributes to custom data"),
tr("Do you really want to move all legacy browser integration data to the latest standard?\n"
"This is necessary to maintain compatibility with the browser plugin."),
MessageBox::Yes | MessageBox::Cancel,
MessageBox::Cancel)) {
return;
}

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>