<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TiltedLunar123</title>
    <description>The latest articles on DEV Community by TiltedLunar123 (@tiltedlunar123).</description>
    <link>https://dev.to/tiltedlunar123</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3847611%2F5372ff69-df32-4335-9ef6-65d8c9504ae5.jpeg</url>
      <title>DEV Community: TiltedLunar123</title>
      <link>https://dev.to/tiltedlunar123</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tiltedlunar123"/>
    <language>en</language>
    <item>
      <title>the size cap on my log scanner turns itself off exactly when it can't measure the file</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:20:19 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/the-size-cap-on-my-log-scanner-turns-itself-off-exactly-when-it-cant-measure-the-file-4j20</link>
      <guid>https://dev.to/tiltedlunar123/the-size-cap-on-my-log-scanner-turns-itself-off-exactly-when-it-cant-measure-the-file-4j20</guid>
      <description>&lt;p&gt;I maintain a small python tool called SIEMForge. it builds and converts SIEM detection content, Sigma rules plus a Sysmon config plus Wazuh rules, and it ships a log scanner so you can test a rule against a raw log file before you push it to a real SIEM.&lt;/p&gt;

&lt;p&gt;a while back i added a size cap to the scanner. every parse path reads the whole file into memory, so a 5GB syslog or some attacker-supplied json blob would eat all the RAM the process can grab before matching even starts. the cap is 100MB by default, overridable with an env var. boring, and exactly the kind of thing you add once and forget.&lt;/p&gt;

&lt;p&gt;last week i was reading the coverage report properly instead of skimming the total, and the size guard had a branch nothing tested. here's the whole function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_check_log_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_resolve_max_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LogFileTooLargeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Log file &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes, which exceeds the &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-byte limit. Set SIEMFORGE_MAX_LOG_BYTES to raise it.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;look at the except. if &lt;code&gt;stat()&lt;/code&gt; throws, the function returns None and the scan keeps going. so the safety check disables itself precisely when it can't measure the file. and "can't stat" is not a friendly condition. it's a permissions problem, or a race where the file changed under you, or some weird mount. the kind of thing that shows up when someone is poking at your box, not when everything is calm.&lt;/p&gt;

&lt;p&gt;i wrote a test to pin the behavior down, not to change it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_check_log_size_ignores_stat_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;boom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stat blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;boom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;_check_log_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that's a characterization test. it says "this is what the code does today," and if someone changes it the test yells. i left the behavior in place because there's a real argument for it: don't refuse to scan a file just because you couldn't read its size. but for a security tool the other side of that argument is louder. the guard exists to stop a memory blowup, and the one case where it steps aside is the adversarial one.&lt;/p&gt;

&lt;p&gt;then i kept pulling the thread, and the same question showed up everywhere. what does this scanner do when it meets something it doesn't understand? the answer wasn't consistent.&lt;/p&gt;

&lt;p&gt;the size-too-big path fails loud. it raises, &lt;code&gt;scan_logs&lt;/code&gt; catches it, prints "Failed to parse", returns -1. you know something went wrong. but an unknown file extension doesn't raise. the format resolver just falls back to json, the json parser reads a not-json file, gets nothing back, and you see "No alerts, all events are clean." drop a &lt;code&gt;.evtx&lt;/code&gt; in by mistake and the tool hands it a clean bill of health.&lt;/p&gt;

&lt;p&gt;CSV is the same shape. if the header sniffer can't make up its mind, the parser treats every row as data and names the columns col_0, col_1, col_2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_csv_sniffer_error_falls_back_to_positional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;boom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;could not determine delimiter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sniffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;boom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ambiguous.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;a,b
1,2
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_log_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;col_0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;col_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the data is all there, but your Sigma rule is keyed on CommandLine and Image, and there is no CommandLine anymore, there's col_4. so the rule matches nothing and the scan comes back quiet.&lt;/p&gt;

&lt;p&gt;the part that bugs me is the split. some of these fail loud and some fail silent, and there's no principle behind which is which. it's just wherever i happened to write a &lt;code&gt;raise&lt;/code&gt; versus a &lt;code&gt;return&lt;/code&gt;. for most tools that's fine. for a thing whose entire job is answering "did anything match," silent-zero is the worst possible default, because "0 alerts" now means one of two opposite things: your logs are clean, or my scanner quietly gave up. an analyst can't tell those apart from the outside, and the second one is the one that gets someone owned.&lt;/p&gt;

&lt;p&gt;the tests i added don't fix any of this. they took the suite from 90% to 96%, but the number isn't the point. the point is they made the inconsistency legible. i can now see, in one file, every place the scanner swallows a problem and returns empty. that list is the actual to-do.&lt;/p&gt;

&lt;p&gt;what i'd change, roughly in order: make the unknown-suffix fallback print a warning instead of silently guessing json, log a one-line notice when a CSV drops to positional keys, and reconsider the stat() swallow so a file it can't measure gets flagged rather than waved through. none of that is hard. the hard part was noticing that "0 alerts" had been lying to me by omission.&lt;/p&gt;

