def bySlug(slug, path): session = db.getSession() ret = session.query(TargetConfig).filter( TargetConfig.target_slug == slug).first() if ret is None: ret = TargetConfig(slug, path) session.add(ret) session.commit() return ret
def export(self, export_cb=None): crashes = self.get_crashes() csvPath = os.path.join(self.exportDir, "triage.csv") with open(csvPath, "w") as f: csvWriter = csv.writer(f, lineterminator="\n") checksec_no_gs = self.checksec_cols.copy() checksec_no_gs[checksec_no_gs.index("gs")] = "guardStack" csvWriter.writerow(checksec_no_gs + self.crash_cols + ["tracer.formatted"]) session = db.getSession() target = session.query(TargetConfig).filter( TargetConfig.target_slug == self.slug).first() if not target: print("[!] Could not retrieve target config!") checksec = session.query(Checksec).filter( Checksec.hash == target.hash).first() if not checksec: print("[!} Could not retrieve Checksec results!") for crash in crashes: tracer = session.query(Tracer).filter( Tracer.runid == crash.runid).first() if not tracer: print("[!] Could not retrive Tracer results!") row = [] for col in self.checksec_cols: row.append(getattr(checksec, col, None)) for col in self.crash_cols: row.append(getattr(crash, col, None)) row.append(tracer.formatted) csvWriter.writerow(row) for idx, crash in enumerate(crashes): try: if export_cb: export_cb(idx) dstdir = os.path.join( self.exportDir, sanitizeString(crash.exploitability), sanitizeString(crash.crashReason), sanitizeString(crash.crashash), crash.runid, ) # os.makedirs( dstdir, exist_ok=True ) srcdir = os.path.dirname(crash.minidumpPath) print("%s -> %s" % (srcdir, dstdir)) shutil.copytree(srcdir, dstdir, ignore=ignore_patterns("mem*.dmp")) except FileExistsError as x: print("File already exists for crash ", crash.minidumpPath, x)
def factory(runid, formatted=None, raw=None): runid = str(runid) session = db.getSession() ret = session.query(Tracer).filter(Tracer.runid == runid).first() if ret: return ret if not formatted and not raw: print("Could not find tracer for runid ", runid) return None ret = Tracer(runid, formatted, raw) session.add(ret) session.commit()
def _handle_completion(self): session = db.getSession() record = RunBlock( self.target_slug, self.started, self.runs_counted, self.crash_counter, self.run_dict["bkt"], self.run_dict["scr"], self.run_dict["rem"], ) session.add(record) session.commit()
def incrementPath(pathhash, target_slug): session = db.getSession() session.expire_on_commit = False ret = session.query(PathRecord).filter( PathRecord.hash == pathhash).first() if ret: ret.count = ret.count + 1 ret.last_seen = datetime.datetime.utcnow() else: ret = PathRecord(pathhash, target_slug) session.add(ret) session.commit() session.close()
def factory(key, value=None): session = db.getSession() ret = session.query(Conf).filter(Conf.key == key).first() if ret: return ret if not value: return None conf = Conf(key, value) session.add(conf) session.commit() return conf
def factory(runid, slug=None, targetPath=None): cfg = config session = db.getSession() runid = str(runid) ret = session.query(Crash).filter(Crash.runid == runid).first() if ret: return ret dmpPath = db.utilz.runidToDumpPath(runid) if not dmpPath: print("Unable to find dumpfile for runid %s" % runid) return None # Runs triager, which will give us exploitability info # using 2 engines: Google's breakpad and an reimplementation of Microsofts # !exploitable cmd = [cfg.config["triager_path"], dmpPath] out = subprocess.check_output(cmd, shell=False) # TODO: sorry didn't have time to clean this up before leave dirname = os.path.dirname(dmpPath) path = os.path.join(dirname, "triage.txt") with open(path, "wb", newline=None) as f: f.write(out) out = out.decode("utf8") j = None for line in out.splitlines(): line = line.strip() if re.match(r"{.*}", line): j = json.loads(line) j["output"] = out if not j: return None try: ret = Crash(j, slug, runid, targetPath) except: # noqa: E722 print("Unable to process crash json") return None ret.mergeTracer() ret.reconstructor() session.add(ret) session.commit() return ret
def plot_run_rate(target_slug): session = db.getSession() target_runs = session.query(RunBlock).filter(RunBlock.target_config_slug == target_slug).all() rates = [] for block in target_runs: elapsed = block.ended - block.started rate = block.runs / elapsed.total_seconds() rates.append(rate) stddev = statistics.stdev(rates) mean = statistics.mean(rates) no_outliers = list(filter(lambda x: abs(x[0] - mean) < 4 * stddev, zip(rates, target_runs))) plt.plot(get_fuzzing_time(k[1] for k in no_outliers), [k[0] for k in no_outliers], marker="o") plt.xlabel("Seconds spent fuzzing") plt.ylabel("Runs/Second (single threaded)") return plt
def update(self): # create a query for the current target session = getSession() basequery = session.query(Crash).filter( Crash.target_config_slug == self.target_slug) # Set default values self.crashes = basequery.all() self.crashesCnt = len(self.crashes) self.exploitabilityCnts = { "High": 0, "Medium": 0, "Low": 0, "Unknown": 0, "None": 0 } self.uniquesCnt = 0 self.dupesCount = 0 self.ranksMean = 0 self.ranksMedian = 0 # if there are crashes, update the rest of the counters if self.crashesCnt > 0: self.uniquesCnt = basequery.distinct(Crash.crashash).group_by( Crash.crashash).count() self.dupesCount = self.crashesCnt - self.uniquesCnt self.ranks = [_.rank for _ in self.crashes] self.ranksMean = statistics.mean(self.ranks) self.ranksMedian = statistics.median(self.ranks) self.exploitabilityCnts["High"] = basequery.filter( Crash.rank == 4).count() self.exploitabilityCnts["Medium"] = basequery.filter( Crash.rank == 3).count() self.exploitabilityCnts["Low"] = basequery.filter( Crash.rank == 2).count() self.exploitabilityCnts["Unknown"] = basequery.filter( Crash.rank == 1).count() self.exploitabilityCnts["None"] = basequery.filter( Crash.rank == 0).count() html = self.toHTML() self.web.setText(html)
def plot_discovered_paths(target_slug): session = db.getSession() target_runs = session.query(RunBlock).filter(RunBlock.target_config_slug == target_slug).all() figure, count = plt.subplots() # Plot the number of paths over time count.plot(get_fuzzing_time(target_runs), [block.num_paths for block in target_runs], marker=".", color="black") count.set_xlabel("Seconds spent fuzzing") count.set_ylabel("Unique Paths Encountered") count.legend(["Unique Paths"]) # Plot the estimated percentage of all paths over time percentage = count.twinx() percentage.plot( get_fuzzing_time(target_runs), [(block.path_coverage * 100) for block in target_runs], marker=",", color="r" ) percentage.legend(["Estimated Completion"]) percentage.set_ylabel("Estimated path completion percentage") plt.title("Code Paths Over Time") figure.tight_layout() return plt
def estimate_current_path_coverage(target_slug): session = db.getSession() target_query = session.query(PathRecord).filter( PathRecord.target_config_slug == target_slug) num_paths = target_query.count() num_singletons = target_query.filter(PathRecord.count == 1).count() num_doubletons = target_query.filter(PathRecord.count == 2).count() num_runs = sum(x.count for x in target_query.all()) session.close() # TODO - is it fair to calculate assuming at least one doubleton here? c = num_paths / (num_paths + ((num_runs - 1) / num_runs) * ((num_singletons**2) / (2 * max(num_doubletons, 1)))) print( "Total Paths:", num_paths, # "Singletons:", num_singletons, # "Doubletons:", num_doubletons, "Total Runs:", num_runs, "Estimated Path Fraction:", c, ) return num_paths, c
def byExecutable(path): cfg = config session = db.getSession() session.expire_on_commit = False ret = session.query(Checksec).filter( Checksec.hash == hash_file(path)).first() if ret: session.close() return ret checker = cfg.config["checksec_path"] cmd = [checker, "-j", path] try: out = subprocess.check_output(cmd) ret = json.loads(out) ret = Checksec(ret) session.add(ret) session.commit() session.close() except subprocess.CalledProcessError as x: print("Exception", x) return ret
def generate_report(dest=None, browser=True): # Create the template environment env = Environment(loader=PackageLoader("sl2", "reporting/templates")) env.filters["comma_ify"] = comma_ify # Pass in the comma_ify filter so we can use it when rendering template = env.get_template("index.html") # Look for any existing reports and increment the revision number if necessary target_dir = get_target_dir(sl2.harness.config.config) found = [ int(f.split("Report_v")[1].replace(".html", "")) for f in glob.glob(os.path.join(target_dir, "Report_v*.html")) ] revision = 0 if len(found) == 0 else max(found) + 1 # Render the graph of the estimated path coverage and base64 encode it slug = get_target_slug(sl2.harness.config.config) coverage_img = io.BytesIO() plt = plot_discovered_paths(slug) plt.savefig(coverage_img, format="png", dpi=200) coverage_graph = b64encode(coverage_img.getvalue()).decode("utf-8") # Get a current estimate of the path coverage num_paths, coverage_estimate = PathRecord.estimate_current_path_coverage(slug) # Grab the run blocks and crashes from the database session = db.getSession() run_blocks = session.query(RunBlock).filter(RunBlock.target_config_slug == slug).all() crash_base = session.query(Crash).filter(Crash.target_config_slug == slug) # create a big dict with all the environment variables for the template to render vars = { # Get the css framework and custom styles "normalize_css": env.get_template("css/normalize.css").render(), "skeleton_css": env.get_template("css/skeleton.css").render(), "custom_css": env.get_template("css/sl2.css").render(), # Get the base64 encoded logo "logo": env.get_template("images/logo.png.b64").render(), # Get app metadata "app_name": sl2.harness.config.profile, "revision": revision, "generated": datetime.datetime.now().isoformat(timespec="minutes"), "version": pkg_resources.require("sl2")[0].version, # Get the count of unique, total, and severe crashes from the database "uniq_crash_count": crash_base.distinct(Crash.crashash).group_by(Crash.crashash).count(), "total_crash_count": crash_base.count(), "severe_crash_count": crash_base.filter( Crash.exploitability != "None", Crash.exploitability != "Unknown", Crash.exploitability != "Low" ) .group_by(Crash.crashash) .count(), # Get the number of runs and time spent "run_count": sum(x.runs for x in run_blocks), "cpu_time": sum(((block.ended - block.started).total_seconds()) for block in run_blocks), # Get the number of paths and coverage "path_count": num_paths, "coverage_estimate": coverage_estimate * 100, "coverage_graph": coverage_graph, # Generate a list of the unique crashes "crashes": sorted( session.query(Crash).filter(Crash.target_config_slug == slug).group_by(Crash.crashash).all(), key=lambda crash: crash.int_exploitability, ), } # Write the report to the disk fname = os.path.join(target_dir, "Report_v{}.html".format(revision)) with open(fname, "w") as outfile: outfile.write(template.render(**vars)) # Copy the report to the user-selected output folder (if given) if dest is not None: if ".html" not in dest: dest = os.path.join(dest, "Report_v{}.html".format(revision)) copyfile(fname, dest) if browser: os.startfile(dest) else: print("Written to", fname) if browser: os.startfile(fname)
def get_crashes(self): return db.getSession().query(Crash).filter( Crash.target_config_slug == self.slug).all()
def getAll(): session = db.getSession() return session.query(Crash).all()
def __init__(self): QtWidgets.QMainWindow.__init__(self) self.crashes = [] self.thread_holder = [] self.paused_fuzzer_threads = [] self.start_time = None # Select config profile before starting self.cfg = ConfigWindow() if self.cfg.exec() == QtWidgets.QDialog.Rejected: sys.exit(1) # Set up basic window self.setWindowTitle("Sienna Locomotive 2") self.setMinimumSize(QSize(1800, 1300)) _central_widget = QtWidgets.QWidget(self) self.setCentralWidget(_central_widget) self._layout = QtWidgets.QVBoxLayout(_central_widget) _central_widget.setLayout(self._layout) # Set up Checksec, Wizard and Server threads so we don't block the UI # when they're running self.checksec_thread = ChecksecThread( config.config["target_application_path"]) self.wizard_thread = WizardThread(config.config) self.server_thread = ServerThread(close_on_exit=True) # CREATE WIDGETS # # Menu bar self.menu_bar = self.menuBar() self.file_menu = self.menu_bar.addMenu("&File") self.change_profile_action = self.file_menu.addAction("Change Profile") self.open_report_in_browser = QtWidgets.QAction( "Open exported report in browser", self, checkable=True) self.file_menu.addAction(self.open_report_in_browser) self.open_report_in_browser.setChecked(True) # Target info self.target_status = QtWidgets.QStatusBar() self.target_label = QtWidgets.QLabel() self.target_status.addWidget(self.target_label) self._layout.addWidget(self.target_status) self.checksec_thread.start() self.checksec_thread.result_ready.connect(self.checksec_finished) # Create wizard button self.wizard_button = QtWidgets.QPushButton("Run Wizard") self._layout.addWidget(self.wizard_button) # Set up function tree display self._func_tree = CheckboxTreeWidget() self._layout.addWidget(self._func_tree) # Set up underlying model for exposing function data self.target_data = get_target(config.config) self.model = CheckboxTreeModel() self.func_proxy_model = CheckboxTreeSortFilterProxyModel() self.func_proxy_model.setSourceModel(self.model) self.func_proxy_model.setFilterKeyColumn(0) self.file_proxy_model = CheckboxTreeSortFilterProxyModel() self.file_proxy_model.setSourceModel(self.func_proxy_model) self.file_proxy_model.setFilterKeyColumn(1) self.module_proxy_model = CheckboxTreeSortFilterProxyModel() self.module_proxy_model.setSourceModel(self.file_proxy_model) self.module_proxy_model.setFilterKeyColumn(4) self.build_func_tree() self._func_tree.setModel(self.module_proxy_model) self._func_tree.setItemDelegate( ComboboxTreeItemDelegate(self.target_data)) # These need to happen after we set the model self._func_tree.expandAll() self._func_tree.resizeColumnToContents(0) self._func_tree.resizeColumnToContents(1) self._func_tree.resizeColumnToContents(2) self._func_tree.resizeColumnToContents(3) # Create menu items for the context menu self.expand_action = QtWidgets.QAction("Expand All") self.collapse_action = QtWidgets.QAction("Collapse All") self.check_action = QtWidgets.QAction("Check All") self.uncheck_action = QtWidgets.QAction("Uncheck All") # Build layout for function filter text boxes self.filter_layout = QtWidgets.QHBoxLayout() self.filter_layout.addWidget(QtWidgets.QLabel("Filter Function: ")) self.func_filter_box = QtWidgets.QLineEdit() self.filter_layout.addWidget(self.func_filter_box) self.filter_layout.addWidget(QtWidgets.QLabel("Filter Files: ")) self.file_filter_box = QtWidgets.QLineEdit() self.filter_layout.addWidget(self.file_filter_box) self.filter_layout.addWidget(QtWidgets.QLabel("Filter Modules: ")) self.module_filter_box = QtWidgets.QLineEdit() self.filter_layout.addWidget(self.module_filter_box) # Set up fuzzer button self.fuzzer_button = QtWidgets.QPushButton("Fuzz selected targets") if not self.target_data.target_list: self.fuzzer_button.setEnabled(False) # Create checkboxes for continuous mode self.continuous_mode_cbox = QtWidgets.QCheckBox("Continuous") self.pause_mode_cbox = QtWidgets.QCheckBox("Pause on crash") if config.config["continuous"]: self.continuous_mode_cbox.setChecked(True) if config.config["exit_early"]: self.pause_mode_cbox.setChecked(True) # Set up spinboxes for setting timeout values self.fuzz_timeout_box = QtWidgets.QSpinBox() self.fuzz_timeout_box.setSuffix(" seconds") self.fuzz_timeout_box.setMaximum(1200) if "fuzz_timeout" in config.config: self.fuzz_timeout_box.setValue(config.config["fuzz_timeout"]) self.fuzz_timeout_box.setSpecialValueText("None") self.tracer_timeout_box = QtWidgets.QSpinBox() self.tracer_timeout_box.setSuffix(" seconds") self.tracer_timeout_box.setMaximum(2400) if "tracer_timeout" in config.config: self.tracer_timeout_box.setValue(config.config["tracer_timeout"]) self.tracer_timeout_box.setSpecialValueText("None") self.tracer_timeout_box.setSingleStep(10) self.verbose_cbox = QtWidgets.QCheckBox() self.verbose_cbox.clicked.connect(self.toggle_verbose_state) # Create spinbox for controlling simultaneous fuzzing instances self.thread_count = QtWidgets.QSpinBox() self.thread_count.setSuffix(" threads") self.thread_count.setRange(1, 2 * cpu_count()) if "simultaneous" in config.config: self.thread_count.setValue(config.config["simultaneous"]) # Create button for hiding and showing the extended controls self.expand_button = QtWidgets.QToolButton() self.expand_button.setArrowType(Qt.DownArrow) # Create nested widget to hold the expanded fuzzing controls self.extension_widget = QtWidgets.QWidget() self.extension_layout = QtWidgets.QGridLayout() self.extension_widget.setLayout(self.extension_layout) # Create layouts for fuzzing controls self.fuzz_controls_outer_layout = QtWidgets.QHBoxLayout() self.fuzz_controls_inner_left = QtWidgets.QVBoxLayout() self.fuzz_controls_inner_right = QtWidgets.QVBoxLayout() # Add widgets to left, right, and expanded layouts self.fuzz_controls_inner_left.addLayout(self.filter_layout) self.fuzz_controls_inner_left.addWidget(self.extension_widget) self.extension_widget.hide() self.fuzz_controls_inner_left.addWidget(self.fuzzer_button) self.extension_layout.addWidget(self.continuous_mode_cbox, 0, 0) self.extension_layout.addWidget(self.pause_mode_cbox, 1, 0) self.extension_layout.addWidget(QtWidgets.QLabel("Fuzz timeout:"), 0, 1, 1, 1, Qt.AlignRight) self.extension_layout.addWidget(self.fuzz_timeout_box, 0, 2, 1, 1, Qt.AlignLeft) self.extension_layout.addWidget(QtWidgets.QLabel("Triage Timeout:"), 1, 1, 1, 1, Qt.AlignRight) self.extension_layout.addWidget(self.tracer_timeout_box, 1, 2, 1, 1, Qt.AlignLeft) self.extension_layout.addWidget( QtWidgets.QLabel("Simultaneous fuzzing threads:"), 0, 3, 1, 1, Qt.AlignRight) self.extension_layout.addWidget(self.thread_count, 0, 4, 1, 1, Qt.AlignLeft) self.extension_layout.addWidget(QtWidgets.QLabel("Verbose:"), 1, 3, 1, 1, Qt.AlignRight) self.extension_layout.addWidget(self.verbose_cbox, 1, 4, 1, 1, Qt.AlignLeft) self.fuzz_controls_inner_right.addWidget(self.expand_button) # Compose layouts self.fuzz_controls_outer_layout.addLayout( self.fuzz_controls_inner_left) self.fuzz_controls_outer_layout.addLayout( self.fuzz_controls_inner_right) self._layout.addLayout(self.fuzz_controls_outer_layout) # Crashes table session = db.getSession() self.crashes_model = sqlalchemy_model.SqlalchemyModel( session, db.Crash, [ ("Time", db.Crash.timestamp, "timestamp", {}), ("RunID", db.Crash.runid, "runid", {}), ("Reason", db.Crash.crashReason, "crashReason", {}), ("Exploitability", db.Crash.exploitability, "exploitability", {}), ("Ranks", db.Crash.ranksString, "ranksString", {}), ("Crashash", db.Crash.crashash, "crashash", {}), ("Crash Address", db.Crash.crashAddressString, "crashAddressString", {}), ("RIP", db.Crash.instructionPointerString, "instructionPointerString", {}), ("RSP", db.Crash.stackPointerString, "stackPointerString", {}), ("RDI", db.Crash.rdi, "rdi", {}), ("RSI", db.Crash.rsi, "rsi", {}), ("RBP", db.Crash.rdx, "rbp", {}), ("RAX", db.Crash.rax, "rax", {}), ("RBX", db.Crash.rbx, "rbx", {}), ("RCX", db.Crash.rcx, "rcx", {}), ("RDX", db.Crash.rdx, "rdx", {}), ], orderBy=desc(db.Crash.timestamp), filters={"target_config_slug": get_target_slug(config.config)}, ) self.crashes_table = QtWidgets.QTableView() self.crashes_table.setFont( QFontDatabase.systemFont(QFontDatabase.FixedFont)) self.crashes_table.setModel(self.crashes_model) self._layout.addWidget(self.crashes_table) self.crashes_table.horizontalHeader().setStretchLastSection(True) self.crashes_table.resizeColumnsToContents() self.crashes_table.show() self.crashes_table.clicked.connect(self.crash_clicked) # Crash Browser, details about a crash self.crash_browser = QtWidgets.QTextBrowser() self.crash_browser.setText("<NO CRASH SELECTED>") self.crash_browser.setFont( QFontDatabase.systemFont(QFontDatabase.FixedFont)) self._layout.addWidget(self.crash_browser) self.stats_widget = stats.StatsWidget(get_target_slug(config.config)) self._layout.addWidget(self.stats_widget) # Set up stop button (and hide it) self.stop_button = QtWidgets.QPushButton("Stop Fuzzing") self.stop_button.hide() self._layout.addWidget(self.stop_button) self.export_triage_button = QtWidgets.QPushButton("Export Triage") self._layout.addWidget(self.export_triage_button) # Set up status bar self.status_bar = QtWidgets.QStatusBar() self._layout.addWidget(self.status_bar) # Create helper variables for storing counters with signals attached self.runs, self.crash_counter = QIntVariable(0), QIntVariable(0) self.throughput = QFloatVariable(0.0) self.run_adapter = QTextAdapter("Fuzzing Runs: {0.value} ", self.runs) self.throughput_adapter = QTextAdapter(" {0.value:.3f} Runs/s ", self.throughput) self.crash_adapter = QTextAdapter(" Crashes Found: {0.value} ", self.crash_counter) # Create the busy label self.busy_label = QtWidgets.QLabel() busy_gif = QMovie("gui/busy.gif") self.busy_label.setMovie(busy_gif) busy_gif.start() self.busy_label.hide() # Set up labels for the status bar self.fuzz_count = QtWidgets.QLabel() self.throughput_label = QtWidgets.QLabel() self.crash_count = QtWidgets.QLabel() # Add all the labels to the status bar self.status_bar.addPermanentWidget(self.busy_label) self.status_bar.addWidget(self.fuzz_count) self.status_bar.addWidget(self.throughput_label) self.status_bar.addWidget(self.crash_count) # CONNECT SIGNALS # self.change_profile_action.triggered.connect(self.change_profile) # Update the text of the status bar adapters whenever the underlying variables change self.runs.valueChanged.connect(self.run_adapter.update) self.throughput.valueChanged.connect(self.throughput_adapter.update) self.crash_counter.valueChanged.connect(self.crash_adapter.update) self.run_adapter.updated.connect(self.fuzz_count.setText) self.throughput_adapter.updated.connect(self.throughput_label.setText) self.crash_adapter.updated.connect(self.crash_count.setText) # Start the wizard when we click the button and update the tree when we're done self.wizard_button.clicked.connect(self.wizard_thread.start) self.wizard_thread.started.connect( partial(self.setCursor, Qt.WaitCursor)) self.wizard_thread.finished.connect(self.unsetCursor) self.wizard_thread.result_ready.connect(self.wizard_finished) # Connect the context menu buttons self.expand_action.triggered.connect(self._func_tree.expandAll) self.collapse_action.triggered.connect(self._func_tree.collapseAll) self.check_action.triggered.connect(self.check_all) self.uncheck_action.triggered.connect(self.uncheck_all) # Filter the list of functions displayed when we type things into the boxes self.func_filter_box.textChanged.connect( self.func_proxy_model.setFilterFixedString) self.file_filter_box.textChanged.connect( self.file_proxy_model.setFilterFixedString) self.module_filter_box.textChanged.connect( self.module_proxy_model.setFilterFixedString) # Handle checks/unchecks in the target tree self._func_tree.itemCheckedStateChanged.connect(self.tree_changed) self.export_triage_button.clicked.connect(self.export_triage) # Fuzzer control buttons for showing the panel and starting a run self.expand_button.clicked.connect(self.toggle_expansion) self.fuzzer_button.clicked.connect(self.server_thread.start) self.server_thread.finished.connect(self.start_all_threads) # If the user changes the continuous or pause mode, then we make sure the # two are consistent. self.pause_mode_cbox.stateChanged.connect(self.unify_pause_state) self.continuous_mode_cbox.stateChanged.connect( self.unify_continuous_state) # Connect the stop button to the thread so we can pause it self.stop_button.clicked.connect(self.pause_all_threads) # Connect the thread counter to the thread pool self.thread_count.valueChanged.connect(self.change_thread_count) self.change_thread_count(self.thread_count.value())
def occurrences(self): session = db.getSession() return session.query(Crash).filter( Crash.crashash == self.crashash).count()