Skip to content

Commit

Permalink
Reports: Add "Known Bad" flag for entries
Browse files Browse the repository at this point in the history
* Fixes keepassxreboot#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.
  • Loading branch information
wolframroesler authored and droidmonkey committed May 9, 2020
1 parent ce8f32e commit 3c19fdd
Show file tree
Hide file tree
Showing 20 changed files with 620 additions and 279 deletions.
2 changes: 2 additions & 0 deletions COPYING
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
Binary file modified share/demo.kdbx
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions share/icons/application/scalable/actions/reports.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion share/icons/icons.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
3 changes: 3 additions & 0 deletions src/core/PasswordHealth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions src/core/PasswordHealth.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ class PasswordHealth
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;
Expand Down
2 changes: 1 addition & 1 deletion src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
12 changes: 12 additions & 0 deletions src/gui/entry/EditEntryWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 24 additions & 1 deletion src/gui/entry/EditEntryWidgetAdvanced.ui
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -174,9 +174,31 @@
</layout>
</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">
Expand Down Expand Up @@ -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>
Expand Down
118 changes: 108 additions & 10 deletions src/gui/reports/ReportsWidgetHealthcheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand All @@ -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)
{
}

Expand All @@ -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

Expand All @@ -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);
}
Expand All @@ -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()
Expand All @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/gui/reports/ReportsWidgetHealthcheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ class ReportsWidgetHealthcheck : public QWidget
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;

Expand All @@ -65,6 +68,7 @@ public slots:
QScopedPointer<QStandardItemModel> m_referencesModel;
QSharedPointer<Database> m_db;
QList<QPair<const Group*, const Entry*>> m_rowToEntry;
Entry* m_contextmenuEntry = nullptr;
};

#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
Loading

0 comments on commit 3c19fdd

Please sign in to comment.