mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-22 15:39:57 -05:00
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.
This commit is contained in:
parent
ce8f32e797
commit
3c19fdd193
2
COPYING
2
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/)
|
||||
|
BIN
share/demo.kdbx
BIN
share/demo.kdbx
Binary file not shown.
@ -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>
|
After Width: | Height: | Size: 713 B |
1
share/icons/application/scalable/actions/reports.svg
Normal file
1
share/icons/application/scalable/actions/reports.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 758 B |
@ -51,6 +51,8 @@
|
||||
<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/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>
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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"));
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
@ -174,9 +174,31 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="knownBadCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>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.</p></body></html></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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
@ -23,14 +23,11 @@
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="healthcheckGroupBox">
|
||||
<property name="title">
|
||||
<string>Health Check</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableView" name="healthcheckTableView">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
@ -47,7 +44,7 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderVisible">
|
||||
<bool>true</bool>
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
@ -57,6 +54,13 @@
|
||||
</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">
|
||||
@ -71,9 +75,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -11,16 +11,22 @@
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Have I Been Pwned?</string>
|
||||
<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>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stackedWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="confirmation">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
@ -39,6 +45,19 @@
|
||||
<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>
|
||||
@ -82,13 +101,6 @@
|
||||
</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">
|
||||
@ -102,6 +114,13 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="resultsTable">
|
||||
@ -120,6 +139,9 @@
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTableView" name="hibpTableView">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
@ -135,6 +157,9 @@
|
||||
<property name="sortingEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
@ -143,10 +168,30 @@
|
||||
</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>
|
||||
</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">
|
||||
@ -182,9 +227,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -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(),
|
||||
|
@ -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>
|
||||
@ -23,12 +23,6 @@
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="statisticsGroupBox">
|
||||
<property name="title">
|
||||
<string>Statistics</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableView" name="statisticsTableView">
|
||||
<property name="editTriggers">
|
||||
@ -71,9 +65,6 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -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");
|
||||
|
@ -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 ;;
|
||||
|
Loading…
x
Reference in New Issue
Block a user