• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

squinky86 / STIGQter / 35633460098

21 Sep 2026 05:39PM UTC coverage: 75.921%. First build
35633460098

push

github

squinky86
Make Sonar coverage tests deterministic

64 of 69 new or added lines in 2 files covered. (92.75%)

10266 of 13522 relevant lines covered (75.92%)

578277.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.31
/src/common.cpp
1
/*
2
 * STIGQter - STIG fun with Qt
3
 *
4
 * Copyright © 2018–2023 Jon Hood, http://www.hoodsecurity.com/
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
 */
19

20
#include "common.h"
21
#include "dbmanager.h"
22

23
bool IgnoreWarnings = false;
24

25
#include <zip.h>
26

27
#include <QApplication>
28
#include <QDebug>
29
#include <QEventLoop>
30
#include <QFileInfo>
31
#include <QtGlobal>
32
#include <QMessageBox>
33
#include <QRegularExpression>
34
#include <QString>
35
#include <QtNetwork>
36

37
/**
38
 * @brief MessageHandler
39
 * @param type
40
 * @param context
41
 * @param msg
42
 *
43
 * Logging of Qt messages occurs here. Set the LogLevel to 6 for full
44
 * debugging. Default is LogLevel 5.
45
 */
46
void MessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
5✔
47
{
48
    int severity = 0;
5✔
49
    switch (type)
5✔
50
    {
51
    case QtDebugMsg:
1✔
52
        severity = 5;
1✔
53
        break;
1✔
54
    case QtInfoMsg:
1✔
55
        severity = 4;
1✔
56
        break;
1✔
57
    case QtWarningMsg:
1✔
58
        severity = 3;
1✔
59
        break;
1✔
60
    case QtCriticalMsg:
1✔
61
        severity = 2;
1✔
62
        break;
1✔
63
    case QtFatalMsg:
1✔
64
        severity = 1;
1✔
65
        break;
1✔
66
    //should not be any other types; "default" is for scenarios where new types are added later.
67
    //[[unlikely]] - uncomment when moving to >=Qt 5.14 and C++20
68
    default:
×
69
        severity = 0;
×
70
        break;
×
71
    }
72
    DbManager db;
5✔
73
    db.Log(severity, context.file + QStringLiteral(":") + QString::number(context.line) + QStringLiteral(" ") + context.function, msg);
15✔
74
}
5✔
75

76
/**
77
 * @brief DownloadFile
78
 * @param url
79
 * @param file
80
 * @return @c True when the file is successfully downloaded.
81
 * Otherwise, @c false.
82
 *
83
 * Given a @a url, the contents of that URL are written to the handle
84
 * supplied in the @a file parameter.
85
 */
86
bool DownloadFile(const QUrl &url, QFile *file)
1✔
87
{
88
    bool close = false;
1✔
89

90
    //check if the file is currently open
91
    if (!file->isOpen())
1✔
92
    {
93
        file->open(QIODevice::WriteOnly);
×
94
        if (!file->isOpen())
×
95
            return false;
×
96
        close = true;
×
97
    }
98

99
    // Local URLs are useful for offline imports and deterministic tests. They
100
    // have no host name, so the network-only path below would reject them.
101
    if (url.isLocalFile())
1✔
102
    {
103
        QFile source(url.toLocalFile());
1✔
104
        if (!source.open(QIODevice::ReadOnly))
1✔
105
        {
NEW
106
            if (close)
×
NEW
107
                file->close();
×
NEW
108
            return false;
×
109
        }
110

111
        const QByteArray contents = source.readAll();
1✔
112
        const bool written = file->write(contents) == contents.size();
1✔
113
        file->flush();
1✔
114
        if (close)
1✔
NEW
115
            file->close();
×
116
        else
117
            file->seek(0);
1✔
118
        return written;
1✔
119
    }
1✔
120

121
    QNetworkAccessManager manager;
×
122
    QNetworkRequest req = QNetworkRequest(url);
×
123
    req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
×
124

125
    //set the User-Agent so that this program appears in logs correctly
126
    QString userAgent = GetUserAgent();
×
127
    req.setRawHeader("User-Agent", userAgent.toStdString().c_str());
×
128

129
    auto addresses = QHostInfo::fromName(url.host()).addresses();
×
130
    if (!addresses.isEmpty())
×
131
    {
132
        //log HTTP headers, STIG Rule SV-83997r1_rule
133
        //log IP address, STIG Rule SV-84045r1_rule
134
        Warning(QStringLiteral("Downloading File"), "Downloading " + url.toString() + " (ip address " + addresses.first().toString() + ") with header User-Agent: " + userAgent, true);
×
135

136
        //clean up the socket event when the response is finished reading
137
        QNetworkReply *response = manager.get(req);
×
138
        QEventLoop event;
×
139
        QObject::connect(response,SIGNAL(finished()),&event,SLOT(quit()));
×
140
        event.exec();
×
141

142
        //read entire contents to memory before saving it to the file
143
        QByteArray tmpArray = response->readAll();
×
144

145
        //save contents of the response to the file
146
        file->write(tmpArray, tmpArray.size());
×
147
        file->flush();
×
148
        delete response;
×
149

150
        /*
151
         * If the file was already open, seek back to the beginning of
152
         * the file. Otherwise, close it. This preserves the state of the
153
         * file before this function ran.
154
         */
155
        if (close)
×
156
            file->close();
×
157
        else
158
            file->seek(0);
×
159

160
        return true;
×
161
    }
×
162

163
    if (close)
×
164
        file->close();
×
165
    else
166
        file->seek(0);
×
167

168
    return false;
×
169
}
×
170

