Skip to content

Commit

Permalink
Add db statistic output to CLI db-info command.
Browse files Browse the repository at this point in the history
Closes #6920
  • Loading branch information
glysbaysb committed Oct 9, 2021
1 parent bd744d1 commit c03817e
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 125 deletions.
23 changes: 23 additions & 0 deletions src/cli/Info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
#include "Info.h"

#include "Utils.h"
#include "core/DatabaseStats.h"
#include "core/Global.h"
#include "core/Group.h"
#include "core/Metadata.h"

#include <QCommandLineParser>
Expand Down Expand Up @@ -47,5 +49,26 @@ int Info::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
} else {
out << QObject::tr("Recycle bin is not enabled.") << endl;
}

DatabaseStats stats(database);
out << QObject::tr("Database name: ") << database->metadata()->name() << endl;
out << QObject::tr("Description: ") << database->metadata()->description() << endl;
out << QObject::tr("Location: ") << database->filePath() << endl;
out << QObject::tr("Database created: ") <<
database->rootGroup()->timeInfo().creationTime().toString(Qt::DefaultLocaleShortDate) << endl;
out << QObject::tr("Last saved: ") << stats.modified.toString(Qt::DefaultLocaleShortDate) << endl;
out << QObject::tr("Unsaved changes: ") <<
(database->isModified() ? QObject::tr("yes") : QObject::tr("no")) << endl;
out << QObject::tr("Number of groups: ") << QString::number(stats.groupCount) << endl;
out << QObject::tr("Number of entries: ") << QString::number(stats.entryCount) << endl;
out << QObject::tr("Number of expired entries: ") << QString::number(stats.expiredEntries) << endl;
out << QObject::tr("Unique passwords: ") << QString::number(stats.uniquePasswords) << endl;
out << QObject::tr("Non-unique passwords: ") << QString::number(stats.reusedPasswords) << endl;
out << QObject::tr("Maximum password reuse: ") << QString::number(stats.maxPwdReuse()) << endl;
out << QObject::tr("Number of short passwords: ") << QString::number(stats.shortPasswords) << endl;
out << QObject::tr("Number of weak passwords: ") << QString::number(stats.weakPasswords) << endl;
out << QObject::tr("Entries excluded from reports: ") << QString::number(stats.excludedEntries) << endl;
out << QObject::tr("Average password length: ") << QObject::tr("%1 characters").arg(stats.averagePwdLength()) << endl;

return EXIT_SUCCESS;
}
144 changes: 144 additions & 0 deletions src/core/DatabaseStats.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright (C) 2021 KeePassXC Team <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef KEEPASSXC_DATABASESTATS_H
#define KEEPASSXC_DATABASESTATS_H
#include "PasswordHealth.h"
#include "core/Group.h"
#include <QFileInfo>
#include <cmath>
class DatabaseStats
{
public:
// The statistics we collect:
QDateTime modified; // File modification time
int groupCount = 0; // Number of groups in the database
int entryCount = 0; // Number of entries (across all groups)
int expiredEntries = 0; // Number of expired entries
int excludedEntries = 0; // Number of known bad entries
int weakPasswords = 0; // Number of weak or poor passwords
int shortPasswords = 0; // Number of passwords 8 characters or less in size
int uniquePasswords = 0; // Number of unique passwords
int reusedPasswords = 0; // Number of non-unique passwords
int totalPasswordLength = 0; // Total length of all passwords

// Ctor does all the work
explicit DatabaseStats(QSharedPointer<Database> db)
: modified(QFileInfo(db->filePath()).lastModified())
, m_db(db)
{
gatherStats(db->rootGroup()->groupsRecursive(true));
}

// Get average password length
int averagePwdLength() const
{
const auto passwords = uniquePasswords + reusedPasswords;
return passwords == 0 ? 0 : std::round(totalPasswordLength / double(passwords));
}

// Get max number of password reuse (=how many entries
// share the same password)
int maxPwdReuse() const
{
int ret = 0;
for (const auto& count : m_passwords) {
ret = std::max(ret, count);
}
return ret;
}

// A warning sign is displayed if one of the
// following returns true.
bool isAnyExpired() const
{
return expiredEntries > 0;
}

bool areTooManyPwdsReused() const
{
return reusedPasswords > uniquePasswords / 10;
}

bool arePwdsReusedTooOften() const
{
return maxPwdReuse() > 3;
}

bool isAvgPwdTooShort() const
{
return averagePwdLength() < 10;
}

private:
QSharedPointer<Database> m_db;
QHash<QString, int> m_passwords;

void gatherStats(const QList<Group*>& groups)
{
auto checker = HealthChecker(m_db);

for (const auto* group : groups) {
// Don't count anything in the recycle bin
if (group->isRecycled()) {
continue;
}

++groupCount;

for (const auto* entry : group->entries()) {
// Don't count anything in the recycle bin
if (entry->isRecycled()) {
continue;
}

++entryCount;

if (entry->isExpired()) {
++expiredEntries;
}

// Get password statistics
const auto pwd = entry->password();
if (!pwd.isEmpty()) {
if (!m_passwords.contains(pwd)) {
++uniquePasswords;
} else {
++reusedPasswords;
}

if (pwd.size() < 8) {
++shortPasswords;
}

// Speed up Zxcvbn process by excluding very long passwords and most passphrases
if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
++weakPasswords;
}

if (entry->excludeFromReports()) {
++excludedEntries;
}

totalPasswordLength += pwd.size();
m_passwords[pwd]++;
}
}
}
}
};
#endif // KEEPASSXC_DATABASESTATS_H
127 changes: 2 additions & 125 deletions src/gui/reports/ReportsWidgetStatistics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,137 +19,14 @@
#include "ui_ReportsWidgetStatistics.h"

