Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWolfram Rösler <wolfram@roesler-ac.de>2020-04-03 23:01:00 +0300
committerJonathan White <support@dmapps.us>2020-05-09 03:51:11 +0300
commit3c19fdd193336c9830fa112a720b42292c37e28b (patch)
treee260d462d3d90c56cf56580a3d3b45d62f738fb7
parentce8f32e7973b7f58eb360dd641c8e9b9f990dda3 (diff)
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.
-rw-r--r--COPYING2
-rw-r--r--share/demo.kdbxbin65285 -> 65637 bytes
-rw-r--r--share/icons/application/scalable/actions/reports-exclude.svg1
-rw-r--r--share/icons/application/scalable/actions/reports.svg1
-rw-r--r--share/icons/icons.qrc4
-rw-r--r--src/core/PasswordHealth.cpp3
-rw-r--r--src/core/PasswordHealth.h8
-rw-r--r--src/gui/MainWindow.cpp2
-rw-r--r--src/gui/entry/EditEntryWidget.cpp12
-rw-r--r--src/gui/entry/EditEntryWidgetAdvanced.ui25
-rw-r--r--src/gui/reports/ReportsWidgetHealthcheck.cpp118
-rw-r--r--src/gui/reports/ReportsWidgetHealthcheck.h6
-rw-r--r--src/gui/reports/ReportsWidgetHealthcheck.ui95
-rw-r--r--src/gui/reports/ReportsWidgetHibp.cpp132
-rw-r--r--src/gui/reports/ReportsWidgetHibp.h7
-rw-r--r--src/gui/reports/ReportsWidgetHibp.ui372
-rw-r--r--src/gui/reports/ReportsWidgetStatistics.cpp12
-rw-r--r--src/gui/reports/ReportsWidgetStatistics.ui85
-rw-r--r--tests/gui/TestGui.cpp12
-rw-r--r--utils/makeicons.sh2
20 files changed, 620 insertions, 279 deletions
diff --git a/COPYING b/COPYING
index 9d430b7e9..453b8ab60 100644
--- a/COPYING
+++ b/COPYING
@@ -182,6 +182,8 @@ Files: share/icons/application/scalable/categories/preferences-other.svg
share/icons/application/scalable/actions/document-save-as.svg
share/icons/application/scalable/actions/refresh.svg
share/icons/application/scalable/actions/clipboard-text.svg
+ share/icons/application/scalable/actions/reports.svg
+ share/icons/application/scalable/actions/reports-exclude.svg
Copyright: 2019 Austin Andrews <http://templarian.com/>
License: SIL OPEN FONT LICENSE Version 1.1
Comment: Taken from Material Design icon set (https://github.com/templarian/MaterialDesign/)
diff --git a/share/demo.kdbx b/share/demo.kdbx
index 1f2cb1472..736fe7544 100644
--- a/share/demo.kdbx
+++ b/share/demo.kdbx
Binary files differ
diff --git a/share/icons/application/scalable/actions/reports-exclude.svg b/share/icons/application/scalable/actions/reports-exclude.svg
new file mode 100644
index 000000000..4418319dc
--- /dev/null
+++ b/share/icons/application/scalable/actions/reports-exclude.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-lightbulb-off-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2C9.76,2 7.78,3.05 6.5,4.68L7.93,6.11C8.84,4.84 10.32,4 12,4A5,5 0 0,1 17,9C17,10.68 16.16,12.16 14.89,13.06L16.31,14.5C17.94,13.21 19,11.24 19,9A7,7 0 0,0 12,2M3.28,4L2,5.27L5.04,8.3C5,8.53 5,8.76 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H14.73L18.73,22L20,20.72L3.28,4M7.23,10.5L12.73,16H10V13.58C8.68,13 7.66,11.88 7.23,10.5M9,20V21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9Z" /></svg> \ No newline at end of file
diff --git a/share/icons/application/scalable/actions/reports.svg b/share/icons/application/scalable/actions/reports.svg
new file mode 100644
index 000000000..3d62971d2
--- /dev/null
+++ b/share/icons/application/scalable/actions/reports.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-lightbulb-on-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M11,18H13V15.87C14.73,15.43 16,13.86 16,12A4,4 0 0,0 12,8A4,4 0 0,0 8,12C8,13.86 9.27,15.43 11,15.87V18Z" /></svg> \ No newline at end of file
diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc
index 0e1150b6c..0f3ff6aff 100644
--- a/share/icons/icons.qrc
+++ b/share/icons/icons.qrc
@@ -50,7 +50,9 @@
<file>application/scalable/actions/password-generator.svg</file>
<file>application/scalable/actions/password-show-off.svg</file>
<file>application/scalable/actions/password-show-on.svg</file>
- <file>application/scalable/actions/refresh.svg</file>
+ <file>application/scalable/actions/refresh.svg</file>
+ <file>application/scalable/actions/reports.svg</file>
+ <file>application/scalable/actions/reports-exclude.svg</file>
<file>application/scalable/actions/sort-alphabetical-ascending.svg</file>
<file>application/scalable/actions/sort-alphabetical-descending.svg</file>
<file>application/scalable/actions/statistics.svg</file>
diff --git a/src/core/PasswordHealth.cpp b/src/core/PasswordHealth.cpp
index c179db77c..bb313170a 100644
--- a/src/core/PasswordHealth.cpp
+++ b/src/core/PasswordHealth.cpp
@@ -24,6 +24,9 @@
#include "PasswordHealth.h"
#include "zxcvbn.h"
+// Define the static member variable with the custom field name
+const QString PasswordHealth::OPTION_KNOWN_BAD = QStringLiteral("KnownBad");
+
PasswordHealth::PasswordHealth(double entropy)
: m_score(entropy)
, m_entropy(entropy)
diff --git a/src/core/PasswordHealth.h b/src/core/PasswordHealth.h
index 70f83eee7..ef3249380 100644
--- a/src/core/PasswordHealth.h
+++ b/src/core/PasswordHealth.h
@@ -83,6 +83,14 @@ public:
return m_entropy;
}
+ /**
+ * Name of custom data field that holds the "this is a known
+ * bad password" flag. Legal values of the field are TRUE_STR
+ * and FALSE_STR, the default (used if the field doesn't exist)
+ * is false.
+ */
+ static const QString OPTION_KNOWN_BAD;
+
private:
int m_score = 0;
double m_entropy = 0.0;
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 0b3bb04aa..f20f3b9d1 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -355,7 +355,7 @@ MainWindow::MainWindow()
m_ui->actionDatabaseSave->setIcon(resources()->icon("document-save"));
m_ui->actionDatabaseSaveAs->setIcon(resources()->icon("document-save-as"));
m_ui->actionDatabaseClose->setIcon(resources()->icon("document-close"));
- m_ui->actionReports->setIcon(resources()->icon("help-about"));
+ m_ui->actionReports->setIcon(resources()->icon("reports"));
m_ui->actionChangeDatabaseSettings->setIcon(resources()->icon("document-edit"));
m_ui->actionChangeMasterKey->setIcon(resources()->icon("database-change-key"));
m_ui->actionLockDatabases->setIcon(resources()->icon("database-lock"));
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 90b49b3da..8f81d42cc 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -42,6 +42,7 @@
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Metadata.h"
+#include "core/PasswordHealth.h"
#include "core/Resources.h"
#include "core/TimeDelta.h"
#include "core/Tools.h"
@@ -423,6 +424,7 @@ void EditEntryWidget::setupEntryUpdate()
// Advanced tab
connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_advancedUi->knownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified()));
@@ -827,6 +829,9 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
editTriggers = QAbstractItemView::DoubleClicked;
}
m_advancedUi->attributesView->setEditTriggers(editTriggers);
+ m_advancedUi->knownBadCheckBox->setChecked(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD)
+ == TRUE_STR);
setupColorButton(true, entry->foregroundColor());
setupColorButton(false, entry->backgroundColor());
m_iconsWidget->setEnabled(!m_history);
@@ -1031,6 +1036,13 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
entry->setNotes(m_mainUi->notesEdit->toPlainText());
+ const auto wasKnownBad = entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR;
+ const auto isKnownBad = m_advancedUi->knownBadCheckBox->isChecked();
+ if (isKnownBad != wasKnownBad) {
+ entry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
+ }
+
if (m_advancedUi->fgColorCheckBox->isChecked() && m_advancedUi->fgColorButton->property("color").isValid()) {
entry->setForegroundColor(m_advancedUi->fgColorButton->property("color").toString());
} else {
diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui
index 7b079b676..8faa7a4f3 100644
--- a/src/gui/entry/EditEntryWidgetAdvanced.ui
+++ b/src/gui/entry/EditEntryWidgetAdvanced.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>532</width>
- <height>374</height>
+ <height>469</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -175,8 +175,30 @@
</widget>
</item>
<item>
+ <widget class="QCheckBox" name="knownBadCheckBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, the entry will not appear in reports like Health Check and HIBP even if it doesn't match the quality requirements (e. g. password entropy or re-use). You can set the check mark if the password is beyond your control (e. g. if it needs to be a four-digit PIN) to prevent it from cluttering the reports.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Exclude from database reports</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QWidget" name="colorsBox" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
+ <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="QCheckBox" name="fgColorCheckBox">
<property name="text">
@@ -293,6 +315,7 @@
<tabstop>editAttributeButton</tabstop>
<tabstop>protectAttributeButton</tabstop>
<tabstop>revealAttributeButton</tabstop>
+ <tabstop>knownBadCheckBox</tabstop>
<tabstop>fgColorCheckBox</tabstop>
<tabstop>fgColorButton</tabstop>
<tabstop>bgColorCheckBox</tabstop>
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp
index 6fa8e78a6..00194e182 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.cpp
+++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp
@@ -20,11 +20,13 @@
#include "core/AsyncTask.h"
#include "core/Database.h"
+#include "core/Global.h"
#include "core/Group.h"
#include "core/PasswordHealth.h"
#include "core/Resources.h"
#include "gui/styles/StateColorPalette.h"
+#include <QMenu>
#include <QSharedPointer>
#include <QStandardItemModel>
@@ -38,11 +40,14 @@ namespace
QPointer<const Group> group;
QPointer<const Entry> entry;
QSharedPointer<PasswordHealth> health;
+ bool knownBad = false;
Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> h)
: group(g)
, entry(e)
, health(h)
+ , knownBad(e->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && e->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR)
{
}
@@ -59,10 +64,16 @@ namespace
return m_items;
}
+ bool anyKnownBad() const
+ {
+ return m_anyKnownBad;
+ }
+
private:
QSharedPointer<Database> m_db;
HealthChecker m_checker;
QList<QSharedPointer<Item>> m_items;
+ bool m_anyKnownBad = false;
};
} // namespace
@@ -86,8 +97,13 @@ Health::Health(QSharedPointer<Database> db)
continue;
}
- // Add entry if its password isn't at least "good"
+ // Evaluate this entry
const auto item = QSharedPointer<Item>(new Item(group, entry, m_checker.evaluate(entry)));
+ if (item->knownBad) {
+ m_anyKnownBad = true;
+ }
+
+ // Add entry if its password isn't at least "good"
if (item->health->quality() < PasswordHealth::Quality::Good) {
m_items.append(item);
}
@@ -110,8 +126,10 @@ ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
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(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
+ connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
}
ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck()
@@ -120,7 +138,8 @@ ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck()
void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> health,
const Group* group,
- const Entry* entry)
+ const Entry* entry,
+ bool knownBad)
{
QString descr, tip;
QColor qualityColor;
@@ -151,9 +170,14 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> healt
break;
}
+ auto title = entry->title();
+ if (knownBad) {
+ title.append(tr(" (Excluded)"));
+ }
+
auto row = QList<QStandardItem*>();
row << new QStandardItem(descr);
- row << new QStandardItem(entry->iconPixmap(), entry->title());
+ row << new QStandardItem(entry->iconPixmap(), title);
row << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"));
row << new QStandardItem(QString::number(health->score()));
row << new QStandardItem(health->scoreReason());
@@ -167,6 +191,9 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> healt
// Set tooltips
row[0]->setToolTip(tip);
+ if (knownBad) {
+ row[1]->setToolTip(tr("This entry is being excluded from reports"));
+ }
row[4]->setToolTip(health->scoreDetails());
// Store entry pointer per table row (used in double click handler)
@@ -201,21 +228,41 @@ void ReportsWidgetHealthcheck::calculateHealth()
{
m_referencesModel->clear();
+ // Perform the health check
const QScopedPointer<Health> health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); }));
- if (health->items().empty()) {
- // No findings
- m_referencesModel->clear();
+
+ // Display entries that are marked as "known bad"?
+ const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
+
+ // Display the entries
+ m_rowToEntry.clear();
+ for (const auto& item : health->items()) {
+ if (item->knownBad && !showKnownBad) {
+ // Exclude this entry from the report
+ continue;
+ }
+
+ // Show the entry in the report
+ addHealthRow(item->health, item->group, item->entry, item->knownBad);
+ }
+
+ // Set the table header
+ if (m_referencesModel->rowCount() == 0) {
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();
+
+ // Show the "show known bad entries" checkbox if there's any known
+ // bad entry in the database.
+ if (health->anyKnownBad()) {
+ m_ui->showKnownBadCheckBox->show();
+ } else {
+ m_ui->showKnownBadCheckBox->hide();
+ }
}
void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
@@ -232,6 +279,57 @@ void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
}
}
+void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
+{
+
+ // Find which entry has been clicked
+ const auto index = m_ui->healthcheckTableView->indexAt(pos);
+ if (!index.isValid()) {
+ return;
+ }
+ m_contextmenuEntry = const_cast<Entry*>(m_rowToEntry[index.row()].second);
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ // Create the context menu
+ const auto menu = new QMenu(this);
+
+ // Create the "edit entry" menu item
+ const auto edit = new QAction(Resources::instance()->icon("entry-edit"), tr("Edit Entry..."), this);
+ menu->addAction(edit);
+ connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
+
+ // Create the "exclude from reports" menu item
+ const auto knownbad = new QAction(Resources::instance()->icon("reports-exclude"), tr("Exclude from reports"), this);
+ knownbad->setCheckable(true);
+ knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR);
+ menu->addAction(knownbad);
+ connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool)));
+
+ // Show the context menu
+ menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos));
+}
+
+void ReportsWidgetHealthcheck::editFromContextmenu()
+{
+ if (m_contextmenuEntry) {
+ emit entryActivated(m_contextmenuEntry);
+ }
+}
+
+void ReportsWidgetHealthcheck::toggleKnownBad(bool isKnownBad)
+{
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
+
+ calculateHealth();
+}
+
void ReportsWidgetHealthcheck::saveSettings()
{
// nothing to do - the tab is passive
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h
index 86931c9db..ca848e686 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.h
+++ b/src/gui/reports/ReportsWidgetHealthcheck.h
@@ -54,9 +54,12 @@ signals:
public slots:
void calculateHealth();
void emitEntryActivated(const QModelIndex& index);
+ void customMenuRequested(QPoint);
+ void editFromContextmenu();
+ void toggleKnownBad(bool);
private:
- void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*);
+ void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*, bool knownBad);
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
@@ -65,6 +68,7 @@ private:
QScopedPointer<QStandardItemModel> m_referencesModel;
QSharedPointer<Database> m_db;
QList<QPair<const Group*, const Entry*>> m_rowToEntry;
+ Entry* m_contextmenuEntry = nullptr;
};
#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.ui b/src/gui/reports/ReportsWidgetHealthcheck.ui
index 48d8df07f..202ca6b19 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.ui
+++ b/src/gui/reports/ReportsWidgetHealthcheck.ui
@@ -6,11 +6,11 @@
<rect>
<x>0</x>
<y>0</y>
- <width>327</width>
+ <width>505</width>
<height>379</height>
</rect>
</property>
- <layout class="QVBoxLayout" name="verticalLayout" stretch="0">
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
<property name="leftMargin">
<number>0</number>
</property>
@@ -24,52 +24,53 @@
<number>0</number>
</property>
<item>
- <widget class="QGroupBox" name="healthcheckGroupBox">
- <property name="title">
- <string>Health Check</string>
+ <widget class="QTableView" name="healthcheckTableView">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <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="QCheckBox" name="showKnownBadCheckBox">
+ <property name="text">
+ <string>Also show entries that have been excluded from reports</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="tipLabel">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Hover over reason to show additional details. Double-click entries to edit.</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QTableView" name="healthcheckTableView">
- <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>true</bool>
- </attribute>
- <attribute name="horizontalHeaderStretchLastSection">
- <bool>true</bool>
- </attribute>
- <attribute name="verticalHeaderVisible">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="tipLabel">
- <property name="font">
- <font>
- <italic>true</italic>
- </font>
- </property>
- <property name="text">
- <string>Hover over reason to show additional details. Double-click entries to edit.</string>
- </property>
- </widget>
- </item>
- </layout>
</widget>
</item>
</layout>
diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp
index d4c3e447b..3678c2932 100644
--- a/src/gui/reports/ReportsWidgetHibp.cpp
+++ b/src/gui/reports/ReportsWidgetHibp.cpp
@@ -20,11 +20,32 @@
#include "config-keepassx.h"
#include "core/Database.h"
+#include "core/Global.h"
#include "core/Group.h"
+#include "core/PasswordHealth.h"
+#include "core/Resources.h"
#include "gui/MessageBox.h"
+#include <QMenu>
#include <QStandardItemModel>
+namespace
+{
+ /*
+ * Check if an entry has been marked as "known bad password".
+ * These entries are to be excluded from the HIBP report.
+ *
+ * Question to reviewer: Should this be a member function of Entry?
+ * It's duplicated in EditEntryWidget::setForms, EditEntryWidget::updateEntryData,
+ * ReportsWidgetHealthcheck::customMenuRequested, and Health::Item::Item.
+ */
+ bool isKnownBad(const Entry* entry)
+ {
+ return entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR;
+ }
+} // namespace
+
ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::ReportsWidgetHibp())
@@ -37,6 +58,8 @@ ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
+ connect(m_ui->hibpTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
+ connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(makeHibpTable()));
#ifdef WITH_XC_NETWORKING
connect(&m_downloader, SIGNAL(hibpResult(QString, int)), SLOT(addHibpResult(QString, int)));
connect(&m_downloader, SIGNAL(fetchFailed(QString)), SLOT(fetchFailed(QString)));
@@ -104,18 +127,43 @@ void ReportsWidgetHibp::makeHibpTable()
return lhs.second > rhs.second;
});
+ // Display entries that are marked as "known bad"?
+ const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
+
+ // The colors for table cells
+ const auto red = QBrush("red");
+
// Build the table
+ bool anyKnownBad = false;
for (const auto& item : items) {
const auto entry = item.first;
const auto group = entry->group();
const auto count = item.second;
+ auto title = entry->title();
+
+ // If the entry is marked as known bad, hide it unless the
+ // checkbox is set.
+ bool knownBad = isKnownBad(entry);
+ if (knownBad) {
+ anyKnownBad = true;
+ if (!showKnownBad) {
+ continue;
+ }
+
+ title.append(tr(" (Excluded)"));
+ }
auto row = QList<QStandardItem*>();
- row << new QStandardItem(entry->iconPixmap(), entry->title())
+ row << new QStandardItem(entry->iconPixmap(), title)
<< new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"))
<< new QStandardItem(countToText(count));
+
+ if (knownBad) {
+ row[1]->setToolTip(tr("This entry is being excluded from reports"));
+ }
+
+ row[2]->setForeground(red);
m_referencesModel->appendRow(row);
- row[2]->setForeground(QBrush(QColor("red")));
// Store entry pointer per table row (used in double click handler)
m_rowToEntry.append(entry);
@@ -129,6 +177,22 @@ void ReportsWidgetHibp::makeHibpTable()
row[0]->setForeground(QBrush(QColor("red")));
}
+ // If we're done and everything is good, display a motivational message
+#ifdef WITH_XC_NETWORKING
+ if (m_downloader.passwordsRemaining() == 0 && m_pwndPasswords.isEmpty() && m_error.isEmpty()) {
+ m_referencesModel->clear();
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, no exposed passwords!"));
+ }
+#endif
+
+ // Show the "show known bad entries" checkbox if there's any known
+ // bad entry in the database.
+ if (anyKnownBad) {
+ m_ui->showKnownBadCheckBox->show();
+ } else {
+ m_ui->showKnownBadCheckBox->hide();
+ }
+
m_ui->hibpTableView->resizeRowsToContents();
m_ui->stackedWidget->setCurrentIndex(1);
@@ -176,7 +240,8 @@ void ReportsWidgetHibp::startValidation()
{
#ifdef WITH_XC_NETWORKING
// Collect all passwords in the database (unless recycled, and
- // unless empty) and submit them to the downloader.
+ // unless empty, and unless marked as "known bad") and submit them
+ // to the downloader.
for (const auto* entry : m_db->rootGroup()->entriesRecursive()) {
if (!entry->isRecycled() && !entry->password().isEmpty()) {
m_downloader.add(entry->password());
@@ -238,6 +303,7 @@ void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index)
// Found it, invoke entry editor
m_editedEntry = entry;
m_editedPassword = entry->password();
+ m_editedKnownBad = isKnownBad(entry);
emit entryActivated(const_cast<Entry*>(entry));
}
}
@@ -253,8 +319,13 @@ void ReportsWidgetHibp::refreshAfterEdit()
return;
}
- // No need to re-validate if there was no change
- if (m_editedEntry->password() == m_editedPassword) {
+ // No need to re-validate if there was no change that affects
+ // the HIBP result (i. e., change to the password or to the
+ // "known bad" flag)
+ if (m_editedEntry->password() == m_editedPassword && isKnownBad(m_editedEntry) == m_editedKnownBad) {
+ // Don't go through HIBP but still rebuild the table, the user might
+ // have edited the entry title.
+ makeHibpTable();
return;
}
@@ -270,6 +341,57 @@ void ReportsWidgetHibp::refreshAfterEdit()
m_editedEntry = nullptr;
}
+void ReportsWidgetHibp::customMenuRequested(QPoint pos)
+{
+
+ // Find which entry has been clicked
+ const auto index = m_ui->hibpTableView->indexAt(pos);
+ if (!index.isValid()) {
+ return;
+ }
+ m_contextmenuEntry = const_cast<Entry*>(m_rowToEntry[index.row()]);
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ // Create the context menu
+ const auto menu = new QMenu(this);
+
+ // Create the "edit entry" menu item
+ const auto edit = new QAction(Resources::instance()->icon("entry-edit"), tr("Edit Entry..."), this);
+ menu->addAction(edit);
+ connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
+
+ // Create the "exclude from reports" menu item
+ const auto knownbad = new QAction(Resources::instance()->icon("reports-exclude"), tr("Exclude from reports"), this);
+ knownbad->setCheckable(true);
+ knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR);
+ menu->addAction(knownbad);
+ connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool)));
+
+ // Show the context menu
+ menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos));
+}
+
+void ReportsWidgetHibp::editFromContextmenu()
+{
+ if (m_contextmenuEntry) {
+ emit entryActivated(m_contextmenuEntry);
+ }
+}
+
+void ReportsWidgetHibp::toggleKnownBad(bool isKnownBad)
+{
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
+
+ makeHibpTable();
+}
+
void ReportsWidgetHibp::saveSettings()
{
// nothing to do - the tab is passive
diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h
index cd8456121..f7d1a754b 100644
--- a/src/gui/reports/ReportsWidgetHibp.h
+++ b/src/gui/reports/ReportsWidgetHibp.h
@@ -58,9 +58,12 @@ public slots:
void emitEntryActivated(const QModelIndex&);
void addHibpResult(const QString&, int);
void fetchFailed(const QString& error);
+ void makeHibpTable();
+ void customMenuRequested(QPoint);
+ void editFromContextmenu();
+ void toggleKnownBad(bool);
private:
- void makeHibpTable();
void startValidation();
static QString countToText(int count);
@@ -73,6 +76,8 @@ private:
QList<const Entry*> m_rowToEntry; // List index is table row
QPointer<const Entry> m_editedEntry; // The entry we're currently editing
QString m_editedPassword; // The old password of the entry we're editing
+ bool m_editedKnownBad; // The old "known bad" flag of the entry we're editing
+ Entry* m_contextmenuEntry = nullptr; // The entry that was right-clicked
#ifdef WITH_XC_NETWORKING
HibpDownloader m_downloader; // This performs the actual HIBP online query
diff --git a/src/gui/reports/ReportsWidgetHibp.ui b/src/gui/reports/ReportsWidgetHibp.ui
index bda5da6fd..af5931193 100644
--- a/src/gui/reports/ReportsWidgetHibp.ui
+++ b/src/gui/reports/ReportsWidgetHibp.ui
@@ -11,176 +11,218 @@
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
+ <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="groupBox">
- <property name="title">
- <string>Have I Been Pwned?</string>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="currentIndex">
+ <number>1</number>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <item>
- <widget class="QStackedWidget" name="stackedWidget">
- <property name="currentIndex">
- <number>0</number>
- </property>
- <widget class="QWidget" name="confirmation">
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <property name="spacing">
- <number>15</number>
- </property>
- <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>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QLabel" name="label">
- <property name="maximumSize">
- <size>
- <width>450</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string>CAUTION: This report requires sending information to the Have I Been Pwned online service (https://haveibeenpwned.com). If you proceed, your database passwords will be cryptographically hashed and the first five characters of those hashes will be sent securely to this service. Your database remains secure and cannot be reconstituted from this information. However, the number of passwords you send and your IP address will be exposed to this service.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <property name="topMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QPushButton" name="validationButton">
- <property name="maximumSize">
- <size>
- <width>275</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string>Perform Online Analysis</string>
- </property>
- <property name="default">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QProgressBar" name="progressBar">
- <property name="value">
- <number>0</number>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ <widget class="QWidget" name="confirmation">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>15</number>
+ </property>
+ <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>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="maximumSize">
+ <size>
+ <width>450</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>CAUTION: This report requires sending information to the Have I Been Pwned online service (https://haveibeenpwned.com). If you proceed, your database passwords will be cryptographically hashed and the first five characters of those hashes will be sent securely to this service. Your database remains secure and cannot be reconstituted from this information. However, the number of passwords you send and your IP address will be exposed to this service.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="validationButton">
+ <property name="maximumSize">
+ <size>
+ <width>275</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Perform Online Analysis</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>0</number>
+ </property>
</widget>
- <widget class="QWidget" name="resultsTable">
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <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="QTableView" name="hibpTableView">
- <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="horizontalHeaderStretchLastSection">
- <bool>true</bool>
- </attribute>
- <attribute name="verticalHeaderVisible">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="resultsTable">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <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="QTableView" name="hibpTableView">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <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>
- <widget class="QWidget" name="noNetwork">
- <layout class="QVBoxLayout" name="verticalLayout_5">
- <item>
- <widget class="QLabel" name="networkNoticeLabel">
- <property name="maximumSize">
- <size>
- <width>450</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string>This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases.</string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer_2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showKnownBadCheckBox">
+ <property name="text">
+ <string>Also show entries that have been excluded from reports</string>
+ </property>
</widget>
- </widget>
- </item>
- </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="noNetwork">
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="networkNoticeLabel">
+ <property name="maximumSize">
+ <size>
+ <width>450</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
</layout>
diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp
index f5a99b363..400d82f29 100644
--- a/src/gui/reports/ReportsWidgetStatistics.cpp
+++ b/src/gui/reports/ReportsWidgetStatistics.cpp
@@ -20,6 +20,7 @@
#include "core/AsyncTask.h"
#include "core/Database.h"
+#include "core/Global.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/PasswordHealth.h"
@@ -43,6 +44,7 @@ namespace
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
@@ -138,6 +140,11 @@ namespace
++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]++;
}
@@ -235,6 +242,11 @@ void ReportsWidgetStatistics::calculateStats()
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(),
diff --git a/src/gui/reports/ReportsWidgetStatistics.ui b/src/gui/reports/ReportsWidgetStatistics.ui
index 1f3bf5fea..4b96dc51a 100644
--- a/src/gui/reports/ReportsWidgetStatistics.ui
+++ b/src/gui/reports/ReportsWidgetStatistics.ui
@@ -6,11 +6,11 @@
<rect>
<x>0</x>
<y>0</y>
- <width>327</width>
+ <width>397</width>
<height>379</height>
</rect>
</property>
- <layout class="QVBoxLayout" name="verticalLayout" stretch="0">
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<property name="leftMargin">
<number>0</number>
</property>
@@ -24,52 +24,43 @@
<number>0</number>
</property>
<item>
- <widget class="QGroupBox" name="statisticsGroupBox">
- <property name="title">
- <string>Statistics</string>
+ <widget class="QTableView" name="statisticsTableView">
+ <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="tipLabel">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Hover over lines with error icons for further information.</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QTableView" name="statisticsTableView">
- <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="tipLabel">
- <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>
diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp
index 3b97ffde8..7ce8a0f4a 100644
--- a/tests/gui/TestGui.cpp
+++ b/tests/gui/TestGui.cpp
@@ -45,6 +45,7 @@
#include "core/Entry.h"
#include "core/Group.h"
#include "core/Metadata.h"
+#include "core/PasswordHealth.h"
#include "core/Tools.h"
#include "crypto/Crypto.h"
#include "crypto/kdf/AesKdf.h"
@@ -442,6 +443,17 @@ void TestGui::testEditEntry()
QCOMPARE(entry->historyItems().size(), ++editCount);
QVERIFY(!applyButton->isEnabled());
+ // Test the "known bad" checkbox
+ editEntryWidget->setCurrentPage(1);
+ auto knownBadCheckBox = editEntryWidget->findChild<QCheckBox*>("knownBadCheckBox");
+ QVERIFY(knownBadCheckBox);
+ QCOMPARE(knownBadCheckBox->isChecked(), false);
+ knownBadCheckBox->setChecked(true);
+ QTest::mouseClick(applyButton, Qt::LeftButton);
+ QCOMPARE(entry->historyItems().size(), ++editCount);
+ QCOMPARE(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD), true);
+ QCOMPARE(entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD), TRUE_STR);
+
// Test entry colors (simulate choosing a color)
editEntryWidget->setCurrentPage(1);
auto fgColor = QString("#FF0000");
diff --git a/utils/makeicons.sh b/utils/makeicons.sh
index d4a8848e1..61ee74e4d 100644
--- a/utils/makeicons.sh
+++ b/utils/makeicons.sh
@@ -117,6 +117,8 @@ map() {
preferences-other) echo file-document-edit-outline ;;
preferences-desktop-icons) echo emoticon-happy-outline ;;
preferences-system-network-sharing) echo lan ;;
+ reports) echo lightbulb-on-outline ;;
+ reports-exclude) echo lightbulb-off-outline ;;
security-high) echo shield-outline ;;
sort-alphabetical-ascending) echo sort-alphabetical-ascending ;;
sort-alphabetical-descending) echo sort-alphabetical-descending ;;