171
/**
172
 * @brief DownloadPage
173
 * @param url
174
 * @return A string of the downloaded page.
175
 *
176
 * Downloads the text from the requested @a url.
177
 */
178
QString DownloadPage(const QUrl &url)
1✔
179
{
180
    QNetworkAccessManager manager;
1✔
181
    QNetworkRequest req = QNetworkRequest(url);
1✔
182
    req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
1✔
183

184
    //set the User-Agent so that this program appears in logs correctly
185
    QString userAgent = GetUserAgent();
1✔
186
    req.setRawHeader("User-Agent", userAgent.toStdString().c_str());
1✔
187

188
    if (auto addresses = QHostInfo::fromName(url.host()).addresses(); !addresses.isEmpty())
1✔
189
    {
190
        //log HTTP headers, STIG Rule SV-222447r508029_rule
191
        //log IP address, STIG Rule SV-222448r508029_rule
192
        Warning(QStringLiteral("Downloading Page"), "Downloading " + url.toString() + " (ip address " + addresses.first().toString() + ") with header User-Agent: " + userAgent, true);
2✔
193

194
        //send request and get response
195
        QNetworkReply *response = manager.get(req);
1✔
196

197
        //clean up the socket event when the response is finished reading
198
        QEventLoop event;
1✔
199
        QObject::connect(response,SIGNAL(finished()),&event,SLOT(quit()));
1✔
200
        event.exec();
1✔
201

202
        //read the response and return its contents
203
        QString html = QString::fromLatin1(response->readAll());
1✔
204
        delete response;
1✔
205
        return html;
1✔
206
    }
2✔
207
    return QString(); // unable to download
×
208
}
1✔
209

210
/**
211
 * @brief GetCCINumber
212
 * @param cci
213
 * @return The numeric value of the CCI.
214
 *
215
 * Converts a string "CCI-######" to its integral format.
216
 */
217
int GetCCINumber(QString cci)
1,960✔
218
{
219
    cci = cci.trimmed();
1,960✔
220
    if (cci.startsWith(QStringLiteral("CCI-")))
1,960✔
221
        cci = cci.right(cci.length() - 4);
1,960✔
222
    return cci.toInt();
1,960✔
223
}
224

225
/**
226
 * @brief GetFilesFromZip
227
 * @param fileName
228
 * @param fileNameFilter
229
 * @return A map of the extracted files in the zip.
230
 *
231
 * Extracts a zip file and stores the contents in memory.
232
 *
233
 * When fileNameFilter is set, only the files that end with the
234
 * provided filter are extracted and returned (case-insensitive).
235
 */
