Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ //
++ // qt_tara — TARA (Threat And Risk Assessment) launcher for Qt source files.
++ //
++ // Walks .cpp / .mm / .h files in the given directory (default: the current
++ // working directory), selects every file that carries the marker
++ //
++ // Qt-Security score:critical
++ //
++ // extracts the unique set of Q-prefixed class names declared in those files,
++ // and for every class spawns
++ //
++ // qt_fuzz_gen <ClassName> --qt-prefix <prefix>
++ //
++ // as a parallel background process. All children are started before any is
++ // waited on, so the work runs concurrently. The tool exits with the count
++ // of non-zero child exit codes (0 means all succeeded).
++ //
++ // Usage:
++ // qt_tara --qt-prefix <path> [<directory>]
++ //
++ // Options:
++ // --qt-prefix <path> Qt install prefix passed through to qt_fuzz_gen (required)
++ // <directory> Directory to scan (default: current working directory)
++ // -h / --help Print this help and exit
++
++ #include <filesystem>
++ #include <fstream>
++ #include <iostream>
++ #include <regex>
++ #include <set>
++ #include <sstream>
++ #include <string>
++ #include <vector>
++
++ #ifdef _WIN32
++ # define WIN32_LEAN_AND_MEAN
++ # define NOMINMAX
++ # include <windows.h>
++ #else
++ # include <sys/wait.h>
++ # include <unistd.h>
++ #endif
++
++ namespace fs = std::filesystem;
++
++ // ---------------------------------------------------------------------------
++ // Cross-platform background-process helpers
++ // ---------------------------------------------------------------------------
++
++ #ifdef _WIN32
++
++ using ProcessHandle = HANDLE;
++ static const ProcessHandle kInvalidHandle = INVALID_HANDLE_VALUE;
++
++ static ProcessHandle spawnBackground(const std::string &exe, const std::vector<std::string> &args)
++ {
++ // Build a quoted command line: "exe" "arg1" "arg2" ...
++ std::string cmd = "\"" + exe + "\"";
++ for (const auto &a : args)
++ cmd += " \"" + a + "\"";
++
++ STARTUPINFOA si = { };
++ si.cb = sizeof(si);
++ PROCESS_INFORMATION pi = { };
++
++ if (!CreateProcessA(nullptr, const_cast<char *>(cmd.c_str()), nullptr, nullptr, FALSE, 0,
++ nullptr, nullptr, &si, &pi)) {
++ std::cerr << "[qt_tara] CreateProcess failed (error " << GetLastError() << "): " << cmd
++ << "\n";
++ return kInvalidHandle;
++ }
++ CloseHandle(pi.hThread); // handle not needed
++ return pi.hProcess;
++ }
++
++ static int waitAll(std::vector<ProcessHandle> &handles)
++ {
++ int failed = 0;
++ for (auto h : handles) {
++ if (h == kInvalidHandle) {
++ ++failed;
++ continue;
++ }
++ WaitForSingleObject(h, INFINITE);
++ DWORD code = 0;
++ if (GetExitCodeProcess(h, &code) && code != 0)
++ ++failed;
++ CloseHandle(h);
++ }
++ return failed;
++ }
++
++ #else // POSIX
++
++ using ProcessHandle = pid_t;
++ static const ProcessHandle kInvalidHandle = -1;
++
++ static ProcessHandle spawnBackground(const std::string &exe, const std::vector<std::string> &args)
++ {
++ const pid_t pid = fork();
++ if (pid < 0) {
++ std::cerr << "[qt_tara] fork() failed: " << exe << "\n";
++ return kInvalidHandle;
++ }
++ if (pid == 0) {
++ std::vector<const char *> argv;
++ argv.push_back(exe.c_str());
++ for (const auto &a : args)
++ argv.push_back(a.c_str());
++ argv.push_back(nullptr);
++ execv(exe.c_str(), const_cast<char **>(argv.data()));
++ _exit(127);
++ }
++ return pid;
++ }
++
++ static int waitAll(std::vector<ProcessHandle> &handles)
++ {
++ int failed = 0;
++ for (auto pid : handles) {
++ if (pid == kInvalidHandle) {
++ ++failed;
++ continue;
++ }
++ int wstatus = 0;
++ waitpid(pid, &wstatus, 0);
++ if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0)
++ ++failed;
++ }
++ return failed;
++ }
++
++ #endif
++
++ // ---------------------------------------------------------------------------
++ // File / class-name helpers
++ // ---------------------------------------------------------------------------
++
++ static std::string readFile(const fs::path &p)
++ {
++ std::ifstream f(p);
++ if (!f)
++ return { };
++ std::ostringstream ss;
++ ss << f.rdbuf();
++ return ss.str();
++ }
++
++ static bool hasCriticalMarker(const std::string &src)
++ {
++ return src.find("Qt-Security score:critical") != std::string::npos;
++ }
++
++ // Collect Q-prefixed class names: matches
++ // class [Q_SOMETHING_EXPORT] QFoo
++ // Requires QFoo to start with Q followed by an upper-case letter so that
++ // template parameters and misc. identifiers are excluded.
++ static void collectClassNames(const std::string &src, std::set<std::string> &out)
++ {
++ // Group 1: export macro (e.g. Q_CORE_EXPORT) — only exported classes are in scope.
++ // Group 2: class name
++ static const std::regex kRe(R"(\bclass\s+(Q_\w+_EXPORT)\s+(Q[A-Z]\w+)\b)",
++ std::regex::optimize);
++
++ for (auto it = std::sregex_iterator(src.cbegin(), src.cend(), kRe);
++ it != std::sregex_iterator(); ++it) {
++ const std::string name = (*it)[2].str();
++ // Skip macro-like tokens (e.g. QT_DEPRECATED_VERSION_X_6_15 appearing
++ // as a deprecation annotation between the export macro and the real name).
++ if (name.find('_') != std::string::npos)
++ continue;
++ out.insert(name);
++ }
++ }
++
++ // A private header is in scope for --qml when it registers a QML type
++ // (QML_ELEMENT / QML_NAMED_ELEMENT) that exposes properties.
++ static bool hasQmlType(const std::string &src)
++ {
++ const bool hasRegistration = src.find("QML_ELEMENT") != std::string::npos
++ || src.find("QML_NAMED_ELEMENT") != std::string::npos;
++ return hasRegistration && src.find("Q_PROPERTY") != std::string::npos;
++ }
++
++ // For _p.h files with QML_ELEMENT / QML_NAMED_ELEMENT: class names need not be
++ // Q-prefixed or exported (e.g. SphereGeometry, StateMachine, ColorGradient).
++ static void collectQmlClassNames(const std::string &src, std::set<std::string> &out)
++ {
++ static const std::regex kRe(R"(\bclass\s+(?:Q_\w+_EXPORT\s+)?([A-Z][A-Za-z0-9]+)\b)",
++ std::regex::optimize);
++
++ for (auto it = std::sregex_iterator(src.cbegin(), src.cend(), kRe);
++ it != std::sregex_iterator(); ++it) {
++ const std::string name = (*it)[1].str();
++ if (name.find('_') != std::string::npos)
++ continue;
++ out.insert(name);
++ }
++ }
++
++ // ---------------------------------------------------------------------------
++ // Locate qt_fuzz_gen next to our own binary, fall back to PATH
++ // ---------------------------------------------------------------------------
++
++ static std::string findFuzzGen(const char *argv0)
++ {
++ #ifdef _WIN32
++ static const char kName[] = "qt_fuzz_gen.exe";
++ #else
++ static const char kName[] = "qt_fuzz_gen";
++ #endif
++
++ std::error_code ec;
++ fs::path sibling = fs::absolute(fs::path(argv0), ec).parent_path() / kName;
++ if (!ec && fs::exists(sibling))
++ return sibling.string();
++ return kName; // rely on PATH
++ }
++
++ // ---------------------------------------------------------------------------
++ // main
++ // ---------------------------------------------------------------------------
++
++ int main(int argc, char *argv[])
++ {
++ std::string qtPrefix;
++ std::string scanDirArg;
++ bool listOnly = false;
++ bool includeQml = false;
++
++ for (int i = 1; i < argc; ++i) {
++ const std::string a = argv[i];
++ if (a == "--qt-prefix" && i + 1 < argc) {
++ qtPrefix = argv[++i];
++ } else if (a.rfind("--qt-prefix=", 0) == 0) {
++ qtPrefix = a.substr(12);
++ } else if (a == "--list-classes") {
++ listOnly = true;
++ } else if (a == "--qml") {
++ includeQml = true;
++ } else if (a == "-h" || a == "--help") {
++ std::cout
++ << "Usage: qt_tara --qt-prefix <path> [<directory>]\n\n"
++ << " Scans <directory> (default: current directory) for .cpp / .mm / .h\n"
++ << " files that carry the 'Qt-Security score:critical' marker, extracts\n"
++ << " all Q-prefixed class names, and spawns\n\n"
++ << " qt_fuzz_gen <ClassName> --qt-prefix <path>\n\n"
++ << " in parallel for every unique class found.\n\n"
++ << "Options:\n"
++ << " --qt-prefix <path> Qt install prefix (required unless --list-classes)\n"
++ << " --list-classes Print in-scope class names and exit without "
++ "running qt_fuzz_gen\n"
++ << " --qml Pass --qml to qt_fuzz_gen (include QML-exposed "
++ "private classes)\n"
++ << " <directory> Directory to scan (default: .)\n";
++ return 0;
++ } else if (a.rfind("--", 0) != 0) {
++ scanDirArg = a;
++ } else {
++ std::cerr << "[qt_tara] Unknown option: " << a << " (try --help)\n";
++ return 1;
++ }
++ }
++
++ if (qtPrefix.empty() && !listOnly) {
++ std::cerr << "[qt_tara] Error: --qt-prefix <path> is required.\n";
++ return 1;
++ }
++
++ const fs::path root = scanDirArg.empty() ? fs::current_path() : fs::path(scanDirArg);
++ if (!fs::is_directory(root)) {
++ std::cerr << "[qt_tara] Not a directory: " << root.string() << "\n";
++ return 1;
++ }
++
++ // ── 1. Read all source files, collect security-critical class names ──────────
++
++ std::set<std::string> classes;
++ std::error_code ec;
++ for (const auto &entry : fs::recursive_directory_iterator(root, ec)) {
++ if (ec) {
++ ec.clear();
++ continue;
++ }
++ if (!entry.is_regular_file())
++ continue;
++
++ const auto &ext = entry.path().extension();
++ if (ext != ".cpp" && ext != ".mm" && ext != ".h")
++ continue;
++
++ const std::string src = readFile(entry.path());
++ if (!hasCriticalMarker(src))
++ continue;
++
++ const std::string stem = entry.path().stem().string();
++ std::cout << "[qt_tara] critical: " << entry.path().filename().string() << "\n";
++
++ // Private headers (*_p.h) contain implementation classes, not public API —
++ // unless --qml is active and the file exposes a QML type + Q_PROPERTY.
++ if (stem.size() >= 2 && stem[stem.size() - 2] == '_' && stem[stem.size() - 1] == 'p') {
++ if (!includeQml || !hasQmlType(src))
++ continue;
++ collectQmlClassNames(src, classes);
++ continue;
++ }
++
++ collectClassNames(src, classes);
++
++ // A critical .cpp / .mm usually only contains definitions; the class
++ // declaration (with the export macro) lives in the same-stem .h.
++ // Scan that header too so classes like QImage are not missed.
++ if (ext == ".cpp" || ext == ".mm") {
++ const fs::path hFile = entry.path().parent_path() / (stem + ".h");
++ if (fs::exists(hFile))
++ collectClassNames(readFile(hFile), classes);
++
++ // With --qml, QML types may be declared in the same-stem private
++ // header instead, without the header carrying the marker itself.
++ if (includeQml) {
++ const fs::path pFile = entry.path().parent_path() / (stem + "_p.h");
++ if (fs::exists(pFile)) {
++ const std::string pSrc = readFile(pFile);
++ if (hasQmlType(pSrc))
++ collectQmlClassNames(pSrc, classes);
++ }
++ }
++ }
++ }
++
++ if (classes.empty()) {
++ std::cout << "[qt_tara] No classes found in Qt-Security score:critical files.\n";
++ return 0;
++ }
++
++ if (listOnly) {
++ std::cout << "[qt_tara] " << classes.size() << " class(es) in scope:\n";
++ for (const auto &cls : classes)
++ std::cout << " " << cls << "\n";
++ return 0;
++ }
++
++ std::cout << "[qt_tara] Spawning qt_fuzz_gen for " << classes.size()
++ << " class(es) in parallel:\n";
++
++ // ── 2. Spawn one qt_fuzz_gen process per class ───────────────────────────
++
++ const std::string fuzzGen = findFuzzGen(argv[0]);
++ std::vector<ProcessHandle> handles;
++ handles.reserve(classes.size());
++
++ for (const auto &cls : classes) {
++ std::cout << "[qt_tara] → " << cls << "\n";
++ std::vector<std::string> fgArgs = { cls, "--qt-prefix", qtPrefix };
++ if (includeQml)
++ fgArgs.push_back("--qml");
++ handles.push_back(spawnBackground(fuzzGen, fgArgs));
++ }
++
++ // ── 3. Wait for all children ─────────────────────────────────────────────
++
++ const int failed = waitAll(handles);
++ if (failed > 0)
++ std::cerr << "[qt_tara] " << failed << " process(es) exited with errors.\n";
++ else
++ std::cout << "[qt_tara] All done.\n";
++
++ return failed > 0 ? 1 : 0;
++ }