#include "core/AsyncTask.h"
#include "core/DatabaseStats.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/PasswordHealth.h"
#include "gui/Icons.h"

#include <QFileInfo>
#include <QStandardItemModel>

namespace
{
class Stats
{
public:
// The statistics we collect:
QDateTime modified; // File modification time
int groupCount = 0; // Number of groups in the database
int entryCount = 0; // Number of entries (across all groups)
int expiredEntries = 0; // Number of expired entries
int excludedEntries = 0; // Number of known bad entries
int weakPasswords = 0; // Number of weak or poor passwords
int shortPasswords = 0; // Number of passwords 8 characters or less in size
int uniquePasswords = 0; // Number of unique passwords
int reusedPasswords = 0; // Number of non-unique passwords
int totalPasswordLength = 0; // Total length of all passwords

// Ctor does all the work
explicit Stats(QSharedPointer<Database> db)
: modified(QFileInfo(db->filePath()).lastModified())
, m_db(db)
{
gatherStats(db->rootGroup()->groupsRecursive(true));
}

// Get average password length
int averagePwdLength() const
{
const auto passwords = uniquePasswords + reusedPasswords;
return passwords == 0 ? 0 : std::round(totalPasswordLength / double(passwords));
}

// Get max number of password reuse (=how many entries
// share the same password)
int maxPwdReuse() const
{
int ret = 0;
for (const auto& count : m_passwords) {
ret = std::max(ret, count);
}
return ret;
}

// A warning sign is displayed if one of the
// following returns true.
bool isAnyExpired() const
{
return expiredEntries > 0;
}

bool areTooManyPwdsReused() const
{
return reusedPasswords > uniquePasswords / 10;
}

bool arePwdsReusedTooOften() const
{
return maxPwdReuse() > 3;
}

bool isAvgPwdTooShort() const
{
return averagePwdLength() < 10;
}

private:
QSharedPointer<Database> m_db;
QHash<QString, int> m_passwords;

void gatherStats(const QList<Group*>& groups)
{
auto checker = HealthChecker(m_db);

for (const auto* group : groups) {
// Don't count anything in the recycle bin
if (group->isRecycled()) {
continue;
}

++groupCount;

for (const auto* entry : group->entries()) {
// Don't count anything in the recycle bin
if (entry->isRecycled()) {
continue;
}

++entryCount;

if (entry->isExpired()) {
++expiredEntries;
}

// Get password statistics
const auto pwd = entry->password();
if (!pwd.isEmpty()) {
if (!m_passwords.contains(pwd)) {
++uniquePasswords;
} else {
++reusedPasswords;
}

if (pwd.size() < 8) {
++shortPasswords;
}

// Speed up Zxcvbn process by excluding very long passwords and most passphrases
if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
++weakPasswords;
}

if (entry->excludeFromReports()) {
++excludedEntries;
}

totalPasswordLength += pwd.size();
m_passwords[pwd]++;
}
}
}
}
};
} // namespace

ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent)
: QWidget(parent)
Expand Down Expand Up @@ -205,7 +82,7 @@ void ReportsWidgetStatistics::showEvent(QShowEvent* event)

void ReportsWidgetStatistics::calculateStats()
{
const QScopedPointer<Stats> stats(AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); }));
const QScopedPointer<DatabaseStats> stats(AsyncTask::runAndWaitForFuture([this] { return new DatabaseStats(m_db); }));

m_referencesModel->clear();
addStatsRow(tr("Database name"), m_db->metadata()->name());
Expand Down
16 changes: 16 additions & 0 deletions tests/TestCli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,22 @@ void TestCli::testInfo()
QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Database name: \n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
QVERIFY(m_stdout->readLine().contains(QByteArray("Location: /tmp/testcli"))); // name is randomly generated so just test the path
QVERIFY(m_stdout->readLine().contains(QByteArray("Database created: "))); // date changes often, so just test for the first part
QVERIFY(m_stdout->readLine().contains(QByteArray("Last saved: "))); // date changes often, so just test for the first part
QCOMPARE(m_stdout->readLine(), QByteArray("Unsaved changes: no\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Number of groups: 8\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Number of entries: 2\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Number of expired entries: 0\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Unique passwords: 2\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Non-unique passwords: 0\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Maximum password reuse: 1\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Number of short passwords: 0\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Number of weak passwords: 2\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Entries excluded from reports: 0\n"));
QCOMPARE(m_stdout->readLine(), QByteArray("Average password length: 11 characters\n"));

// Test with quiet option.
setInput("a");
Expand Down

0 comments on commit c03817e

Please sign in to comment.