236
QMap<QString, QByteArray> GetFilesFromZip(const QString &fileName, const QString &fileNameFilter)
10✔
237
{
238
    //map to return
239
    QMap<QString, QByteArray> ret;
10✔
240

241
    //open the zip with libzip
242
    struct zip *za;
243
    int err;
244
    struct zip_stat sb;
245
    zip_stat_init(&sb); //initializes sb
10✔
246
    za = zip_open(fileName.toStdString().c_str(), 0, &err);
10✔
247
    if (za != nullptr)
10✔
248
    {
249
        //cycle through each zip file entry
250
        for (unsigned int i = 0; i < zip_get_num_entries(za, 0); i++)
72✔
251
        {
252
            if (zip_stat_index(za, i, 0, &sb) == 0)
62✔
253
            {
254
                //zip bomb protection
255
                //if file is > 4GB extracted, do not read it
256
                if (sb.size > 4294967295)
62✔
257
                    continue;
×
258

259
                QString name(QString::fromLatin1(sb.name));
62✔
260
                if (!fileNameFilter.isNull() && !fileNameFilter.isEmpty() && !name.endsWith(fileNameFilter, Qt::CaseInsensitive))
62✔
261
                {
262
                    continue;
×
263
                }
264

265
                QByteArray todo;
62✔
266
                struct zip_file *zf = zip_fopen_index(za, i, 0);
62✔
267
                if (zf)
62✔
268
                {
269
                    unsigned int sum = 0;
62✔
270
                    while (sum < sb.size)
16,395✔
271
                    {
272
                        char buf[1024];
273
                        zip_int64_t len = zip_fread(zf, static_cast<void*>(buf), 1024);
16,333✔
274
                        if (len > 0)
16,333✔
275
                        {
276
                            todo.append(static_cast<const char*>(buf), static_cast<int>(len));
16,333✔
277
                            sum += len;
16,333✔
278
                        }
279
                    }
280
                    zip_fclose(zf);
62✔
281
                }
282
                ret.insert(name, todo);
62✔
283
            }
62✔
284
        }
285
        zip_close(za);
10✔
286
    }
287
    return ret;
10✔
288
}
289

290
/**
291
 * @brief CreateZip
292
 * @param fileName
293
 * @param files
294
 * @return @c true when the archive was written successfully.
295
 *
296
 * Writes an in-memory set of files (keyed by their path inside the
297
 * archive) to a new zip archive at @a fileName using libzip. Any
298
 * existing archive at that path is truncated first.
299
 *
300
 * The @a files map is passed by const reference and must outlive this
301
 * call: zip_source_buffer() does not copy the supplied bytes, so the
302
 * QByteArrays are kept alive until zip_close() has flushed them.
303
 */
304
bool CreateZip(const QString &fileName, const QMap<QString, QByteArray> &files)
4✔
305
{
306
    int err = 0;
4✔
307
    struct zip *za = zip_open(fileName.toStdString().c_str(), ZIP_CREATE | ZIP_TRUNCATE, &err);
4✔
308
    if (za == nullptr)
4✔
309
        return false;
×
310

311
    bool ret = true;
4✔
312
    for (auto i = files.constBegin(); i != files.constEnd(); ++i)
18✔
313
    {
314
        const QByteArray &contents = i.value();
14✔
315
        //the buffer is not copied by libzip; the const-ref map keeps it alive until zip_close()
316
        struct zip_source *zs = zip_source_buffer(za, contents.constData(), static_cast<zip_uint64_t>(contents.size()), 0);
14✔
317
        if (zs == nullptr)
14✔
318
        {
319
            ret = false;
×
320
            continue;
×
321
        }
322
        if (zip_file_add(za, i.key().toStdString().c_str(), zs, ZIP_FL_ENC_UTF_8 | ZIP_FL_OVERWRITE) < 0)
14✔
323
        {
324
            zip_source_free(zs);
×
325
            ret = false;
×
326
        }
327
    }
328

329
    if (zip_close(za) < 0)
4✔
330
    {
331
        zip_discard(za);
×
332
        ret = false;
×
333
    }
334

335
    return ret;
4✔
336
}
337

338
/**
339
 * @brief GetClassification
340
 * @param marking
341
 * @return The @a Classification level parsed from a free-text marking.
342
 *
343
 * Markings are free text (e.g. "CUI [Controlled by: …]" or
344
 * "TOP SECRET//SI"). A level is only recognized when the marking contains
345
 * the complete classification word (matched case-insensitively on word
346
 * boundaries) — so an incidental marking like "Storage array" does not
347
 * escalate to SECRET. The highest classification word present wins
348
 * ("TOP SECRET" is checked before "SECRET"), and "CONTROLLED" is treated
349
 * as "CUI". Markings with no recognized word are the lowest level,
350
 * @c classPublicRelease.
351
 */
