nerdymark's Software Engineering & Cybersecurity Blog

Welcome to my digital homestead - a curated collection of projects, writeups, and experiments in Python, cybersecurity, and creative coding.

Here you'll find CTF writeups covering Azure OAuth privilege escalation, AWS S3 multi-service exploitation, Kubernetes SSRF attack chains, Terraform state poisoning, Go malware reverse engineering, and supply-chain compromises on GitHub Actions runners. You'll also find side projects like the Pokemon Sleep Roster Analyzer, an RDP/VNC network scanner 🔒, a LinkedIn feed analyzer 🔒 powered by Gemini, and word-puzzle solvers for Wordle and Hardle. Plus notes on building this Flask site, migrating to AWS, Bluesky cross-posting, and running a personal AI robot 🔒 out of my garage.

I'm Mark LaCore - Software Engineer by day, Raspberry Pi tinkerer by night. 25 years of turning caffeine into code, 20+ years of playing guitar, and a growing collection of CTF trophies. Explore the posts below, browse the CTF writeups, or drop me a line.

always_scan channels: never block, track to landing, learn from history Fixes a...

File: auto_rx/auto_rx.py
 temporary_block_list = {}
+def is_always_scan(freq_hz):
+    """True if freq_hz matches an always_scan (priority) channel — within
+    quantization/2, the same tolerance the scanner uses to match peaks. These
+    are frequencies we deliberately watch every pass (e.g. the local NWS RS41
+    on 403.998, which bursts ~2x daily), so they must NEVER be temporarily
+    blocked and are tracked all the way down rather than abandoned on burst."""
+    _tol = config.get("quantization", 10000) / 2.0
+    for _mhz in config.get("always_scan", []) or []:
+        if abs(freq_hz - _mhz * 1e6) < _tol:
+            return True
+    return False
+
+
 def allocate_sdr(check_only=False, task_description=""):
     """Allocate an un-used SDR for a task.
 def start_scanner():
             sondehub_hint_distance=config["sondehub_hint_distance"],
             sondehub_hint_interval=config["sondehub_hint_interval"],
             sondehub_hint_max_age=config["sondehub_hint_max_age"],
+            always_scan_history=config["always_scan_history"],
+            always_scan_history_max=config["always_scan_history_max"],
+            always_scan_history_days=config["always_scan_history_days"],
+            always_scan_history_interval=config["always_scan_history_interval"],
             station_lat=config["station_lat"],
             station_lon=config["station_lon"],
             never_scan=config["never_scan"],
 def start_decoder(freq, sonde_type, continuous=False):
             save_raw_hex=config["save_raw_hex"],
             wideband_sondes=config["wideband_sondes"],
             close_on_encrypted=config["close_on_encrypted"],
-            abandon_after_burst=config["abandon_after_burst"]
+            # Never abandon-on-burst an always_scan channel: it's a frequency we
+            # deliberately watch (our local balloon), so track it all the way to
+            # landing (feeding the burst/landing notifications) instead of
+            # dropping it to re-scan — and so it never emits the BurstAbandon
+            # exit state that would temporarily block it.
+            abandon_after_burst=0 if is_always_scan(freq)
+            else config["abandon_after_burst"],
         )
         autorx.sdr_list[_device_idx]["task"] = autorx.task_list[freq]["task"]
 def handle_scan_results():
                 if _too_close:
                     continue
+                # always_scan channels are never blocked — purge any stale entry
+                # (e.g. left by an older build) and let the decoder start.
+                if is_always_scan(_freq) and _freq in temporary_block_list:
+                    temporary_block_list.pop(_freq, None)
+                    logging.info(
+                        "Task Manager - %.3f MHz is an always_scan channel; ignoring temporary block."
+                        % (_freq / 1e6)
+                    )
+
                 # Check the frequency is not in our temporary block list
                 # (This may happen from time-to-time depending on the timing of the scan thread)
-                if _freq in temporary_block_list.keys():
+                if (not is_always_scan(_freq)) and _freq in temporary_block_list.keys():
                     if temporary_block_list[_freq] > (
                         time.time() - config["temporary_block_time"] * 60
                     ):
 def clean_task_list():
             # Check the exit state of the task for any abnormalities:
             if (_exit_state == "Encrypted") or (_exit_state == "TempBlock"):
                 # This task was a decoder, and it has encountered an encrypted sonde, or one too far away.
-                logging.info(
-                    "Task Manager - Adding temporary block for frequency %.3f MHz"
-                    % (_key / 1e6)
-                )
-                # Add the sonde's frequency to the global temporary block-list
-                temporary_block_list[_key] = time.time()
-                # If there is a scanner currently running, add it to the scanners internal block list.
-                if "SCAN" in autorx.task_list:
-                    autorx.task_list["SCAN"]["task"].add_temporary_block(_key)
+                if is_always_scan(_key):
+                    # Our watched channel — never block it, just let it re-scan.
+                    logging.info(
+                        "Task Manager - %.3f MHz exited %s but is an always_scan channel; not blocking."
+                        % (_key / 1e6, _exit_state)
+                    )
+                else:
+                    logging.info(
+                        "Task Manager - Adding temporary block for frequency %.3f MHz"
+                        % (_key / 1e6)
+                    )
+                    # Add the sonde's frequency to the global temporary block-list
+                    temporary_block_list[_key] = time.time()
+                    # If there is a scanner currently running, add it to the scanners internal block list.
+                    if "SCAN" in autorx.task_list:
+                        autorx.task_list["SCAN"]["task"].add_temporary_block(_key)
-            if _exit_state == "BurstAbandon":
+            if _exit_state == "BurstAbandon" and not is_always_scan(_key):
                 # This decoder abandoned a burst/descending sonde so the SDR can
                 # re-scan for other sondes still aloft. Block its frequency for
                 # burst_block_time minutes (backdated stamp - the shared expiry
File: auto_rx/autorx/config.py
 def read_auto_rx_config(filename, no_sdr_test=False):
         "sondehub_hint_distance": 0.0,
         "sondehub_hint_interval": 600,
         "sondehub_hint_max_age": 60,
+        "always_scan_history": False,
+        "always_scan_history_max": 10,
+        "always_scan_history_days": 0,
+        "always_scan_history_interval": 3600,
         "always_decode": [],
         # Location Settings
         "station_lat": 0.0,
 def read_auto_rx_config(filename, no_sdr_test=False):
             )
             auto_rx_config["sondehub_hint_max_age"] = 60
+        # nerdscan fork - learn always_scan (priority) channels from our own
+        # decoded-flight history (log/ filenames). See scan._update_history_hints.
+        try:
+            auto_rx_config["always_scan_history"] = config.getboolean(
+                "search_params", "always_scan_history"
+            )
+        except:
+            logging.debug(
+                "Config - Missing always_scan_history option, using default (False)"
+            )
+            auto_rx_config["always_scan_history"] = False
+        try:
+            auto_rx_config["always_scan_history_max"] = config.getint(
+                "search_params", "always_scan_history_max"
+            )
+        except:
+            auto_rx_config["always_scan_history_max"] = 10
+        try:
+            auto_rx_config["always_scan_history_days"] = config.getint(
+                "search_params", "always_scan_history_days"
+            )
+        except:
+            auto_rx_config["always_scan_history_days"] = 0
+        try:
+            auto_rx_config["always_scan_history_interval"] = config.getint(
+                "search_params", "always_scan_history_interval"
+            )
+        except:
+            auto_rx_config["always_scan_history_interval"] = 3600
+
         # Location Settings
         auto_rx_config["station_lat"] = config.getfloat("location", "station_lat")
         auto_rx_config["station_lon"] = config.getfloat("location", "station_lon")
File: auto_rx/autorx/scan.py
 def __init__(
         sondehub_hint_distance=0.0,
         sondehub_hint_interval=600,
         sondehub_hint_max_age=60,
+        always_scan_history=False,
+        always_scan_history_max=10,
+        always_scan_history_days=0,
+        always_scan_history_interval=3600,
         station_lat=0.0,
         station_lon=0.0,
         never_scan=[],
 def __init__(
         # Live SondeHub hint frequencies (MHz), refreshed by _update_sondehub_hints.
         self.sondehub_hints = []
         self.sondehub_hints_last_query = 0.0
+        # Learned priority frequencies (MHz) from our own decoded-flight history,
+        # refreshed by _update_history_hints. Radiosonde launch sites reuse the
+        # same frequency, so a channel we've decoded before is a good bet next
+        # launch — this auto-grows always_scan from experience.
+        self.always_scan_history = always_scan_history
+        self.always_scan_history_max = always_scan_history_max
+        self.always_scan_history_days = always_scan_history_days
+        self.always_scan_history_interval = always_scan_history_interval
+        self.history_hints = []
+        self.history_hints_last_query = 0.0
         self.never_scan = never_scan
         self.snr_threshold = snr_threshold
         self.min_distance = min_distance
 def scan_loop(self):
             # Refresh SondeHub nearby-sonde hints (rate-limited; no-op if disabled).
             self._update_sondehub_hints()
+            # Refresh learned priority channels from our flight history.
+            self._update_history_hints()
             try:
                 _results = self.sonde_search()
 def sonde_search(self, first_only=False):
             )
             # Remove any frequencies in the temporary block list
+            # Priority (always_scan / SondeHub-hint) frequencies are never
+            # removed here: they are appended above precisely because we want a
+            # detect dwell on them every pass, so a block entry landing within
+            # quantization/2 of one (e.g. a spur next to the local balloon on
+            # 403.998) must not delete it.
+            _priority_hz = np.array(self._priority_frequencies()) * 1e6
             self.temporary_block_list_lock.acquire()
             for _frequency in self.temporary_block_list.copy().keys():
                 # Check the time the block was added.
                 if self.temporary_block_list[_frequency] > (
                     time.time() - self.temporary_block_time * 60
                 ):
                     # We should still be blocking this frequency, so remove any peaks with this frequency.
-                    _index = np.argwhere(
-                        np.abs(peak_frequencies - _frequency)
-                        < (self.quantization / 2.0)
+                    _block_mask = np.abs(peak_frequencies - _frequency) < (
+                        self.quantization / 2.0
                     )
+                    if _priority_hz.size and peak_frequencies.size:
+                        _protected = np.min(
+                            np.abs(peak_frequencies[:, None] - _priority_hz[None, :]),
+                            axis=1,
+                        ) < (self.quantization / 2.0)
+                        _block_mask &= ~_protected
+                    _index = np.argwhere(_block_mask)
                     peak_frequencies = np.delete(peak_frequencies, _index)
                     if len(_index) > 0:
                         self.log_debug(
 def running(self):
         return self.sonde_scanner_running
     def _priority_frequencies(self):
-        """The always_scan list plus any live SondeHub hints (MHz), with hints
-        that duplicate an always_scan channel (within quantization/2) removed."""
+        """The always_scan list plus any live SondeHub hints and learned
+        history frequencies (MHz), de-duplicated so a frequency within
+        quantization/2 of one already in the list is not added twice.
+        Explicit always_scan entries take precedence, then SondeHub hints,
+        then learned history."""
         _prio = list(self.always_scan)
-        for _hint in self.sondehub_hints:
+        for _extra in list(self.sondehub_hints) + list(self.history_hints):
             if all(
-                abs(_hint - _f) * 1e6 > (self.quantization / 2.0) for _f in _prio
+                abs(_extra - _f) * 1e6 > (self.quantization / 2.0) for _f in _prio
             ):
-                _prio.append(_hint)
+                _prio.append(_extra)
         return _prio
+    def _update_history_hints(self):
+        """Learn priority scan channels from our own decoded-flight history.
+
+        Every sonde log file is a flight we SUCCESSFULLY decoded, and its
+        transmit frequency is right in the filename, so the set of frequencies
+        we've heard before is a strong predictor of future launches from the
+        same sites (radiosonde sites reuse their frequency). We rank those
+        frequencies by most-recent-flight then flight-count and stage the top
+        always_scan_history_max as priority channels — treated exactly like
+        always_scan (scanned first, interleaved, never auto-blocked). Filename
+        parsing only (no file reads), rate-limited, and a failure just keeps
+        the previous list.
+        """
+        if not self.always_scan_history or self.always_scan_history_max <= 0:
+            return
+        _now = time.time()
+        if (_now - self.history_hints_last_query) < self.always_scan_history_interval:
+            return
+        self.history_hints_last_query = _now
+
+        try:
+            from autorx.log_files import list_log_files
+
+            _cutoff = 0.0
+            if self.always_scan_history_days > 0:
+                _cutoff = _now - self.always_scan_history_days * 86400.0
+
+            # Group flights that sit in the same quantization bucket (so a site
+            # heard at 403.997/403.998/403.999 counts as one channel), but keep
+            # the ACTUAL most-recent decoded frequency for each — the bucket
+            # centre can be a couple kHz off the real transmit frequency (we have
+            # better luck on the exact freq we last heard).
+            # bucket (Hz) -> [flight_count, latest_epoch, latest_freq_mhz]
+            _by_freq = {}
+            for _f in list_log_files(quicklook=False):
+                _freq_mhz = _f.get("freq")
+                _dt = _f.get("datetime")           # e.g. "2026-08-08T23:18:59Z"
+                if not _freq_mhz:
+                    continue
+                _epoch = 0.0
+                if _dt:
+                    try:
+                        _epoch = datetime.datetime.strptime(
+                            _dt.replace("Z", ""), "%Y-%m-%dT%H:%M:%S"
+                        ).replace(tzinfo=datetime.timezone.utc).timestamp()
+                    except Exception:
+                        _epoch = 0.0
+                if _cutoff and _epoch and _epoch < _cutoff:
+                    continue
+                _bucket = round(_freq_mhz * 1e6 / self.quantization) * self.quantization
+                _rec = _by_freq.setdefault(_bucket, [0, 0.0, _freq_mhz])
+                _rec[0] += 1
+                if _epoch >= _rec[1]:
+                    _rec[1] = _epoch
+                    _rec[2] = _freq_mhz            # keep the most-recent exact freq
+
+            # Rank most-recent first, then by flight count; emit the real freqs.
+            _ranked = sorted(
+                _by_freq.values(), key=lambda v: (v[1], v[0]), reverse=True
+            )
+            _hints = [round(_v[2], 3) for _v in _ranked[: self.always_scan_history_max]]
+
+            if _hints != self.history_hints:
+                self.log_info(
+                    "Learned %d priority channel(s) from flight history: %s"
+                    % (len(_hints), ", ".join("%.3f MHz" % _h for _h in _hints))
+                )
+            self.history_hints = _hints
+        except Exception as e:
+            self.log_warning("Flight-history hint update failed - %s" % str(e))
+
     def _update_sondehub_hints(self):
         """Query SondeHub for sondes currently aloft within
         sondehub_hint_distance km of the station, and stage their reported
 def add_temporary_block(self, frequency, block_time_min=None):
                 temporary_block_time) releases them after this many minutes instead.
                 None = the full temporary_block_time.
         """
+        # Never block a priority (always_scan / SondeHub-hint) frequency — these
+        # are channels we deliberately watch every pass.
+        for _always in self._priority_frequencies():
+            if abs(frequency - _always * 1e6) < (self.quantization / 2.0):
+                self.log_debug(
+                    "Not blocking %.3f MHz — it is an always_scan channel."
+                    % (frequency / 1e6)
+                )
+                return
+
         if block_time_min is None:
             _stamp = time.time()
         else:
Read more...