&lt;p&gt;honest gaps, since i'd want them if i were reading this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the stat() fail-open only triggers if stat() actually throws, which is rare. rare is not never, and rare tends to line up with "under attack."&lt;/li&gt;
&lt;li&gt;the quantifier parser fails closed on a malformed count, which is defensible. a broken rule condition shouldn't invent an alert. but it's still a silent no-match with no warning that the rule was bad, so a typo in a condition just detects nothing, forever.&lt;/li&gt;
&lt;li&gt;the positional-key CSV fallback keeps the data addressable, which really is better than crashing, but only if something downstream knows to read col_N. nothing does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;repo if you want to see the rest: &lt;a href="https://github.com/TiltedLunar123/SIEMForge" rel="noopener noreferrer"&gt;https://github.com/TiltedLunar123/SIEMForge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;it works. the detection logic is fine. i just learned that the scariest output a detection tool can give you is a calm, confident zero.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>python</category>
      <category>testing</category>
      <category>siem</category>
    </item>
    <item>
      <title>How to actually unsubscribe from marketing email in Gmail (and when you shouldn't)</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Sat, 11 Jul 2026 12:38:58 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/how-to-actually-unsubscribe-from-marketing-email-in-gmail-and-when-you-shouldnt-1lg2</link>
      <guid>https://dev.to/tiltedlunar123/how-to-actually-unsubscribe-from-marketing-email-in-gmail-and-when-you-shouldnt-1lg2</guid>
      <description>&lt;p&gt;Everyone tells you to just unsubscribe. Then you click the tiny link at the bottom of a newsletter, land on a page that wants your email again and twelve checkboxes, and three days later the same sender is back in your inbox. What went wrong? Unsubscribing does work. It just does not work the way most people do it. Here is what is actually happening under the hood, and how to clear promotional mail without making it worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The header that does the real work
&lt;/h2&gt;

&lt;p&gt;Legitimate bulk senders attach a List-Unsubscribe header to every message, and it has been a standard since RFC 2369 back in 1998. The header carries a way to opt out, usually a mailto address or an https URL, sometimes both. Your mail client reads it and turns it into the "Unsubscribe" link Gmail shows right next to the sender name at the top of the email. That link is not the same as the unsubscribe link buried down in the email body, and the difference matters more than you would think.&lt;/p&gt;

&lt;p&gt;In 2017, RFC 8058 added one-click unsubscribe on top of that. It lets Gmail fire a single POST request to the sender's opt-out URL for you. No page load, no website to poke at. When you tap the Unsubscribe link at the top of a Gmail message, that is usually what happens behind the scenes. One tap and you are off the list. Done. As of early 2024, Google and Yahoo require this header from anyone sending more than 5,000 messages a day, so it now covers most of what actually floods a normal inbox.&lt;/p&gt;

&lt;p&gt;The first rule is simple: use the Unsubscribe link Gmail puts at the top of the message, not the one inside it. The top one runs through the header, which makes it both faster and safer. Skip the body link.&lt;/p&gt;

&lt;h2&gt;
  
  
  When unsubscribing backfires
&lt;/h2&gt;

&lt;p&gt;Unsubscribing assumes the sender is honest. For real companies under CAN-SPAM or GDPR, that holds, and they have to process your opt-out, so they mostly do. For actual spam it falls apart. Clicking a link inside a shady email can confirm to the sender that your address is live and watched, which is worth more to them than any single campaign. So they keep it. That is the whole reason unsubscribe advice always comes with an asterisk.&lt;/p&gt;

&lt;p&gt;So how do you tell the two apart? Mail from a company you recognize, even one you have no memory of signing up with, is safe to unsubscribe from. Mail from a sender you cannot place, with no real business behind it, should be reported as spam instead. Report Spam does two things at once. It pulls the message and teaches Gmail's filter to catch that sender next time, and it never pings the sender back to confirm you exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three tools for three jobs
&lt;/h2&gt;

&lt;p&gt;Not everything deserves an unsubscribe. So which mail gets which treatment?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unsubscribe when you never want this sender again and they are legitimate.&lt;/li&gt;
&lt;li&gt;Filter when you still want the mail but not in your face. A filter that auto-archives or labels the routine stuff, like receipts and shipping notices, keeps them searchable without cluttering the inbox.&lt;/li&gt;
&lt;li&gt;Bulk delete when the mail already piled up. Unsubscribing stops the future flow, but it does nothing about the 4,000 old promos already sitting in your account and quietly eating storage. That pile only grows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most people only ever reach for the first one. That is exactly why their inbox never gets lighter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it at scale
&lt;/h2&gt;

&lt;p&gt;The manual version of all this is slow. Gmail hands you one Unsubscribe link at a time, so with a few hundred senders you are looking at an afternoon of clicking. Nobody finishes that. That gap is why I built Gmail One-Click Cleaner, a Chrome extension that works through your subscriptions in bulk using Gmail's own native unsubscribe controls, then clears out the backlog those senders already left behind. It runs entirely in your browser, nothing gets sent to a server, and a dry-run mode shows you exactly what it would touch before it touches anything. The core cleanup is free, and the bulk-unsubscribe pass is a one-time $5 unlock. Listing is here: &lt;a href="https://chromewebstore.google.com/detail/bmcfpljakkpcbinhgiahncpcbhmihgpc" rel="noopener noreferrer"&gt;Gmail One-Click Cleaner on the Chrome Web Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whichever way you get there, by hand or with a tool, the order stays the same. Kill the future flow with the header-based unsubscribe, report the senders who ignore it, filter the mail you still want, and bulk-delete the pile that already built up. Do those four and the promo tide finally goes back out.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>gmail</category>
      <category>email</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Zero Trust on Security+ clicks once you split it into two planes</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Sat, 11 Jul 2026 12:18:54 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/zero-trust-on-security-clicks-once-you-split-it-into-two-planes-2n46</link>
      <guid>https://dev.to/tiltedlunar123/zero-trust-on-security-clicks-once-you-split-it-into-two-planes-2n46</guid>
      <description>&lt;p&gt;Zero Trust shows up all over SY0-701, and it trips people up for one reason: CompTIA borrows the vocabulary straight from NIST 800-207, then scatters the pieces across the objectives without ever drawing you the picture. So you end up memorizing a stack of terms that all start with the word "Policy" and all sound alike. Which one actually made the decision, and which one just carried it out? On exam day, that blank is where the points leak.&lt;/p&gt;

&lt;p&gt;Here is the mental model that fixes it. Everything in Zero Trust lives in one of two planes. Access decisions get made in the control plane. Traffic actually moves in the data plane. Sort every term into one of those two buckets and the whole topic collapses into something you can hold in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea Zero Trust is built on
&lt;/h2&gt;

&lt;p&gt;Old-school network security trusted location. Get inside the perimeter and you were treated as friendly. That inside-the-wall assumption is what CompTIA calls an implicit trust zone, and Zero Trust exists to kill it. The rule is "never trust, always verify." Every request to reach a resource gets checked. Every time. Whether you are on the corporate LAN or a coffee-shop network, being inside the wall buys you nothing.&lt;/p&gt;

&lt;p&gt;Two ideas prop that up. Adaptive identity means the trust decision reacts to context, not just a password. The same user asking for the same file gets a different answer depending on the device they are on and where the request is coming from. Managed laptop at 10 a.m.? Fine. Same credentials from a new device in another country at 3 a.m.? Now the system can demand step-up authentication or refuse outright. Threat scope reduction is the blast-radius argument: segment access tightly and verify each hop, so a single compromised account can only reach the narrow slice it was explicitly allowed, not the whole flat network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control plane: where the decision happens
&lt;/h2&gt;

&lt;p&gt;The control plane is the brain. It decides who gets to talk to what, using policy-driven access control, which means rules built from who you are and what device you are on rather than an IP address alone. Two components do the work, and this is the pair people mix up.&lt;/p&gt;

&lt;p&gt;The Policy Engine is the decision maker. It runs the trust algorithm and spits out a verdict: grant, deny, or revoke. That is all it does. The Engine decides.&lt;/p&gt;

&lt;p&gt;The Policy Administrator is the executor. Once the Engine rules "grant," the Administrator opens the session and hands out the token for that specific connection. Later it tears the session back down. On its own it decides nothing. It only carries out what the Engine already ruled.&lt;/p&gt;

&lt;p&gt;A clean way to hold it: the Engine judges, the Administrator acts. Something weighing signals and reaching a verdict? Engine. Something setting up or tearing down the actual session? Administrator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data plane: where the traffic flows
&lt;/h2&gt;

&lt;p&gt;The data plane is where the real work happens once a decision is made. A few terms live here. The Subject (or System) is whoever is asking for access. Could be a person. Might be a service account or a device. The Policy Enforcement Point is the gate. It sits in the traffic path and enforces whatever the control plane decided; it lets the connection through and keeps monitoring, ready to cut it the second the control plane says so. It never decides on its own. It asks upstream and does what it is told. Nothing more. Implicit trust zones are the thing you are trying to shrink. Anywhere access gets granted broadly once you are inside, that is a trust zone, and good Zero Trust design keeps chipping those down.&lt;/p&gt;

&lt;p&gt;The flow, end to end: a Subject asks for a resource. The request hits the Policy Enforcement Point out in the data plane. That gate checks with the control plane, where the Policy Engine makes the call and the Policy Administrator sets up (or refuses) the session. Decision in the control plane; enforcement in the data plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this shows up on the exam
&lt;/h2&gt;

&lt;p&gt;CompTIA likes to hand you a scenario and ask which component is responsible. The trap is that all three names sound interchangeable if you learned them as a flat list. Classic trap. So anchor on the verbs instead. Something that evaluates policy and makes the access call is the Policy Engine. The piece that establishes or tears down the session, and issues the credential, is the Policy Administrator. Whatever sits in front of the resource actually enforcing the decision is the Enforcement Point. Then layer the plane split on top, because that is its own question type. Engine and Administrator live in the control plane, while the Enforcement Point lives out in the data plane.&lt;/p&gt;

&lt;p&gt;Drill this the way you would drill any confusable cluster. Cover the definitions and rebuild the request flow from memory until the plane split is automatic. Want to know whether Zero Trust is actually costing you points? The free diagnostic at &lt;a href="https://secplusmastery.com/diagnostic" rel="noopener noreferrer"&gt;secplusmastery.com/diagnostic&lt;/a&gt; breaks your score down by domain, so you learn whether architecture is a real gap or just an annoying one. The full run through the SY0-701 objectives lives at &lt;a href="https://secplusmastery.com" rel="noopener noreferrer"&gt;secplusmastery.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Get the two planes straight and Zero Trust stops being word salad. It becomes exactly what it is. A decision made in one spot and enforced in a different one, with every single request verified on its way through.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>comptia</category>
      <category>learning</category>
    </item>
    <item>
      <title>A Ryzen 7 5800X came out at $80 next to a $200 i7 because my tool read AMD model numbers on Intel's scale</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Fri, 10 Jul 2026 19:23:11 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/a-ryzen-7-5800x-came-out-at-80-next-to-a-200-i7-because-my-tool-read-amd-model-numbers-on-intels-1fen</link>
      <guid>https://dev.to/tiltedlunar123/a-ryzen-7-5800x-came-out-at-80-next-to-a-200-i7-because-my-tool-read-amd-model-numbers-on-intels-1fen</guid>
      <description>&lt;p&gt;I have a little PowerShell script that looks at your PC's hardware and guesses what it'd sell for. Detect the CPU, GPU, RAM, storage, and age, price each part, add it up. I've been fixing it in small pieces for a couple months.&lt;/p&gt;

&lt;p&gt;This week I ran it on a desktop with a Ryzen 7 5800X and the CPU line said $80. That's off by a lot. A used 5800X goes for way more. So I checked what an Intel chip from the same era does, dropped in an i7-10700, and got $200. Same class of part, same year, and my tool valued the AMD one at less than half.&lt;/p&gt;

&lt;p&gt;Here's the pricing, roughly. A base value by tier, then a multiplier for how new the chip is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Base value by tier&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'I9|RYZEN\s*9'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$baseValue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'I7|RYZEN\s*7'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$baseValue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'I5|RYZEN\s*5'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$baseValue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'I3|RYZEN\s*3'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$baseValue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Generation multiplier&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$genMultiplier&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tier part was fine. Ryzen 7 and i7 both land on 200. The problem was &lt;code&gt;$gen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That ladder was built for Intel, where the leading digits of the model number are the generation. An i7-10700 is 10th gen. An i9-14900K is 14th gen. So the code grabbed those digits and used them straight.&lt;/p&gt;

&lt;p&gt;For AMD I was doing the same thing, and AMD doesn't count like that. A Ryzen 7 5800X is a 5000-series part, but 5000 doesn't mean the fifth generation of anything Intel would recognize. It's Zen 3, from late 2020, which lines up against Intel's 11th gen. My code read the 5, called it generation 5, and dropped it in the &lt;code&gt;$gen -ge 4&lt;/code&gt; band, which is the 0.4 multiplier meant for 2015 Skylake chips. 200 times 0.4 is 80.&lt;/p&gt;

&lt;p&gt;The top of the stack was worse. A Ryzen 9 9950X (Zen 5, 2024) read as generation 9, hit the 0.8 band, and came out at $240 while an i9-14900K sitting next to it got $420.&lt;/p&gt;

&lt;p&gt;The fix is to translate the series digit into the Intel generation the part actually launched against, before the shared ladder ever sees it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'RYZEN\s*(?:\d\s+)?(?:PRO\s+)?(\d)\d{3}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$amdGen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="bp"&gt;$Matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# Zen, 2017&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# Zen+, 2018&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# Zen 2, 2019&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# Zen 2 mobile/APU, 2020&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# Zen 3, 2020&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# Zen 3+ mobile, 2022&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# Zen 4, 2022&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# Zen 4 APU, 2024&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# Zen 5, 2024&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$gen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$intelGen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$amdGen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the 5800X reads as gen 11, hits the 1.0 band, and comes out at $200, same as the i7-10700. I added three vendor pairs as tests (10700 vs 5800X, 14900K vs 9950X, 9400F vs 3600) that assert both sides price the same, so if I ever touch this ladder again and knock AMD back down, the suite catches it.&lt;/p&gt;

&lt;p&gt;That regex has a second thing baked in. It optionally allows a PRO token, because "Ryzen 7 PRO 5850U" was slipping past the old pattern and falling all the way to the &lt;code&gt;else&lt;/code&gt; branch (0.7), which somehow made a slower laptop chip worth more than the desktop 5800X it should lose to. Fixing the generation math and the PRO name in the same pass lined both back up.&lt;/p&gt;

&lt;p&gt;While I had the valuation code open I hit a GPU version of the same mistake. Every discrete Intel Arc card reports its name as something like "Intel(R) Arc(TM) A770 Graphics." My integrated-GPU check was matching on the word "Graphics," so it flagged every Arc card as integrated, valued it at $0, and then the code that picks the primary GPU skips integrated cards. An Arc-only build reported no discrete GPU at all.&lt;/p&gt;

&lt;p&gt;I couldn't just exclude "Arc," because the iGPU baked into Core Ultra chips is also called "Intel(R) Arc(TM) Graphics." The thing that separates them is the model number: discrete cards carry an A or B code, the integrated one doesn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get-GPUIntegratedFlag&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Arc\b.*\b[AB]\d{3}\b'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nv"&gt;$Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Intel.*(?:UHD|HD|Iris|Graphics)|AMD.*Radeon.*Graphics$|Vega.*Graphics'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulling that into its own function was half the fix. It used to live inside the big hardware-detection function behind live CIM queries, which is exactly why it never had a test. On its own it takes a string and returns a bool, so I could throw the real reported names at it: the A-series and B-series cards come back discrete, the bare "Arc Graphics" and a plain UHD 770 come back integrated.&lt;/p&gt;

&lt;p&gt;What's still rough, and I left it on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AMD map is a lookup, not real knowledge. It doesn't know clock speeds or core counts, so two chips in the same tier and generation get the same number even if one is clearly faster.&lt;/li&gt;
&lt;li&gt;Making the tier digit optional in that regex means a Threadripper name could in theory get read as a low-gen desktop part. Right now the 4-digit model requirement and the word "Threadripper" sitting in the way keep it from matching, but that's luck as much as design.&lt;/li&gt;
&lt;li&gt;The whole thing still values a CPU on tier and generation alone. A 65W and a 105W part of the same name price identically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a hobby script, not a pricing service, so being in the right ballpark and not embarrassingly wrong is the bar. Reading AMD on an Intel ruler was embarrassingly wrong.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/TiltedLunar123/pc-worth" rel="noopener noreferrer"&gt;https://github.com/TiltedLunar123/pc-worth&lt;/a&gt;&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>windows</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>The part of Security+ nobody studies: exam day itself</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Fri, 10 Jul 2026 10:21:46 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/the-part-of-security-nobody-studies-exam-day-itself-2dkf</link>
      <guid>https://dev.to/tiltedlunar123/the-part-of-security-nobody-studies-exam-day-itself-2dkf</guid>
      <description>&lt;p&gt;Everyone drills the objectives. Almost nobody rehearses the ninety minutes where the score actually gets made.&lt;/p&gt;

&lt;p&gt;I sit SY0-701 in August. Booking it dropped me into a pile of small operational details that have nothing to do with any exam objective, and several of them can burn your clock or end the attempt before question one ever loads. So here's the logistics side, which is the part no study guide covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick your delivery method on purpose
&lt;/h2&gt;

&lt;p&gt;You can take Security+ at a Pearson VUE test center, or at home through their online proctored option.&lt;/p&gt;

&lt;p&gt;Test center: someone else owns the machine and the network. Hardware dies, that's their problem. You pay for that with your morning, since you're driving there and then waiting behind other people at check-in.&lt;/p&gt;

&lt;p&gt;Home: no commute, and you sit in a chair you already know. In exchange you inherit every failure mode in your apartment. Your webcam. Your upload speed. The roommate who wanders through frame forty minutes in.&lt;/p&gt;

&lt;p&gt;Neither one is easier. The real question is which failures you can actually control. If your internet drops twice a week, that decision has already been made for you, hasn't it?&lt;/p&gt;

&lt;h2&gt;
  
  
  If you test at home, rehearse the room
&lt;/h2&gt;

&lt;p&gt;Run the system check on the exact machine, in the exact room, on the network you plan to use. Not the laptop you might use. The one you will.&lt;/p&gt;

&lt;p&gt;Then look at your desk the way a proctor will. Second monitor unplugged (better yet, moved). Nothing on the surface. Nothing within camera reach that could pass for a note, and that includes the sticky note on the wall you stopped seeing eight months ago. Your phone goes wherever they tell you to put it, because you'll be asked to show that it went there.&lt;/p&gt;

&lt;p&gt;Read the confirmation email for the rules attached to your specific booking. They vary, and they get updated. Being surprised by one at check-in costs you time you had budgeted for questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your ID has to match your registration
&lt;/h2&gt;

&lt;p&gt;The name on the booking has to match the name on your government photo ID. Not resemble it. Match it, down to the middle initial and the hyphen.&lt;/p&gt;

&lt;p&gt;Registered as Mike while your license says Michael? Fix that today. Right now it's a five minute account edit. On exam morning it's a canceled seat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check in early
&lt;/h2&gt;

&lt;p&gt;Check-in isn't instant. Photos of your ID. Photos of the room. Then a human somewhere reviewing all of it before you're cleared. Start the moment your booking window opens. Why? Because when something's wrong, checking in early is what turns a dead attempt into an annoying twenty minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know what 750 means before you build a study plan
&lt;/h2&gt;

&lt;p&gt;SY0-701 gives you a maximum of 90 questions in 90 minutes, mixed multiple choice and performance-based. Passing is 750 on a scale of 100 to 900.&lt;/p&gt;

&lt;p&gt;So that's 83 percent, right? No. The scale doesn't start at zero, and 750 is a scaled score whose raw-to-scaled conversion isn't published anywhere.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds like it should. Somebody scores 78 percent on a practice test, figures they're a few points off passing, and books the date. A percentage on a practice exam does tell you which domains are weak, though it won't convert into a scaled score. That's the whole reason you can find "passed, was scoring 95 on practice" sitting three replies above "failed, was scoring 85" in the same forum thread on the same afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide your clock rules while you're still calm
&lt;/h2&gt;

&lt;p&gt;Ninety questions, ninety minutes, and the performance-based ones usually show up first, quietly eating four or five minutes apiece if you let them.&lt;/p&gt;

&lt;p&gt;Decide in advance what happens when a PBQ stalls out. Flag it, fill in the fields you're sure of, and come back with whatever minutes survive. A rule you invent while the clock is running isn't really a rule; it's a panic response wearing a rule's clothes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know what happens when you finish
&lt;/h2&gt;

&lt;p&gt;Pass or fail shows up on screen when you end the exam. The score report, with its per-domain breakdown, lands in your CompTIA account rather than in your hand at the desk.&lt;/p&gt;

&lt;p&gt;If it's a fail, that breakdown is the most valuable document you own, and you should read it before you touch your study material again. Check CompTIA's current retake policy before you rebook, since the waiting period depends on which attempt you're on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The readiness question
&lt;/h2&gt;

&lt;p&gt;None of this substitutes for knowing the material. It only keeps the material from being the thing that goes wrong.&lt;/p&gt;

&lt;p&gt;Are you actually ready, or does it just feel that way after a good study week? That question deserves an answer with data under it. I built &lt;a href="https://secplusmastery.com" rel="noopener noreferrer"&gt;SecPlus Mastery&lt;/a&gt; while studying for this exam, and the piece worth pointing at here is the &lt;a href="https://secplusmastery.com/diagnostic" rel="noopener noreferrer"&gt;free diagnostic&lt;/a&gt;: an untimed run across the objectives that tells you which domains are soft before you put money down on a seat.&lt;/p&gt;

&lt;p&gt;Book the date once the diagnostic stops embarrassing you. Then give one evening to the list above. Losing an exam you knew the answers to is a genuinely stupid way to lose an exam, and it happens to people who never once opened the confirmation email.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>comptia</category>
      <category>career</category>
    </item>
    <item>
      <title>Stockfish already picks the best move. Getting it to pick a human-looking one was the hard part</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Thu, 09 Jul 2026 17:09:35 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/stockfish-already-picks-the-best-move-getting-it-to-pick-a-human-looking-one-was-the-hard-part-357n</link>
      <guid>https://dev.to/tiltedlunar123/stockfish-already-picks-the-best-move-getting-it-to-pick-a-human-looking-one-was-the-hard-part-357n</guid>
      <description>&lt;p&gt;so i built a desktop chess gui around stockfish. you against the engine, adjustable strength, move hints, edit the board, the usual. the part that took the longest wasn't the board or the animations. it was making the engine feel like an opponent instead of a wall.&lt;/p&gt;

&lt;p&gt;the problem: stockfish at full strength plays the single best move every time. that's correct and it's also boring, and worse, it doesn't feel human even when you crank the rating down. the built-in way to weaken it is UCI_Elo, and that does make it lose more. but the moves it loses with still look like engine moves. it'll slide a rook one square along the back rank because to the engine that's 4 centipawns better, and no club player alive would play that in a casual game.&lt;/p&gt;

&lt;p&gt;so i split the problem in two. strength stays with stockfish. the "feel" is a separate layer on top, and it's not allowed to touch the strength.&lt;/p&gt;

&lt;h2&gt;
  
  
  strength comes from UCI_Elo, flavour comes from re-ranking
&lt;/h2&gt;

&lt;p&gt;when human mode is on, i don't ask stockfish for one move. i ask it for its top 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;infos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multipv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;now i've got up to four candidate moves, each with a centipawn score. the top one is the engine's pick. the rest are moves it also considered, ranked slightly lower. those alternatives are the raw material.&lt;/p&gt;

&lt;p&gt;every candidate gets a weight, and the base weight is just how close it is to the best move:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;best_cp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.18&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the whole thing hinges on that taper. a move that's 80+ centipawns worse than the best basically never gets picked (weight 0.01). so whatever the human-feel layer does next, it physically can't make the engine hang a piece. it can only shuffle the order of moves that are already good.&lt;/p&gt;

&lt;p&gt;then i multiply those weights by human-feel factors, and every one is gated on the move already being close to the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# early game: develop your pieces, don't shuffle the queen out
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;move_num&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;piece&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KNIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BISHOP&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;1.18&lt;/span&gt;                       &lt;span class="c1"&gt;# minor piece off the back rank
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;chess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KNIGHT&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;to_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;                   &lt;span class="c1"&gt;# knight on the rim looks engine-y
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;chess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUEEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;                       &lt;span class="c1"&gt;# early queen sortie is a tell
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;there's more of these. recapturing on the square your opponent just took on gets a 1.5x bump (people recapture on reflex). castling gets 1.25x. giving check gets a small 1.1x. all gated so they only fire when the move is within about 30cp of best.&lt;/p&gt;

&lt;p&gt;last step, i don't just take the highest weight. i do a weighted random pick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moves&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so in a quiet equal position it might develop a knight one game and castle the next. that variety is most of what makes it feel less robotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  the style knob is the same trick, simpler
&lt;/h2&gt;

&lt;p&gt;separate from human mode there's an aggressive/balanced/defensive setting. same idea, flatter math. instead of a random draw i just re-rank the candidates by a bias:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aggressive: +25 for a capture, +30 for a check, +5 for pushing a pawn&lt;/li&gt;
&lt;li&gt;defensive: +15 for a quiet move, +8 for not giving check, +25 for castling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;with the same guard: a move more than 80cp below the best can't be biased ahead of it, ever. so aggressive mode grabs the sharp move when it's roughly as good, and shuts up when it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  testing move-taste without launching stockfish
&lt;/h2&gt;

&lt;p&gt;this is the part i actually liked. &lt;code&gt;_human_style&lt;/code&gt; talks to a real stockfish subprocess, which is a miserable thing to depend on in CI. so the tests hand the engine a fake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeEngine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multipv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_infos&lt;/span&gt;   &lt;span class="c1"&gt;# canned candidate list
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;no binary, no download, the inputs are just python-chess objects. that lets me assert the thing i actually care about, which is that the taste layer never costs real strength:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_top_move_dominates_when_alternatives_are_much_worse&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# top move at +100, alternative at -60 (160cp worse)
&lt;/span&gt;    &lt;span class="n"&gt;picks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_human_style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;_limit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;200 samples, fixed seed, and the top move has to win at least 150 of them. if i ever fat-finger the taper and let a bad move through, that test goes red.&lt;/p&gt;

&lt;h2&gt;
  
  
  what's still not great
&lt;/h2&gt;

&lt;p&gt;honest version: this is taste, not skill. every bonus only reshuffles moves stockfish already thinks are fine. a real 1400 hangs a knight sometimes. mine never will, because everything's gated on small centipawn diffs. so "human mode" really means "a tidy player who happens to never blunder," which isn't the same thing as a 1400.&lt;/p&gt;

&lt;p&gt;and the numbers are all eyeballed. 1.18, 0.55, the +25 for a capture, none of it is fit to real games. i picked values that looked right across a dozen test positions and stopped. the honest upgrade would be to score moves against a database of actual human games at a target rating, or use something like Maia (a net trained to predict the move a human of a given rating actually plays) instead of my hand-waving multipliers.&lt;/p&gt;

&lt;p&gt;it works though. in a quiet middlegame it'll castle and develop and recapture like a person, and that was the whole point. not perfect. but it plays like someone's sitting across from you now, which the raw engine never did.&lt;/p&gt;

&lt;p&gt;repo's here if you want to poke at it: &lt;a href="https://github.com/TiltedLunar123/stockfish-chess" rel="noopener noreferrer"&gt;https://github.com/TiltedLunar123/stockfish-chess&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>chess</category>
      <category>testing</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>The Gmail search operators I use to clear storage instead of paying Google for more</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:02:40 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/the-gmail-search-operators-i-use-to-clear-storage-instead-of-paying-google-for-more-382b</link>
      <guid>https://dev.to/tiltedlunar123/the-gmail-search-operators-i-use-to-clear-storage-instead-of-paying-google-for-more-382b</guid>
      <description>&lt;p&gt;Google's answer to a full Gmail is a button that sells you more storage. There's a cheaper one, and it's the search bar you already have. Gmail's search operators can find the exact mail that's eating your 15 GB, and you can clear most of it in a few passes. Here's the order I actually run them in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with size, because attachments are the real hogs
&lt;/h2&gt;

&lt;p&gt;Text emails are tiny. What fills a Gmail account is attachments. So the first search is always this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;larger:10M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That surfaces every message over 10 MB. Sort through a screen of those and you'll usually find old design files, phone-camera dumps, and PDFs you forwarded to yourself years ago. Delete a dozen of those and you've often freed more space than deleting ten thousand text emails would.&lt;/p&gt;

&lt;p&gt;Then step down a tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;larger:5M older_than:1y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;older_than:1y&lt;/code&gt; keeps recent stuff you might still need out of the results. You can use &lt;code&gt;older_than:6m&lt;/code&gt;, &lt;code&gt;older_than:2y&lt;/code&gt;, whatever cutoff you trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then clear the categories that refill on their own
&lt;/h2&gt;

&lt;p&gt;Promotions and social notifications arrive constantly and you almost never go back to read them. Gmail already sorts them, so you can target them directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;category:promotions older_than:6m
category:social older_than:1y
category:updates older_than:1y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;older_than&lt;/code&gt; piece matters here. It's the difference between clearing genuine clutter and accidentally trashing the shipping notification for the thing that hasn't arrived yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch the no-reply senders
&lt;/h2&gt;

&lt;p&gt;A lot of clutter comes from addresses that can't receive mail anyway. This finds them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from:no-reply older_than:1y
from:noreply older_than:1y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Newsletters and receipts often come from these. Skim before you delete, since some receipts are worth keeping, but most of it is safe to clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful modifiers to combine
&lt;/h2&gt;

&lt;p&gt;A few operators make the searches sharper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;has:attachment&lt;/code&gt; limits results to messages with files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is:unread&lt;/code&gt; finds mail you never even opened, which is usually the safest to delete.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-in:starred&lt;/code&gt; excludes anything you've starred, so you don't touch flagged mail.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;before:2023/01/01&lt;/code&gt; targets a hard date instead of a relative age.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack them. &lt;code&gt;category:promotions has:attachment older_than:1y -in:starred&lt;/code&gt; is a very safe, very productive delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing the operators can't tell you
&lt;/h2&gt;

&lt;p&gt;Search is great at finding mail by rule. It's bad at answering "which senders are actually eating my storage?" Gmail won't rank your space by sender, so you end up eyeballing &lt;code&gt;larger:&lt;/code&gt; results and guessing.&lt;/p&gt;

&lt;p&gt;That gap is most of why I built &lt;a href="https://chromewebstore.google.com/detail/bmcfpljakkpcbinhgiahncpcbhmihgpc" rel="noopener noreferrer"&gt;Gmail One-Click Cleaner&lt;/a&gt;. It runs these same size searches for you, attributes the space back to the senders behind it, and shows your top storage hogs so you're not guessing. It runs the category and no-reply sweeps as one-click presets too, and every run has a Restore button, so if you clear something you wanted back, one click puts it in your inbox. It's free, open source, and nothing leaves your browser.&lt;/p&gt;

&lt;p&gt;But you don't need it to start. Open your Gmail, type &lt;code&gt;larger:10M&lt;/code&gt;, and see what's been sitting there. Most people are surprised how fast the number drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that keeps it clean
&lt;/h2&gt;

&lt;p&gt;Doing this once helps. Doing it every month or two is what keeps you off the "storage almost full" banner for good. Pick a cutoff you're comfortable with, run the size search and the category sweeps, and you'll reclaim gigabytes on a schedule instead of panicking when Google tells you you're out.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>gmail</category>
      <category>email</category>
      <category>chrome</category>
    </item>
    <item>
      <title>How to survive the Security+ PBQs without wrecking your timing</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Thu, 09 Jul 2026 09:52:09 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/how-to-survive-the-security-pbqs-without-wrecking-your-timing-3gdc</link>
      <guid>https://dev.to/tiltedlunar123/how-to-survive-the-security-pbqs-without-wrecking-your-timing-3gdc</guid>
      <description>&lt;p&gt;The multiple choice on SY0-701 is not what sinks most people. The performance-based questions are. Not because they're harder, but because of when they show up and how much time they quietly take.&lt;/p&gt;

&lt;p&gt;PBQs usually sit at the front of the exam. You open the test and the first thing staring at you is a firewall rule table or a log file you have to interpret. That placement is a trap for anyone who works in order. You spend twelve minutes dragging controls into boxes, second-guessing every move, and now you've got 78 minutes left for everything else and your heart rate is up.&lt;/p&gt;

&lt;p&gt;Here's how to keep that from happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skip them first, come back last
&lt;/h2&gt;

&lt;p&gt;There's a flag button. Use it on every PBQ you hit early. Read it, get a rough sense of what it wants, then mark it and move on to the multiple choice.&lt;/p&gt;

&lt;p&gt;Two reasons this works. The multiple choice is faster money. You can answer forty of those in the time one messy PBQ eats, so bank the points you know are yours. And the questions you answer later often jog something loose for the PBQ you skipped. A definition buried in question 30 reminds you how the thing in PBQ 2 actually works. By the time you circle back, you've got context you didn't have at minute zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most PBQs give partial credit, so never leave one blank
&lt;/h2&gt;

&lt;p&gt;People don't believe this until they see their score report. A firewall PBQ with six rules does not grade pass-or-fail on the whole thing. You get credit for the rules you set correctly. A matching question gives you points for each correct pair.&lt;/p&gt;

&lt;p&gt;So do the parts you're sure about, make a reasonable guess on the rest, and never walk away with empty fields. A blank answer is a guaranteed zero on that piece. A guess isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the requirement, because it's usually written down
&lt;/h2&gt;

&lt;p&gt;PBQ scenarios tell you what they want. They just bury it in a sentence. "All directory lookups must be encrypted." "Follow least privilege." "Only the web server should be reachable from the internet." That line is your answer key. Lock onto it before you touch anything.&lt;/p&gt;

&lt;p&gt;If the scenario says least privilege and you open a rule to any/any, you already know that's wrong, no matter how reasonable it looks on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know the handful of shapes that keep coming back
&lt;/h2&gt;

&lt;p&gt;You're not going to see something exotic. The formats repeat.&lt;/p&gt;

&lt;p&gt;Firewall and ACL rules. Read top to bottom, first match wins, and there's an implicit deny at the bottom. Put your specific allow rules above any broad deny. If a rule never gets reached because a line above it already matched, it does nothing for you.&lt;/p&gt;

&lt;p&gt;Log analysis, identify the attack. Learn the tells. A pile of failed logins followed by one success is brute force or password spray. Requests marching through ports 20, 21, 22, 23 is a scan. A single quote with OR 1=1 in a URL is SQL injection. Dot-dot-slash is directory traversal. The log isn't asking you to be clever. It's asking you to recognize a pattern you've seen before.&lt;/p&gt;

&lt;p&gt;Control categorization. Drag the control into technical, managerial, operational, or physical. A firewall is technical. A written policy is managerial. Guard training is operational. A fence is physical. Get comfortable sorting controls that way and by preventive, detective, corrective too.&lt;/p&gt;

&lt;p&gt;Secure protocol and port matching. If you know the insecure-to-secure pairs, these are free. HTTP 80 to HTTPS 443. FTP to FTPS or SFTP. Telnet 23 to SSH 22. LDAP 389 to LDAPS 636.&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget the clock in your head
&lt;/h2&gt;

&lt;p&gt;You get about 90 minutes for up to 90 items, and PBQs count as more than one. If you're five questions deep and the clock says you've burned a third of your time, stop and move. No single question is worth failing the rest of the exam over.&lt;/p&gt;

&lt;p&gt;The people who walk out of SY0-701 frustrated almost never say the content beat them. They say they ran out of time. That's a strategy problem, and you can fix it before you ever sit down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drill the format, not just the facts
&lt;/h2&gt;

&lt;p&gt;Knowing what SFTP is and configuring a rule set under a timer are two different skills. Practice the interactive stuff the way it actually shows up so the format is muscle memory on exam day. I built the labs and PBQ practice on &lt;a href="https://secplusmastery.com" rel="noopener noreferrer"&gt;secplusmastery.com&lt;/a&gt; for that reason, and there's a free diagnostic at &lt;a href="https://secplusmastery.com/diagnostic" rel="noopener noreferrer"&gt;secplusmastery.com/diagnostic&lt;/a&gt; if you want to find your timing and weak spots before you lock in a test date.&lt;/p&gt;

&lt;p&gt;Get the PBQs handled and the rest of the exam feels a lot calmer.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>comptia</category>
      <category>career</category>
    </item>
    <item>
      <title>My VM state check reads one line of VBoxManage output, and it's one character from reading the wrong one</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Wed, 08 Jul 2026 17:20:50 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/my-vm-state-check-reads-one-line-of-vboxmanage-output-and-its-one-character-from-reading-the-107g</link>
      <guid>https://dev.to/tiltedlunar123/my-vm-state-check-reads-one-line-of-vboxmanage-output-and-its-one-character-from-reading-the-107g</guid>
      <description>&lt;p&gt;I have a PowerShell script that builds Fedora VMs in VirtualBox with no clicking. It also does AlmaLinux, Rocky, and CentOS Stream. After it kicks off an install it sits in a loop asking VirtualBox one question over and over: is this VM still running, or did it power off. The install is done when the guest powers itself off, so that answer is the whole exit condition.&lt;/p&gt;

&lt;p&gt;The question goes through one small function, &lt;code&gt;Get-VMState&lt;/code&gt;. It shells out to VBoxManage, reads back the machine-readable dump, and pulls the single line it cares about.&lt;/p&gt;

&lt;p&gt;Here's the whole thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get-VMState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mandatory&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$VBoxManage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mandatory&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$VMName&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Invoke-VBoxManage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-VBoxManage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VBoxManage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Arguments&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s2"&gt;"showvminfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VMName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--machinereadable"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoThrow&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-split&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;`r&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="se"&gt;`n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-like&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'VMState=*'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exciting. Except &lt;code&gt;showvminfo --machinereadable&lt;/code&gt; hands you output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Fedora-Workstation"&lt;/span&gt;
&lt;span class="py"&gt;VMState&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"running"&lt;/span&gt;
&lt;span class="py"&gt;VMStateChangeTime&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"2026-07-08T00:00:00.000000000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at lines 2 and 3. &lt;code&gt;VMState&lt;/code&gt; and &lt;code&gt;VMStateChangeTime&lt;/code&gt;. One is the answer I want. The other is a timestamp that starts with the exact same eight characters.&lt;/p&gt;

&lt;p&gt;The filter is &lt;code&gt;$_ -like 'VMState=*'&lt;/code&gt;. The part carrying the weight there is the &lt;code&gt;=&lt;/code&gt;. &lt;code&gt;VMStateChangeTime&lt;/code&gt; starts with &lt;code&gt;VMState&lt;/code&gt;, but the next character is &lt;code&gt;C&lt;/code&gt;, not &lt;code&gt;=&lt;/code&gt;, so it never matches the pattern. If I'd been a little lazier and written &lt;code&gt;'VMState*'&lt;/code&gt;, both lines match, and then &lt;code&gt;Select-Object -First 1&lt;/code&gt; hands back whichever one happens to print first.&lt;/p&gt;

&lt;p&gt;I had never tested any of this. The function has been in the resume path and the install-wait loop the whole time, working fine, completely unexercised. That's the part that got under my skin. It works because of one character in a wildcard, and nothing in the repo would tell me if I ever broke it.&lt;/p&gt;

&lt;p&gt;So this week I wrote the tests. The one I actually care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;It&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does not pick up VMStateChangeTime instead of VMState"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Mock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Invoke-VBoxManage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s1"&gt;'VMStateChangeTime="2026-07-08T00:00:00.000000000"'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;`n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'VMState="running"'&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Get-VMState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-VBoxManage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vbox"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-VMName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vm"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Should&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"running"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put &lt;code&gt;VMStateChangeTime&lt;/code&gt; first on purpose. If the match ever loosens to a prefix, &lt;code&gt;Select-Object -First 1&lt;/code&gt; grabs the timestamp line, the function returns a date string instead of &lt;code&gt;running&lt;/code&gt; or &lt;code&gt;poweroff&lt;/code&gt;, and the wait loop has no idea the install ever finished. It would just sit there until the 90-minute timeout gives up. Ordering the mock wrong-side-up is the whole point of the test.&lt;/p&gt;

&lt;p&gt;There's a second small thing in that return line I'm glad I pinned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Split("=", 2)&lt;/code&gt; splits on the first &lt;code&gt;=&lt;/code&gt; only and stops. If a value ever contained an &lt;code&gt;=&lt;/code&gt;, splitting on all of them and taking &lt;code&gt;[1]&lt;/code&gt; would chop the value in half. VirtualBox's state values don't have equals signs today, but the two-argument split costs nothing and means I don't have to keep that assumption in my head. &lt;code&gt;Trim('"')&lt;/code&gt; strips the quotes VBoxManage wraps around everything.&lt;/p&gt;

&lt;p&gt;The rest of the tests cover the dull cases that still matter: quotes get stripped, &lt;code&gt;poweroff&lt;/code&gt; reads back as &lt;code&gt;poweroff&lt;/code&gt;, a missing VMState line returns an empty string, and no output at all returns an empty string instead of throwing. Six tests. None of them found a bug.&lt;/p&gt;

&lt;p&gt;That's the honest bit. This wasn't a fix. The code was already right. I just had a function steering a loop that eventually detaches media and deletes a disk with a hashed password on it, and I had zero coverage saying that function stays right. The tests are there so the next time I "clean up" that filter, something goes red before an install quietly hangs for an hour and a half.&lt;/p&gt;

&lt;p&gt;Same commit added tests for two other spots that were one substring away from misbehaving: the artifact cleanup that deletes &lt;code&gt;ks.cfg&lt;/code&gt; and the OEMDRV disk after install, and the existing-VM check that has to match &lt;code&gt;Fedora-Workstation&lt;/code&gt; without also firing on &lt;code&gt;Fedora-Workstation-2&lt;/code&gt;. That last one leans on an anchored regex and &lt;code&gt;[regex]::Escape&lt;/code&gt; so a dot in a VM name stays a literal dot. Different post.&lt;/p&gt;

&lt;p&gt;Whole thing is here if you want to read it: &lt;a href="https://github.com/TiltedLunar123/Fedora-VirtualBox-Auto-Installer-PowerShell" rel="noopener noreferrer"&gt;https://github.com/TiltedLunar123/Fedora-VirtualBox-Auto-Installer-PowerShell&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works. The code I trust the least now is the one-liner I never reopen, the kind that's never once broken so I never look at it. This was one of those.&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>testing</category>
      <category>virtualbox</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Every insecure protocol on Security+ has a secure twin, and the port gives it away</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:26:39 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/every-insecure-protocol-on-security-has-a-secure-twin-and-the-port-gives-it-away-3c36</link>
      <guid>https://dev.to/tiltedlunar123/every-insecure-protocol-on-security-has-a-secure-twin-and-the-port-gives-it-away-3c36</guid>
      <description>&lt;p&gt;If you have studied for the SY0-701 for more than a week, you have hit a question that names a plaintext protocol and asks what you should use instead. FTP shows up in one question, Telnet in another. The four answer choices all look plausible. The trick is that the exam is not testing whether you know what FTP does. It wants the encrypted replacement, and usually the port that replacement runs on.&lt;/p&gt;

&lt;p&gt;Once you start seeing protocols in pairs, a whole category of questions stops being memorization and turns into pattern matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pairs worth knowing cold
&lt;/h2&gt;

&lt;p&gt;Here is the short list that keeps showing up. Left side is the plaintext version you are supposed to flag. Right side is what CompTIA wants you to reach for.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP (80) becomes HTTPS (443). Web traffic wrapped in TLS.&lt;/li&gt;
&lt;li&gt;FTP (20/21) becomes FTPS (implicit 990) or SFTP (22). Watch this one, more below.&lt;/li&gt;
&lt;li&gt;Telnet (23) becomes SSH (22). Remote shell, now encrypted.&lt;/li&gt;
&lt;li&gt;SMTP (25) becomes SMTP with STARTTLS (587) or SMTPS (465).&lt;/li&gt;
&lt;li&gt;LDAP (389) becomes LDAPS (636). Directory lookups over TLS.&lt;/li&gt;
&lt;li&gt;SNMPv1 and v2c (161) become SNMPv3 (161). Same port, the version is the upgrade.&lt;/li&gt;
&lt;li&gt;DNS (53) becomes DNS over TLS (853) or DNS over HTTPS (443).&lt;/li&gt;
&lt;li&gt;POP3 (110) becomes POP3S (995). IMAP (143) becomes IMAPS (993).&lt;/li&gt;
&lt;li&gt;Kerberos (88), RADIUS (1812/1813), and TACACS+ (49) round out the auth-adjacent set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need every obscure port for the exam. The ones above earn their keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two that trip everyone
&lt;/h2&gt;

&lt;p&gt;FTPS versus SFTP. Same protocol with the letters shuffled? Not even close. FTPS is ordinary FTP with a TLS layer bolted on, and it still uses FTP's command and data channel model. SFTP is a completely different protocol that rides inside SSH on port 22. If a question mentions SSH, key-based auth, or a single connection, it is pointing at SFTP. Certificates or a TLS handshake for file transfer? That is FTPS. Same goal, different plumbing.&lt;/p&gt;

&lt;p&gt;Then SNMPv3. Here the upgrade is invisible at the port layer. Authentication and encryption get added to the same service on 161, and the version number is the whole tell. Plenty of people expect the port to change and get baited by an answer choice that swaps the number. Do not fall for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the port is often the real answer
&lt;/h2&gt;

&lt;p&gt;A lot of SY0-701 questions hand you a scenario. It might be a captured packet or a firewall rule or a scan result, followed by a question about what is happening or what to allow. The port is the fingerprint: 636 leaving a domain controller is secure directory traffic, no problem; 389 where policy says everything must be encrypted is your finding. The exam rewards you for reading the number and knowing what it implies. Reciting a definition does not help here.&lt;/p&gt;

&lt;p&gt;This is also why "implement secure protocols" sits in Domain 3 rather than off in some trivia section. It ties into firewall rules and hardening, and into the logs you will be asked to interpret elsewhere on the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually learn these
&lt;/h2&gt;

&lt;p&gt;Do not sit and stare at a table. So what actually works? Three things.&lt;/p&gt;

&lt;p&gt;Build the pairs yourself from memory. Insecure on the left, secure on the right, port on both sides. Rebuild it cold every day for a week. The act of reconstructing it is what makes it stick, not rereading it.&lt;/p&gt;

&lt;p&gt;Turn each pair into a one-line scenario. "A junior admin left Telnet enabled on a switch, what do you recommend and on what port." Answer out loud. If you can write the question, you understand it.&lt;/p&gt;

&lt;p&gt;Drill mixed questions, not topic-sorted ones. Why does that matter? On the real exam these protocol items show up scattered between a risk-management question and an incident-response question, with no heading to warn you that the next one is about ports, so practicing them in a neat labeled block builds a fluency that evaporates the second the topics get shuffled. That is the trap.&lt;/p&gt;

&lt;p&gt;That last point is where a bank of practice questions earns its place. I built SecPlus Mastery around this kind of applied drilling, and the free diagnostic at &lt;a href="https://secplusmastery.com/diagnostic" rel="noopener noreferrer"&gt;https://secplusmastery.com/diagnostic&lt;/a&gt; will tell you fast whether protocol and port questions are a strength or a gap, before you spend a real exam attempt finding out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence version
&lt;/h2&gt;

&lt;p&gt;Learn protocols in insecure-secure pairs, memorize the port that changes or stays the same, and treat a port number in a scenario as a clue rather than decoration. Do that and a whole slice of the exam turns into recognition instead of recall.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>comptia</category>
      <category>learning</category>
    </item>
    <item>
      <title>At rest, in transit, in use: the data state Security+ students keep leaving unprotected</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Tue, 07 Jul 2026 23:14:18 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/at-rest-in-transit-in-use-the-data-state-security-students-keep-leaving-unprotected-ke1</link>
      <guid>https://dev.to/tiltedlunar123/at-rest-in-transit-in-use-the-data-state-security-students-keep-leaving-unprotected-ke1</guid>
      <description>&lt;p&gt;If you have studied any Security+ material, you already know two of the three data states cold. Data at rest is data sitting on a disk, a database, a backup tape, or a phone left in a coffee shop. Data in transit is data moving across a network, like a file upload or an API call. You protect the first with full disk encryption and database encryption. You protect the second with TLS, a VPN, or IPsec. Easy points on the exam.&lt;/p&gt;

&lt;p&gt;Then a question adds a third state, and the miss rate jumps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state people forget
&lt;/h2&gt;

&lt;p&gt;Data in use is data loaded into memory and actively being processed by an application or CPU. A spreadsheet open in RAM. Card numbers being validated during checkout. A decryption key sitting in memory so the app can actually read the file it just decrypted.&lt;/p&gt;

&lt;p&gt;Here is the part that catches people. Encryption at rest and encryption in transit do nothing for data in use. The whole point of processing data is that the CPU needs it in cleartext. Your database can be encrypted on disk, your connection can be wrapped in TLS, and the moment the server pulls a record into memory to work on it, that record is decrypted and exposed. Malware with the right access, a memory-scraping attack, or a compromised process can read it straight out of RAM. Point of sale RAM scrapers built entire breaches on exactly this gap.&lt;/p&gt;

&lt;p&gt;So when a question describes data being actively processed and asks how to protect it, "encrypt it at rest" is the trap answer. It is not wrong in general. It is wrong for that state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually protects data in use
&lt;/h2&gt;

&lt;p&gt;This is where the newer objectives want you to know a few terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Trusted Execution Environment (TEE), or secure enclave, carves out an isolated, hardware-protected region of the processor. Code and data inside the enclave stay protected even from the operating system and anyone with admin on the box.&lt;/li&gt;
&lt;li&gt;Homomorphic encryption lets you run computations on data while it stays encrypted, so the result comes out encrypted and the cleartext is never exposed during processing. It is slow and niche, but it is the textbook answer for "compute without decrypting."&lt;/li&gt;
&lt;li&gt;Supporting controls matter too: tight access control on the processes that touch the data, memory protections, and tokenization so the sensitive value is never sitting in memory in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to implement any of this. You need to recognize that "in use" is a distinct problem with its own set of answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to stop losing these points
&lt;/h2&gt;

&lt;p&gt;The exam rarely says "which state is this." It hides the state inside the scenario and expects you to classify it first, then pick the control. Train the reflex with three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the data sitting still? That is at rest. Think encryption on the disk or in the database.&lt;/li&gt;
&lt;li&gt;Is the data moving between two points? That is in transit. Think TLS, VPN, IPsec.&lt;/li&gt;
&lt;li&gt;Is the data being read, calculated on, or displayed right now? That is in use. Think TEE, secure enclave, and access control around the process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Match the state to the control and most of these questions answer themselves. The mistakes happen when people skip step one and jump straight to "encryption," which is correct for two states and useless for the third.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drill it until it is automatic
&lt;/h2&gt;

&lt;p&gt;Concepts like this stick when you see them in exam form over and over, not when you read them once. That is the idea behind &lt;a href="https://secplusmastery.com" rel="noopener noreferrer"&gt;SecPlus Mastery&lt;/a&gt;, the SY0-701 prep platform I built while studying for the exam myself. It has 1,069 practice questions, 31 reading lessons, hands-on labs and performance-based questions, and acronym flashcards, all mapped to the current objectives.&lt;/p&gt;

&lt;p&gt;If you want to see how well you actually classify these scenarios under pressure, the &lt;a href="https://secplusmastery.com/diagnostic" rel="noopener noreferrer"&gt;free diagnostic exam&lt;/a&gt; is a good gut check before you spend money on anything. It shows you which domains are solid and which ones, like the three data states, are quietly costing you points.&lt;/p&gt;

&lt;p&gt;Learn the three states, learn which control belongs to each, and the data protection questions stop being a coin flip.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>career</category>
      <category>learning</category>
    </item>
    <item>
      <title>Who signs what: a Security+ walkthrough of PKI and certificates</title>
      <dc:creator>TiltedLunar123</dc:creator>
      <pubDate>Tue, 07 Jul 2026 10:25:01 +0000</pubDate>
      <link>https://dev.to/tiltedlunar123/who-signs-what-a-security-walkthrough-of-pki-and-certificates-1m5p</link>
      <guid>https://dev.to/tiltedlunar123/who-signs-what-a-security-walkthrough-of-pki-and-certificates-1m5p</guid>
      <description>&lt;p&gt;PKI is one of those Security+ topics that feels simple until the exam hands you four answers that all use the same five words: key, sign, certificate, CA, and trust. Once you can say exactly who holds each key and who signs what, most PKI questions stop being tricky.&lt;/p&gt;

&lt;p&gt;Here is the whole flow, in the order it actually happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  It starts with a key pair you generate yourself
&lt;/h2&gt;

&lt;p&gt;Before any certificate exists, you generate a key pair on your own server: a private key and a matching public key. The private key stays on that server and never leaves it. Write that down, because it is the single fact most missed questions hinge on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CSR carries your public key, not your private key
&lt;/h2&gt;

&lt;p&gt;To get a certificate, you create a Certificate Signing Request (CSR). The CSR contains your public key and your identity details like the domain name and organization. You sign the CSR with your private key to prove you actually hold it, but the private key itself is not in the request. If an answer choice says you send your private key to the CA, it is wrong every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CA signs, it does not encrypt
&lt;/h2&gt;

&lt;p&gt;The Certificate Authority checks your request, then issues a certificate. Here is the part people blur together: the CA takes a hash of the certificate and signs that hash with the CA's own private key. It is not encrypting your data and it is not touching your private key. Anyone can then verify that signature using the CA's public key, which is baked into the certificate chain. Signing proves who issued the cert and that it has not been altered. That is integrity and authenticity, not confidentiality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chain of trust: leaf, intermediate, root
&lt;/h2&gt;

&lt;p&gt;Your certificate, the leaf, is signed by an intermediate CA, which is signed by a root CA. Your device already trusts the root because it ships in the operating system or browser trust store. The chain lets a client walk from your cert up to a root it already trusts.&lt;/p&gt;

&lt;p&gt;This is also a very common real-world failure. If a server only sends its leaf certificate and forgets the intermediate, some clients throw an "unable to verify" error even though nothing is expired or revoked. The cert is fine. The chain is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revocation: killing a cert before it expires
&lt;/h2&gt;

&lt;p&gt;Say a private key gets stolen. You cannot wait for the certificate to expire on its own, so you revoke it. Two mechanisms show up on the exam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CRL (Certificate Revocation List) is a signed list of revoked serial numbers the CA publishes on a schedule. It works, but it can be large and a little stale between updates.&lt;/li&gt;
&lt;li&gt;OCSP (Online Certificate Status Protocol) lets a client ask about one specific certificate in real time, which is faster and lighter than downloading a whole list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one more worth knowing: OCSP stapling. Instead of every client hitting the CA, the web server itself fetches a recent, timestamped OCSP response and staples it to the TLS handshake. Better privacy, less load on the CA. If a question mentions the server presenting proof of validity during the handshake, that is stapling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The distinctions the exam loves
&lt;/h2&gt;

&lt;p&gt;A few more that separate a pass from a miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A self-signed certificate is signed by its own key, so no external CA vouches for it. It encrypts traffic fine, but nothing outside trusts it by default. Good for internal or test use, not the public web.&lt;/li&gt;
&lt;li&gt;A wildcard certificate (*.example.com) covers all subdomains at one level. A SAN certificate lists specific names. If the question needs several different domains on one cert, that is SAN, not wildcard.&lt;/li&gt;
&lt;li&gt;Certificate pinning means the client only accepts one specific certificate or key it already knows, which stops an attacker who somehow got a valid cert from a different CA.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to study this without memorizing blindly
&lt;/h2&gt;

&lt;p&gt;The trap in almost every PKI question is a role mixup: which key is private, who does the signing, what the CSR carries, and whether revocation or expiration is doing the work. When you read a question, name the actor and the key before you look at the options. If you can say "the CA signs a hash with its private key, and the client verifies with the CA's public key" out loud, you have already answered half the domain.&lt;/p&gt;

&lt;p&gt;If you want to drill this until it is automatic, I built a practice platform at &lt;a href="https://secplusmastery.com" rel="noopener noreferrer"&gt;secplusmastery.com&lt;/a&gt; with 1,069 practice questions, 31 reading lessons, hands-on labs, and PBQs aimed at exactly these SY0-701 traps. There is a free diagnostic at &lt;a href="https://secplusmastery.com/diagnostic" rel="noopener noreferrer"&gt;secplusmastery.com/diagnostic&lt;/a&gt; if you want to see which domains you are actually weak in before you spend hours reviewing.&lt;/p&gt;

&lt;p&gt;Learn the roles once and PKI turns from a guessing game into free points.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>career</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