352
Classification GetClassification(const QString &marking)
285✔
353
{
354
    const QString m = marking.toUpper();
285✔
355
    static const QRegularExpression reTopSecret(QStringLiteral("\\bTOP SECRET\\b"));
286✔
356
    static const QRegularExpression reSecret(QStringLiteral("\\bSECRET\\b"));
286✔
357
    static const QRegularExpression reConfidential(QStringLiteral("\\bCONFIDENTIAL\\b"));
286✔
358
    static const QRegularExpression reCUI(QStringLiteral("\\b(CUI|CONTROLLED)\\b"));
286✔
359
    static const QRegularExpression reFOUO(QStringLiteral("\\bFOUO\\b"));
286✔
360
    static const QRegularExpression reUnclassified(QStringLiteral("\\bUNCLASSIFIED\\b"));
286✔
361

362
    if (m.contains(reTopSecret))
285✔
363
        return Classification::classTopSecret;
×
364
    if (m.contains(reSecret))
285✔
365
        return Classification::classSecret;
×
366
    if (m.contains(reConfidential))
285✔
367
        return Classification::classConfidential;
×
368
    if (m.contains(reCUI))
285✔
369
        return Classification::classCUI;
178✔
370
    if (m.contains(reFOUO))
107✔
371
        return Classification::classFOUO;
×
372
    if (m.contains(reUnclassified))
107✔
373
        return Classification::classUnclassified;
15✔
374
    return Classification::classPublicRelease;
92✔
375
}
285✔
376

377
/**
378
 * @brief GetClassificationString
379
 * @param classification
380
 * @return The canonical, human-readable label for a @a Classification.
381
 */
382
QString GetClassificationString(Classification classification)
5✔
383
{
384
    switch (classification)
5✔
385
    {
386
    case Classification::classUnclassified:
1✔
387
        return QStringLiteral("UNCLASSIFIED");
1✔
388
    case Classification::classFOUO:
×
389
        return QStringLiteral("FOUO");
×
390
    case Classification::classCUI:
1✔
391
        return QStringLiteral("CUI");
1✔
392
    case Classification::classConfidential:
×
393
        return QStringLiteral("CONFIDENTIAL");
×
394
    case Classification::classSecret:
×
395
        return QStringLiteral("SECRET");
×
396
    case Classification::classTopSecret:
×
397
        return QStringLiteral("TOP SECRET");
×
398
    default:
3✔
399
        return QStringLiteral("PUBLIC RELEASE");
3✔
400
    }
401
}
402

403
/**
404
 * @brief GetClassificationColor
405
 * @param classification
406
 * @return The banner color for a @a Classification as a 0xRRGGBB value,
407
 * usable both by Qt (QColor/QRgb) and by libxlsxwriter (lxw_color_t).
408
 *
409
 * Colors follow the standard DoD classification banner palette.
410
 */
411
quint32 GetClassificationColor(Classification classification)
93✔
412
{
413
    switch (classification)
93✔
414
    {
415
    case Classification::classCUI:
87✔
416
        return 0x502B85; //purple
87✔
417
    case Classification::classConfidential:
×
418
        return 0x0033A0; //blue
×
419
    case Classification::classSecret:
×
420
        return 0xC8102E; //red
×
421
    case Classification::classTopSecret:
×
422
        return 0xFF8C00; //orange
×
423
    case Classification::classPublicRelease:
6✔
424
    case Classification::classUnclassified:
425
    case Classification::classFOUO:
426
    default:
427
        return 0x007A33; //green
6✔
428
    }
429
}
430

431
/**
432
 * @brief GetReleaseNumber
433
 * @param release
434
 * @return The release number from a STIG release string
435
 */
436
int GetReleaseNumber(const QString &release)
599✔
437
{
438
    static const QRegularExpression releasePattern(
439
        QStringLiteral("(?:^|\\b)(?:release\\s*:?|r)\\s*(\\d+)\\b"),
2✔
440
        QRegularExpression::CaseInsensitiveOption);
601✔
441
    const QRegularExpressionMatch match = releasePattern.match(release);
599✔
442
    if (!match.hasMatch())
599✔
443
        return -1;
×
444

445
    bool ok = false;
599✔
446
    const int releaseNumber = match.captured(1).toInt(&ok);
599✔
447
    return ok ? releaseNumber : -1;
599✔
448
}
599✔
449

450
/**
451
 * @brief GetUserAgent
452
 * @return The User-Agent to use when making web requests.
453
 */
454
QString GetUserAgent()
1✔
455
{
456
    return QString(QStringLiteral("STIGQter/")) + VERSION;
2✔
457
}
458

459
/**
460
 * @brief Excelify
461
 * @param s
462
 * @return The string @a s formatted in a way that Excel can
463
 * understand.
464
 */
465
QString Excelify(const QString &s)
82,404✔
466
{
467
    //Excel is limited to 32,767 characters per-cell
468
    QString ret = s.left(32767);
82,404✔
469
    //STIG Rule SV-222447r508029_rule - Prevent CSV Injection
470
    if (ret.startsWith('=') || ret.startsWith('+') || ret.startsWith('-') || ret.startsWith('@'))
82,404✔
471
    {
472
        ret.prepend('\'');
×
473
    }
474
    return ret;
82,404✔
475
}
476

477
/**
478
 * @brief Pluralize
479
 * @param count
480
 * @param plural
481
 * @param singular
482
 * @return @c @a plural when @a count indicates that plural usage is
483
 * appropriate. Otherwise, @c @a singular.
484
 */
485
QString Pluralize(const int count, const QString &plural, const QString &singular)
9✔
486
{
487
    return (count == 1) ? singular : plural;
9✔
488
}
489

490
/**
491
 * @brief PrintTrueFalse
492
 * @param tf
493
 * @return human-readable boolean.
494
 */
495
QString PrintTrueFalse(bool tf)
859✔
496
{
497
    return tf ? QStringLiteral("true") : QStringLiteral("false");
2,577✔
498
}
499

500
QString Sanitize(QString s)
40✔
501
{
502
    s = s.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
80✔
503
    s = s.replace(QStringLiteral("\n"), QStringLiteral(" "));
80✔
504
    return s;
40✔
505
}
506

507
QString SanitizeFile(QString s)
9✔
508
{
509
    //UTF replacements have trouble with QStrings
510
    s = s.replace(QStringLiteral("/"), QStringLiteral(" ̸"));
18✔
511
    s = s.replace(QStringLiteral("\\"), QStringLiteral("-"));
18✔
512
    s = s.replace(QStringLiteral("?"), QStringLiteral("-"));
18✔
513
    s = s.replace(QStringLiteral("*"), QStringLiteral("-"));
18✔
514
    s = s.replace(QStringLiteral("\""), QStringLiteral("-"));
18✔
515
    s = s.replace(QStringLiteral("<"), QStringLiteral("-"));
18✔
516
    s = s.replace(QStringLiteral(">"), QStringLiteral("-"));
18✔
517
    s = s.replace(QStringLiteral("|"), QStringLiteral("-"));
18✔
518
    s = s.replace(QStringLiteral(":"), QStringLiteral("-"));
18✔
519
    s = s.replace(QStringLiteral("#"), QStringLiteral("-"));
18✔
520
    s = s.replace(QStringLiteral("%"), QStringLiteral("-"));
18✔
521
    s = s.replace(QStringLiteral("$"), QStringLiteral("-"));
18✔
522
    s = s.replace(QStringLiteral("!"), QStringLiteral("-"));
18✔
523
    s = s.replace(QStringLiteral("{"), QStringLiteral("-"));
18✔
524
    s = s.replace(QStringLiteral("}"), QStringLiteral("-"));
18✔
525
    s = s.replace(QStringLiteral("@"), QStringLiteral("-"));
18✔
526
    return s;
9✔
527
}
528

529
/**
530
 * @brief TrimFileName
531
 * @param fileName
532
 * @return The fileName without any leading directory structure.
533
 */
534
QString TrimFileName(const QString &fileName)
10✔
535
{
536
    QFileInfo fi(fileName);
10✔
537
    return fi.fileName();
10✔
538
}
10✔
539

540
/**
541
 * @brief Warning
542
 * @param title
543
 * @param message
544
 * @param quiet
545
 * @param level
546
 *
547
 * When @a quiet is not @c true, displays a warning box with the
548
 * provided @a title and @a message. The title and message are always
549
 * printed on the console/debug log.
550
 */
551
void Warning(const QString &title, const QString &message, const bool quiet, const int level)
405✔
552
{
553
    DbManager db;
405✔
554
    db.Log(level, QString(), title + ": " + message);
405✔
555
    if (!IgnoreWarnings && !quiet && (QThread::currentThread() == QApplication::instance()->thread())) //make sure we're in the GUI thread before popping a message box
405✔
556
    {
557
        int ret = QMessageBox::warning(nullptr, title, message, QMessageBox::Ignore | QMessageBox::Ok);
×
558
        //if ignoring messages, move to quiet mode
559
        if (ret == QMessageBox::Ignore)
×
560
        {
561
            IgnoreWarnings = true;
×
562
        }
563
    }
564
}
405✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc