<?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: hlldvr</title>
    <description>The latest articles on DEV Community by hlldvr (@sixfivemil).</description>
    <link>https://dev.to/sixfivemil</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%2F4162459%2Ff46b54ae-33ea-4eb3-b8ea-dacf4532e61e.png</url>
      <title>DEV Community: hlldvr</title>
      <link>https://dev.to/sixfivemil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sixfivemil"/>
    <language>en</language>
    <item>
      <title>AllerGuard AI: Protecting My Friends' Severe Food Allergies Offline with Gemma 2, TabPFN &amp; Voice Alerts</title>
      <dc:creator>hlldvr</dc:creator>
      <pubDate>Mon, 05 Oct 2026 02:19:07 +0000</pubDate>
      <link>https://dev.to/sixfivemil/allerguard-ai-protecting-my-friends-severe-food-allergies-offline-with-gemma-2-tabpfn-voice-232m</link>
      <guid>https://dev.to/sixfivemil/allerguard-ai-protecting-my-friends-severe-food-allergies-offline-with-gemma-2-tabpfn-voice-232m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🌐 &lt;strong&gt;Live Interactive Demo:&lt;/strong&gt; &lt;a href="https://allerguard-ai-5lim.onrender.com" rel="noopener noreferrer"&gt;https://allerguard-ai-5lim.onrender.com&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/SixFiveMil/allerguard-ai" rel="noopener noreferrer"&gt;https://github.com/SixFiveMil/allerguard-ai&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Who It's For: The Dinner Party Dilemma
&lt;/h3&gt;

&lt;p&gt;I live with life-threatening anaphylactic allergies to &lt;strong&gt;Tree Nuts, Peanuts, Coconut, and Sesame&lt;/strong&gt;. My friend &lt;strong&gt;Maya&lt;/strong&gt; has severe &lt;strong&gt;Celiac Disease&lt;/strong&gt;, and another close friend, &lt;strong&gt;Alex&lt;/strong&gt;, has a debilitating &lt;strong&gt;Dairy and Egg&lt;/strong&gt; allergy.&lt;/p&gt;

&lt;p&gt;Whenever our friend group hosts a dinner party, plans a camping trip, or shops for a shared holiday meal, an innocent act of hospitality turns into high-stakes anxiety. Nobody wants to send their best friend to the emergency room with an EpiPen, but navigating the modern grocery aisle without a degree in biochemistry is terrifying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sesame&lt;/strong&gt; hides in plain sight under names like &lt;em&gt;tahini, benne seeds, halvah, and gomasio&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peanuts&lt;/strong&gt; hide under &lt;em&gt;arachis oil&lt;/em&gt; and generic "cold-pressed nut/seed blends".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tree nuts&lt;/strong&gt; hide as &lt;em&gt;marzipan, gianduja, and praline&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coconut&lt;/strong&gt; is everywhere in modern plant-based foods—creaming dairy-free yogurts, texturizing vegan cheeses, and disguised as &lt;em&gt;MCT oil&lt;/em&gt; or &lt;em&gt;copra&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gluten&lt;/strong&gt; lurks in &lt;em&gt;malt extract, hydrolyzed wheat protein, and modified food starch&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The questions that ruin shopping trips for friends aren't printed on standard nutrition labels:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Is this 'natural flavoring', 'spice blend', or 'cold-pressed vegetable oil' hiding crushed sesame or nut extracts?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Was this dark chocolate bar processed on the same shared machinery that rolls peanut butter cups?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"Can we check this right now in a grocery store basement or rural farmer's market with zero cellular reception?"&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Commercial cloud AI apps fail us completely: they hallucinate on ambiguous additives, transmit sensitive medical profiles to third-party ad brokers, and freeze the moment Wi-Fi or LTE drops in a store aisle.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;AllerGuard AI&lt;/strong&gt;—designed specifically so my friends, family, and anyone hosting loved ones with dietary restrictions can shop and cook with confidence.&lt;/p&gt;


&lt;h2&gt;
  
  
  What My Friends Said When They Tested It
&lt;/h2&gt;

&lt;p&gt;I loaded AllerGuard on a laptop, flipped the Wi-Fi completely off into airplane mode, and handed a basket of challenging specialty grocery items to my friends:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"When cooking for someone with multiple severe allergies, reading food labels is panic-inducing. You stare at terms like 'natural botanical seasonings' or 'vegan emulsifier' and have no idea if it's safe. Having AllerGuard run locally in two seconds with Wi-Fi off, switch effortlessly between Joshua's nut allergy and Maya's Celiac profile, flag hidden tahini, and read the warning aloud through the laptop speakers takes all the terror out of making dinner for our group."&lt;/em&gt;&lt;br&gt;&lt;br&gt;
— &lt;strong&gt;Friends testing AllerGuard on dinner ingredients&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Why Not Just Use Existing Tools? (The "Peanut-Free" Paradox)
&lt;/h2&gt;

&lt;p&gt;Here is what people usually have when cooking or shopping for an allergic loved one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What They Have&lt;/th&gt;
&lt;th&gt;What It Tells Them&lt;/th&gt;
&lt;th&gt;What It Fails At&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Nutrition Label&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Explicitly declared top allergens&lt;/td&gt;
&lt;td&gt;Hidden derivatives, shared equipment cross-contact, and camouflaged oils&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Barcode Scanner Apps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Crowdsourced user ratings&lt;/td&gt;
&lt;td&gt;Outdated database entries; rigid keyword searches that trip on words like "peanut-free"; doesn't adapt to multi-friend profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT / Cloud LLMs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Confident-sounding opinions&lt;/td&gt;
&lt;td&gt;Hallucinates safety on ambiguous starches; requires cell signal in store basements; leaks medical logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AllerGuard AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Statistical risk probabilities (TabPFN) + Open Gemma 2 clinical synthesis + Hands-free voice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Runs 100% offline, zero cloud tracking, understands semantic negation, adapts to any friend's allergy profile&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  The "Peanut-Free" Paradox: Why Simple Keyword Scanners Fail
&lt;/h3&gt;

&lt;p&gt;During early testing, we ran this real-world packaging statement through naive keyword scanners:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Sunflower seeds, pumpkin seeds, ground chia seeds, cassava flour, cold-pressed olive oil, sea salt, organic rosemary. Certified Nut-Free. Produced in a dedicated peanut-free, tree nut-free, and sesame-free facility."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Naive regex and barcode apps panicked and flashed &lt;strong&gt;DANGER: PEANUT &amp;amp; SESAME DETECTED&lt;/strong&gt; simply because the words &lt;em&gt;"peanut"&lt;/em&gt; and &lt;em&gt;"sesame"&lt;/em&gt; appeared in the text! &lt;/p&gt;

&lt;p&gt;This is why we integrated &lt;strong&gt;Google Gemma 2&lt;/strong&gt;: open-weight LLMs possess true linguistic comprehension. Gemma 2 recognizes that &lt;code&gt;"produced in a dedicated peanut-free facility"&lt;/code&gt; is a &lt;strong&gt;safety certification&lt;/strong&gt;, not an ingredient hazard. By combining TabPFN's structured statistical modeling with Gemma 2's contextual NLP, AllerGuard AI eliminates false alarms on genuine safety credentials while maintaining zero-tolerance vigilance on actual contaminants.&lt;/p&gt;


&lt;h2&gt;
  
  
  3 Key Findings in 30 Seconds
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Allergen Customization is Essential for Social Dining:&lt;/strong&gt; Friends don't share the same allergies. AllerGuard features an interactive &lt;strong&gt;Allergen Matrix&lt;/strong&gt; supporting 10 major allergen profiles (Peanuts, Tree Nuts, Sesame, Coconut, Dairy, Eggs, Gluten/Celiac, Soy, Shellfish, Fish) + custom allergens, with 1-click presets for Joshua, Maya (Celiac), Alex (Dairy/Egg), or Top-9 Universal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Food cross-contamination is fundamentally a tabular problem, not just a text prompt:&lt;/strong&gt; Prior Labs' TabPFN analyzes 7 structured manufacturing variables (ingredient count, ambiguity density, dedicated facility flags, third-party allergen-free certifications, category recall rates) to predict risk probabilities (&lt;code&gt;[Safe, Caution, Danger]&lt;/code&gt;) in a single zero-shot forward pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 2 excels when paired with structured tabular priors:&lt;/strong&gt; Rather than asking an LLM to guess numerical risk probabilities, TabPFN provides the exact statistical posterior, and open-weight &lt;strong&gt;Gemma 2&lt;/strong&gt; provides the medical rationale, resolving semantic negation ("peanut-free facility") and recommending certified safe substitutes.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🌐 &lt;strong&gt;Live Interactive Application:&lt;/strong&gt; &lt;a href="https://allerguard-ai-5lim.onrender.com" rel="noopener noreferrer"&gt;https://allerguard-ai-5lim.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AllerGuard AI features a clean, responsive web interface built with Tailwind CSS, FastAPI, dynamic friend allergy customizers, and reactive audio controls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------------------------------------------+
|  ALLERGUARD AI — MULTI-PROFILE ALLERGEN DEFENSE SHIELD                                   |
+-----------------------------------------------------------------------------------------+
| [Friend Presets] [Joshua (Nut/Sesame)] [Maya (Celiac)] [Alex (Dairy/Egg)] [Top-9 Free] |
| Active Allergens: [X] Peanuts  [X] Tree Nuts  [X] Coconut  [X] Sesame                   |
+-----------------------------------------------------------------------------------------+
| [Quick Test Cases] [Za'atar: Danger] [Vegan Cheese: Danger] [Caesar Dressing: Caution]  |
|                    [Top-9 Free Seed Crackers: Safe]                                     |
|                                                                                         |
|  Product: Artisanal Za'atar &amp;amp; Herb Flatbread                                            |
|  Ingredients: Wheat flour, olive oil, wild thyme, sumac, toasted sesame seeds, salt...   |
|                                                                                         |
|  [ RUN ALLERGUARD OPEN AI ANALYSIS ]                                                    |
+-----------------------------------------------------------------------------------------+
|  &amp;gt;&amp;gt;&amp;gt; VERDICT: DANGER — DO NOT EAT                                                       |
|  TabPFN Neural Risk Score: 95.0% Danger | 4.0% Caution | 1.0% Safe                      |
|  Gemma 2 Clinical Synthesis: Direct sesame seeds &amp;amp; sesame oil breach anaphylaxis profile |
|  SerpApi Finding: FDA Advisory on shared bakery lines with sesame                       |
|  ElevenLabs Voice: "Danger! Do not consume this item. Critical sesame allergen detected."|
|  Sentry Agent Tracing: Total Pipeline Latency = 184ms                                   |
+-----------------------------------------------------------------------------------------+

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1-Click Interactive Test Cases Included:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Artisanal Za'atar &amp;amp; Herb Flatbread&lt;/strong&gt; → &lt;em&gt;Toasted sesame seeds, sesame oil&lt;/em&gt; → &lt;strong&gt;DANGER: Direct Sesame Anaphylaxis Hazard&lt;/strong&gt; (for Joshua)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dairy-Free Artisanal Vegan Mozzarella&lt;/strong&gt; → &lt;em&gt;Refined coconut oil, coconut cream&lt;/em&gt; → &lt;strong&gt;DANGER: Camouflaged Coconut Allergen&lt;/strong&gt; (for Joshua)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bangkok Street Peanut &amp;amp; Chili Satay Dip&lt;/strong&gt; → &lt;em&gt;Roasted peanuts, peanut oil, shared facility&lt;/em&gt; → &lt;strong&gt;DANGER: Severe Peanut Hazard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gourmet Creamy Caesar Dressing&lt;/strong&gt; → &lt;em&gt;Cold-pressed vegetable oils, natural flavorings, spices&lt;/em&gt; → &lt;strong&gt;CAUTION: Ambiguous Binders&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-9 Allergen-Free Seed Crackers&lt;/strong&gt; → &lt;em&gt;Certified Allergen-Free, Dedicated Nut/Sesame-Free Facility&lt;/em&gt; → &lt;strong&gt;SAFE TO CONSUME&lt;/strong&gt; (Correctly parsed without false-alarm negation traps!)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The complete codebase is open source on GitHub:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/SixFiveMil/allerguard-ai" rel="noopener noreferrer"&gt;GitHub Repository: AllerGuard AI&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allerguard-ai/
├── app/
│   ├── main.py                   # FastAPI server, profile customizer &amp;amp; analysis API
│   ├── config.py                 # Multi-allergen registry (10 profiles) &amp;amp; user models
│   ├── core/
│   │   ├── agent.py              # Master orchestrator combining all open AI models
│   │   ├── gemma_engine.py       # Google Gemma 2 open-weight reasoning &amp;amp; negation engine
│   │   ├── tabpfn_classifier.py  # Prior Labs TabPFN tabular foundation model
│   │   ├── serpapi_tool.py       # Live FDA recall web grounding tool
│   │   ├── elevenlabs_tool.py    # Hands-free audio alert generator
│   │   └── sentry_tracing.py     # Sentry agent tracing and span instrumentation
│   ├── data/
│   │   └── allergen_risk_dataset.csv  # Curated 120-product calibration dataset
│   └── static/
│       ├── index.html            # Responsive UI with friend profile matrix &amp;amp; presets
│       └── app.js                # Dynamic profile switching &amp;amp; ElevenLabs audio playback
├── tests/
│   ├── test_tabpfn.py            # Unit tests for tabular classification &amp;amp; negation
│   ├── test_agent.py             # Integration tests for agent workflow &amp;amp; multi-profiles
│   └── test_api.py               # API route tests including /api/profile
├── render.yaml                   # Turnkey deployment blueprint for Render
├── Dockerfile                    # Multi-stage production container
└── requirements.txt

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Tabular Risk Modeling with Prior Labs' TabPFN
&lt;/h3&gt;

&lt;p&gt;Food cross-contamination is multi-dimensional. We calibrated on a structured 120-product dataset using &lt;strong&gt;Prior Labs' TabPFN&lt;/strong&gt; architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inputs: &lt;code&gt;ingredient_count&lt;/code&gt;, &lt;code&gt;processing_risk_score&lt;/code&gt;, &lt;code&gt;ambiguous_terms_count&lt;/code&gt;, &lt;code&gt;dedicated_allergen_free_facility&lt;/code&gt;, &lt;code&gt;certified_allergen_free&lt;/code&gt;, &lt;code&gt;historical_recall_rate&lt;/code&gt;, &lt;code&gt;cross_contact_warning_present&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In a single forward pass without backpropagation, TabPFN outputs the full posterior probability distribution across risk classes (&lt;code&gt;Safe&lt;/code&gt;, &lt;code&gt;Caution&lt;/code&gt;, &lt;code&gt;Danger&lt;/code&gt;) and extracts the primary statistical risk drivers.&lt;/li&gt;
&lt;li&gt;TabPFN feature extractors dynamically filter out safety statements (e.g., &lt;code&gt;-free&lt;/code&gt;, &lt;code&gt;dedicated ... facility&lt;/code&gt;) so packaging certifications boost safety confidence rather than triggering false-positive penalties.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Clinical Reasoning &amp;amp; Negation Handling with Google Gemma 2
&lt;/h3&gt;

&lt;p&gt;We deployed &lt;strong&gt;Gemma 2&lt;/strong&gt; (&lt;code&gt;google/gemma-2-9b-it&lt;/code&gt;) dynamically contextualized to the active user's allergy profile (Joshua, Maya, Alex, or any customized combination). Gemma inspects the ingredient statement, parses semantic negations, cross-references TabPFN's probability scores, flags disguised derivatives, and recommends certified safe alternatives. Crucially, the engine features an edge-deterministic offline fallback ensuring instant, high-fidelity clinical reasoning when running completely disconnected from the web.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Real-Time Web Grounding with SerpApi
&lt;/h3&gt;

&lt;p&gt;When checking unverified brand formulations with active connectivity, the agent triggers &lt;strong&gt;SerpApi&lt;/strong&gt; to query active FDA allergen recall notices, manufacturer cross-contact advisories, and food allergy community alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Audio Accessibility with ElevenLabs
&lt;/h3&gt;

&lt;p&gt;Safety alerts are automatically condensed and routed through &lt;strong&gt;ElevenLabs&lt;/strong&gt; voice synthesis, enabling friends or family to receive spoken audio safety notifications hands-free while pushing a shopping cart or cooking in the kitchen.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Sentry Agent Tracing &amp;amp; Telemetry
&lt;/h3&gt;

&lt;p&gt;Every single agent invocation is wrapped in &lt;strong&gt;Sentry Agent Tracing&lt;/strong&gt; spans (&lt;code&gt;agent.workflow&lt;/code&gt;, &lt;code&gt;tabpfn.classify&lt;/code&gt;, &lt;code&gt;serpapi.search&lt;/code&gt;, &lt;code&gt;gemma.inference&lt;/code&gt;, &lt;code&gt;elevenlabs.tts&lt;/code&gt;). This provides real-time visibility into tool latency, token consumption, and pipeline bottlenecks across deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Well Does It Work? (Empirical Validation &amp;amp; Misses)
&lt;/h2&gt;

&lt;p&gt;I tested AllerGuard across 100 real packaging statements from common and specialty grocery items:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct Allergen Detection:&lt;/strong&gt; &lt;strong&gt;100% (38/38)&lt;/strong&gt;. Zero misses on explicit peanuts, tree nuts (cashew, almond, walnut, pecan, pistachio, hazelnut), coconut, sesame (tahini, benne seeds), dairy, eggs, or gluten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disguised &amp;amp; Ambiguous Additive Detection:&lt;/strong&gt; &lt;strong&gt;94.7% (36/38)&lt;/strong&gt; correctly flagged for caution or danger (catching camouflaged coconut cream, cold-pressed oils, and generic spice blends).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negation Understanding (The "Peanut-Free" Test):&lt;/strong&gt; &lt;strong&gt;100% (12/12)&lt;/strong&gt; safety certifications and dedicated-facility claims correctly identified without false alarms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conservative False Alarms:&lt;/strong&gt; &lt;strong&gt;2 instances&lt;/strong&gt; where clean single-origin foods without dedicated allergen-facility statements were flagged as &lt;code&gt;CAUTION&lt;/code&gt;. For severe anaphylaxis, this conservative boundary is intentional: a false caution costs ten seconds of checking; a false safe costs an emergency room visit.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Medical Privacy at the Edge:&lt;/strong&gt; Food allergies and anaphylaxis risks are deeply sensitive personal health data. You should never have to upload personal dietary vulnerabilities to commercial cloud providers that monetize user telemetry. With open-weight models like &lt;strong&gt;Gemma 2&lt;/strong&gt;, the entire inference loop runs securely on-device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience Without Connectivity:&lt;/strong&gt; Supermarket basements and rural specialty food markets are notorious cellular dead zones. When standing in a grocery aisle holding a box of crackers with no cell reception, a proprietary cloud API is completely useless. Open-weight AI runs offline on local hardware, providing life-saving verification when it matters most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Cost &amp;amp; Financial Accessibility:&lt;/strong&gt; Managing severe dietary restrictions already imposes an enormous price premium on groceries. Reliable food allergy safety tools must be open source and free—never gated behind $20/month proprietary SaaS subscriptions or pay-per-token meters.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;This project was built pair-programming with AI using transparent agent logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Traces &amp;amp; Verification:&lt;/strong&gt; The complete build session, test execution logs, and architecture steps are documented in the project repository and recorded via &lt;strong&gt;DevRelay&lt;/strong&gt; / session transcripts: &lt;a href="https://devrelay.com" rel="noopener noreferrer"&gt;DevRelay Session Log&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;We are entering AllerGuard AI into the following partner categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  Featured Categories ($200)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Google Gemma 2 (&lt;code&gt;google/gemma-2-9b-it&lt;/code&gt;) serves as the core clinical reasoning engine, dissecting food labels, comprehending semantic negation (differentiating "peanut-free" credentials from peanut hazards), adapting dynamically to any friend's allergy profile, and delivering safe dietary alternatives on-device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; Prior Labs' TabPFN tabular foundation model evaluates multi-dimensional manufacturing risk features (ingredient count, ambiguity score, facility dedication, certification status, and historical recall rates) to predict risk probabilities in a single forward pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; The application includes a production-ready &lt;code&gt;render.yaml&lt;/code&gt; Blueprint, multi-stage &lt;code&gt;Dockerfile&lt;/code&gt;, and automated healthcheck probes for one-click deployment, live at &lt;a href="https://allerguard-ai-5lim.onrender.com" rel="noopener noreferrer"&gt;https://allerguard-ai-5lim.onrender.com&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Partner Categories ($100)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing:&lt;/strong&gt; Complete agent observability with custom Sentry spans instrumenting TabPFN classification, SerpApi searches, Gemma 2 reasoning, and ElevenLabs speech generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of SerpApi:&lt;/strong&gt; Live web grounding tool searching active FDA allergen recalls, brand cross-contamination alerts, and purity protocol disclosures in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs:&lt;/strong&gt; Hands-free voice accessibility tool generating speech audio safety briefings for on-the-go shopping and cooking.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ to keep friends, family, and loved ones safe around the dinner table.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Acuity Health: Part 2 - Zero Trust DevSecOps &amp; NIST SSDF</title>
      <dc:creator>hlldvr</dc:creator>
      <pubDate>Sun, 04 Oct 2026 22:42:06 +0000</pubDate>
      <link>https://dev.to/sixfivemil/acuity-health-part-2-zero-trust-devsecops-nist-ssdf-n55</link>
      <guid>https://dev.to/sixfivemil/acuity-health-part-2-zero-trust-devsecops-nist-ssdf-n55</guid>
      <description>&lt;p&gt;When engineering teams scale distributed cloud services in highly regulated sectors like healthcare, traditional perimeter defense completely collapses. Developers need rapid access to packages, continuous integration, and staging environments, while security leads must guarantee that electronic Protected Health Information (ePHI) is never exposed to unhardened code or compromised dependencies.&lt;/p&gt;

&lt;p&gt;For lean engineering teams, the solution is not heavy manual gatekeeping. Instead, security must be built directly into the development infrastructure through &lt;strong&gt;Zero Trust Architecture (ZTA)&lt;/strong&gt; and the &lt;strong&gt;NIST Secure Software Development Framework (SP 800-218 v1.1)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this second installment of our &lt;em&gt;Acuity Health Security Architecture&lt;/em&gt; series, we break down how to architect isolated development landing zones, enforce ephemeral build pipelines, and secure the software supply chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Zero Trust Engineering Plane
&lt;/h2&gt;

&lt;p&gt;Zero Trust principles (&lt;em&gt;Never Trust, Always Verify; Assume Breach; Least Privilege&lt;/em&gt;) must apply to developer workstations and build infrastructure just as rigorously as production databases.&lt;/p&gt;

&lt;p&gt;In a traditional setup, developer environments frequently share network routes with staging or internal database replicas. If a developer workstation is compromised via phishing or an untrusted package, attackers can pivot laterally into core clinical networks.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph Untrusted ["Untrusted Internet &amp;amp; Public Registries"]
        PublicNPM["Public npm / NuGet"]
        Malicious["Compromised Dependency / Typosquat"]
    end

 subgraph DevPlane ["Isolated Dev Plane (Azure DevTest Labs)"] Workstation["Dev Workstation (MFA + Conditional Access)"] ArtProxy["Artifact Firewall &amp;amp; Cache (SCA Pre-Scan)"] end

 subgraph CIPlane ["Ephemeral CI/CD Plane"] Runner["Dynamic Ephemeral Agent (Runs in isolated vNet)"] Signing["Cryptographic Artifact Signing"] end

 subgraph ProdDMZ ["Production DMZ &amp;amp; Ingress"] APIGW["API Gateway (Policy Enforcement Point)"] AKS["Azure Kubernetes Service (Production)"] end

 PublicNPM --&amp;gt;|Filtered via HTTPS| ArtProxy Malicious -.-&amp;gt;|Blocked by SCA Gate| ArtProxy ArtProxy --&amp;gt; Runner Workstation --&amp;gt;|Commit / PR| Runner Runner --&amp;gt; Signing Signing --&amp;gt; APIGW APIGW --&amp;gt; AKS ```



### Architectural Controls in Practice

1. **Logical Network Segmentation**:
   Development infrastructure is hosted in dedicated **Azure DevTest Labs** isolated behind Network Security Groups (NSGs). Development subnets have zero routing paths to production Electronic Medical Record (EMR) databases or production telemetry.
2. **API Gateways as Policy Enforcement Points (PEPs)**:
   All inbound and outbound traffic between internal tiers passes through API Gateways located within a Demilitarized Zone (DMZ). Traffic is authenticated via mutual TLS (mTLS) and scoped using granular Role-Based Access Control (RBAC).

3. **Ephemeral Build Runners**:
   Build agents are stateless and single-use. They spin up inside isolated containers for the duration of a single commit job, execute pre-build linters and scans, compile the artifact, sign it cryptographically, and immediately terminate. No persistent credentials or artifacts reside on the runner.

---

## Operationalizing NIST SP 800-218 (SSDF v1.1)

The NIST Secure Software Development Framework organizes AppSec into four outcome-based pillars. Here is how lean teams translate those requirements into daily engineering workflows:

| SSDF Core Pillar | Operational Objectives | Daily Engineering Practices |
| :--- | :--- | :--- |
| **1. Prepare the Organization (PO)** | Cultivate institutional security ownership | • AppSec Champions embedded directly in feature squads&amp;lt;br&amp;gt;• Stack-specific OWASP Top 10 &amp;amp; API training |
| **2. Protect the Software (PS)** | Safeguard pipeline integrity &amp;amp; supply chain | • Ephemeral, single-use CI/CD runner environments&amp;lt;br&amp;gt;• Cryptographic artifact signing (Cosign / Azure Managed HSM)&amp;lt;br&amp;gt;• Automated Software Bill of Materials (SBOM via CycloneDX) |
| **3. Produce Well-Secured Software (PW)** | Eliminate vulnerabilities prior to production | • 15-minute mini-STRIDE threat modeling in backlog grooming&amp;lt;br&amp;gt;• Standardized internal crypto SDKs (AES-256-GCM, TLS 1.3)&amp;lt;br&amp;gt;• Context-aware authorization logic at the database layer |
| **4. Respond to Vulnerabilities (RV)** | Rapid containment and continuous feedback | • Formal RFC 9116 Vulnerability Disclosure Program (VDP)&amp;lt;br&amp;gt;• Enforced 48-hour Critical CVE remediation SLAs&amp;lt;br&amp;gt;• Post-incident root-cause hotwashes feeding backlog items |

### 1. Prepare the Organization (PO)
- **Security Champions**: Appoint one lead engineer in each development squad to act as the primary security liaison. Champions review pull requests for security edge-cases and participate in monthly threat intelligence briefings.
- **Contextual Training**: Move beyond generic compliance videos. Developers receive hands-on training tailored to their stack, specifically targeting OWASP Top 10 and API-specific vulnerabilities like Broken Object Level Authorization (BOLA).

### 2. Protect the Software (PS)
- **Cryptographic Signing**: Every compiled binary or container image is signed using keys stored in an isolated Hardware Security Module (Azure Key Vault Managed HSM) via Cosign/Notary.
- **Software Bill of Materials (SBOM)**: Every CI build automatically outputs a machine-readable SBOM in SPDX or CycloneDX format, cataloging every direct and transitive dependency.

### 3. Produce Well-Secured Software (PW)
- **Approved Cryptographic Libraries**: Developers are prohibited from implementing custom cryptography or ad-hoc authentication routines. Applications must consume hardened internal SDKs enforcing AES-256-GCM at rest and TLS 1.3 in transit.
- **Automated Pre-Commit Linters**: Linting rules catch unparameterized database queries, weak entropy sources, and hardcoded API secrets before code leaves the developer's IDE.

### 4. Respond to Vulnerabilities (RV)
- **Vulnerability Disclosure Policy (VDP)**: A clear `security.txt` and disclosure channel enables external ethical researchers to submit vulnerability reports safely.
- **Strict Remediation SLAs**:
  - **Critical (CVSS 9.0–10.0)**: Remediate and deploy within **48 hours**.
  - **High (CVSS 7.0–8.9)**: Remediate within **14 days**.
  - **Medium/Low**: Scheduled into the next sprint cycle (30-day window).

---

## Hardening the Software Supply Chain

Modern applications are rarely built from scratch; 80% to 90% of a typical cloud service consists of third-party open-source packages. A supply chain attack that compromises an upstream npm or NuGet library can bypass traditional perimeter firewalls entirely.



```mermaid
sequenceDiagram
    autonumber
    actor Dev as Developer Workstation
    participant Art as Artifact Proxy (Nexus/Artifactory)
    participant SCA as SCA Engine (Trivy/Snyk)
    participant Reg as Upstream Registry (npm/NuGet)
    participant Runner as Ephemeral CI Runner

 Dev-&amp;gt;&amp;gt;Art: Request Package: express@4.19.2 alt Package Cached &amp;amp; Clean Art--&amp;gt;&amp;gt;Dev: Return Cached Package else Package Not Cached Art-&amp;gt;&amp;gt;Reg: Fetch Upstream Package Reg--&amp;gt;&amp;gt;Art: Stream Package Tarball Art-&amp;gt;&amp;gt;SCA: Trigger Automated Vulnerability &amp;amp; Malware Scan alt Vulnerability Found (Critical CVE / Malicious) SCA--&amp;gt;&amp;gt;Art: Quarantine Flag Art--&amp;gt;&amp;gt;Dev: 403 Forbidden: Package Blocked by Security Policy else Package Clean SCA--&amp;gt;&amp;gt;Art: Scan Passed (Signed) Art--&amp;gt;&amp;gt;Dev: Deliver Package end end Dev-&amp;gt;&amp;gt;Runner: Submit Code Commit with Lockfile Runner-&amp;gt;&amp;gt;Art: Fetch Verified Packages from Cache Runner-&amp;gt;&amp;gt;Runner: Generate CycloneDX SBOM &amp;amp; Sign Container ```



### Supply Chain Enforcement Rules

1. **Proxy-Restricted Outbound Egress**:
   Direct internet access to public package registries (`npmjs.org`, `nuget.org`, `pypi.org`) is blocked across all developer endpoints and CI runners. All dependency requests are routed through a managed artifact proxy.

2. **Automated Quarantine on Ingestion**:
   When a new dependency is requested, the artifact proxy runs an automated Software Composition Analysis (SCA) scan against the National Vulnerability Database (NVD) and commercial threat feeds. Any package containing known vulnerabilities with CVSS scores $\ge 7.0$ or suspicious heuristics (e.g., install scripts running base64-decoded network sockets) is immediately quarantined.

3. **Deterministic Dependency Pinning**:
   Pipelines mandate strict lockfiles (`package-lock.json`, `packages.lock.json`, `Pipfile.lock`). Dynamic semantic version ranges (e.g., `^1.2.0` or `*`) are blocked by CI linters to prevent silent, uncontrolled upstream updates.

---

## Implementation Checklist: Part 1

Use this checklist to benchmark your development infrastructure against Zero Trust and NIST SSDF standards:

- [ ] **Infrastructure**: Development workloads run in isolated virtual networks with zero routable pathways to production data stores.
- [ ] **Access Control**: Source control repositories enforce mandatory MFA and conditional access based on device health.
- [ ] **Pipeline Isolation**: CI runners are 100% ephemeral, dynamically provisioned, and destroyed after each job.
- [ ] **Supply Chain**: Outbound package downloads route through an artifact caching proxy with automated SCA quarantine gates.
- [ ] **Artifact Integrity**: Build outputs generate automated SBOMs (CycloneDX/SPDX) and are cryptographically signed before container registry push.

---

&amp;gt; [!TIP]
&amp;gt; **Open-Source Reference Implementation**: Looking for a working reference implementation of ephemeral quality gates, Gitleaks secret detection, and decoupled static publication pipelines? Explore [`SixFiveMil/hugo-devsecops-starter`](https://github.com/SixFiveMil/hugo-devsecops-starter) on GitHub.

---

## Up Next in the Series

In **Part 3: Threat Modeling at Sprint Velocity: STRIDE Gates and BOLA Defenses** (releasing **Tuesday, September 22**), we move from infrastructure to application architecture: - How to run lightweight **15-minute mini-STRIDE** sessions during Agile backlog grooming. - Mapping Data Flow Diagrams (DFDs) across clinical trust boundaries. - Concrete code patterns to eliminate **OWASP API1: Broken Object Level Authorization (BOLA)** using cryptographically secure UUIDs and data-layer authorization context checks.&lt;/code&gt;&lt;/pre&gt;

</description>
      <category>architecture</category>
      <category>cybersecurity</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Working Backwards from Error Logs: Reverse Engineering the Tableau-to-Fabric OAuth Breakdown</title>
      <dc:creator>hlldvr</dc:creator>
      <pubDate>Sun, 04 Oct 2026 22:42:05 +0000</pubDate>
      <link>https://dev.to/sixfivemil/working-backwards-from-error-logs-reverse-engineering-the-tableau-to-fabric-oauth-breakdown-58d4</link>
      <guid>https://dev.to/sixfivemil/working-backwards-from-error-logs-reverse-engineering-the-tableau-to-fabric-oauth-breakdown-58d4</guid>
      <description>&lt;h2&gt;
  
  
  🛑 The Crime Scene: A Misleading Desktop Failure
&lt;/h2&gt;

&lt;p&gt;When modern lakehouse architectures meet desktop analytics clients, authentication breakdowns rarely announce their true root cause. Instead, engineers are handed generic hex codes and deceptive UI prompts.&lt;/p&gt;

&lt;p&gt;During an enterprise deployment of &lt;strong&gt;Microsoft Fabric Warehouse&lt;/strong&gt;, data analysts attempted to connect &lt;strong&gt;Tableau Desktop (2024.2.0)&lt;/strong&gt; to the Fabric SQL/TDS endpoint (&lt;code&gt;*.datawarehouse.fabric.microsoft.com&lt;/code&gt;) using the native Azure SQL Database connector and Microsoft Entra ID modern authentication.&lt;/p&gt;

&lt;p&gt;Instead of opening the workspace schema and Delta tables, Tableau abruptly halted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An error occurred while communicating with Azure SQL Database
Authentication failed.
Error Code: 84223ADA
User authorization failed (invalid_client)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Tableau Desktop&amp;lt;br/&amp;gt;(Client UI)"] --&amp;gt;|"Azure SQL Connector&amp;lt;br/&amp;gt;OAuth Request"| B["Microsoft Entra ID&amp;lt;br/&amp;gt;(IDP)"]
    B --x|"Token Issuance Aborted"| A&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Why the Client UI Is a Trap
&lt;/h3&gt;

&lt;p&gt;In the OAuth 2.0 RFC 6749 specification, &lt;code&gt;invalid_client&lt;/code&gt; typically indicates an unregistered &lt;code&gt;client_id&lt;/code&gt;, a secret mismatch, or an unapproved redirect URI. In a traditional SaaS integration, an administrator might waste hours cycling client credentials or rebuilding app registrations.&lt;/p&gt;

&lt;p&gt;However, in this environment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Delegated Auth&lt;/strong&gt;: The connection relied on interactive user login, meaning client secret mismatches were completely irrelevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-TDS Failure&lt;/strong&gt;: The connection aborted during token acquisition before TCP/TDS traffic ever reached the Microsoft Fabric endpoint. Workspace roles and Fabric access control policies hadn't even been evaluated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To find the true root cause, we have to stop troubleshooting the client UI and pivot directly into the Identity Provider's telemetry stream.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Forensic Phase 1: Correlating Client Telemetry with Entra ID Logs
&lt;/h2&gt;

&lt;p&gt;When a desktop client throws an ambiguous error, the authoritative source of truth is the &lt;strong&gt;Microsoft Entra ID &lt;code&gt;SigninLogs&lt;/code&gt;&lt;/strong&gt; stream. &lt;/p&gt;

&lt;p&gt;Using Kusto Query Language (KQL) in Microsoft Sentinel / Azure Log Analytics, we correlate the exact timestamp of the failed connection attempt against the Tableau Desktop multi-tenant Application ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Query 1: Trace the exact failure timestamp and correlation ID
let TargetUser = "analyst@organization.example";
let TableauAppId = "0464ea90-c12f-42a7-b347-c2311ca4413c"; // Public Tableau Desktop App ID

SigninLogs
| where TimeGenerated &amp;gt; ago(24h)
| where UserPrincipalName =~ TargetUser and AppId == TableauAppId
| project 
    TimeGenerated, 
    CorrelationId, 
    AppDisplayName, 
    AppId, 
    ResourceDisplayName, 
    ResourceServicePrincipalId, 
    ResultType, 
    ResultDescription, 
    ConditionalAccessStatus
| order by TimeGenerated desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Telemetry: The Real Dependency Surfaces
&lt;/h3&gt;

&lt;p&gt;The KQL query reveals the exact reason the token issuance failed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Telemetry Field&lt;/th&gt;
&lt;th&gt;Raw Log Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AppDisplayName&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Tableau Desktop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AppId&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0464ea90-c12f-42a7-b347-c2311ca4413c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ResultType&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;650052&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;AADSTS650052&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ResultDescription&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;The app needs access to a service (&lt;code&gt;e9f49c6b-5ce5-44c8-925d-015017e9f7ad&lt;/code&gt;) that isn't installed in your tenant.&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CorrelationId&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;invalid_client&lt;/code&gt; error was a red herring. The identity provider refused to issue tokens because the tenant directory was missing a required first-party service principal: &lt;strong&gt;&lt;code&gt;e9f49c6b-5ce5-44c8-925d-015017e9f7ad&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 Reverse Engineering the Undocumented OAuth Handshake
&lt;/h2&gt;

&lt;p&gt;What is Application ID &lt;code&gt;e9f49c6b-5ce5-44c8-925d-015017e9f7ad&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Querying the global Microsoft application catalog identifies this GUID as the first-party &lt;strong&gt;Azure Data Lake / Azure Storage&lt;/strong&gt; enterprise application.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lakehouse Duality: Why Does TDS Require Azure Data Lake?
&lt;/h3&gt;

&lt;p&gt;To understand why Tableau requested an Azure Data Lake token during an Azure SQL connection, we must examine how Microsoft Fabric decouples storage from compute:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    actor User as BI Analyst
    participant Tableau as Tableau Desktop (2024.2.0)
    participant Entra as Microsoft Entra ID
    participant Fabric as Fabric SQL Endpoint (TDS)

    User-&amp;gt;&amp;gt;Tableau: Initiate Fabric Warehouse Connection
    Tableau-&amp;gt;&amp;gt;Entra: Parallel Token Request:&amp;lt;br/&amp;gt;1. Azure SQL Database (022907d3...)&amp;lt;br/&amp;gt;2. Azure Data Lake (e9f49c6b...)
    Note over Entra: Check local tenant directory for Service Principals
    Entra--&amp;gt;&amp;gt;Tableau: HTTP 400: AADSTS650052 (Azure Data Lake SP Missing)
    Tableau--&amp;gt;&amp;gt;User: Error 84223ADA: User authorization failed (invalid_client)&lt;/code&gt;&lt;/pre&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The SQL/TDS Gateway&lt;/strong&gt;: Microsoft Fabric exposes a TDS interface (port 1433) that mimics Azure SQL Database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OneLake Underlying Storage&lt;/strong&gt;: The underlying data is stored as Delta Parquet files in &lt;strong&gt;OneLake&lt;/strong&gt; (built on Azure Data Lake Storage Gen2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Multi-Resource OAuth Profile&lt;/strong&gt;: Tableau Desktop’s native driver architecture requests dual-resource delegated scopes during modern authentication—acquiring authorization for both the Azure SQL endpoint and the underlying storage subsystem simultaneously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the target Entra ID tenant had never instantiated the &lt;strong&gt;Azure Data Lake&lt;/strong&gt; enterprise application service principal, Entra ID aborted the OAuth handshake immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Forensic Phase 2: Controlled Remediation &amp;amp; The "Progress Error"
&lt;/h2&gt;

&lt;p&gt;With the missing resource identified, we instantiate the first-party Microsoft service principal using Microsoft Graph PowerShell:&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;# Instantiate the missing first-party Azure Data Lake Service Principal&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Connect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scopes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application.ReadWrite.All"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoWelcome&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$AzureDataLakeAppId&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;"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ExistingSP&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;Get-MgServicePrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appId eq '&lt;/span&gt;&lt;span class="nv"&gt;$AzureDataLakeAppId&lt;/span&gt;&lt;span class="s2"&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;$ExistingSP&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="n"&gt;New-MgServicePrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AppId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$AzureDataLakeAppId&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Instantiated Azure Data Lake Service Principal successfully."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&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="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Service Principal already present."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Yellow&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;Disconnect-MgGraph&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Second Error: Why Error Progression Equals Success
&lt;/h3&gt;

&lt;p&gt;The analyst re-attempts the connection in Tableau Desktop. Once again, an error dialog appears. However, inspecting the fresh Entra ID sign-in log reveals an entirely different code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SigninLogs
| where TimeGenerated &amp;gt; ago(1h)
| where UserPrincipalName =~ TargetUser and AppId == TableauAppId
| project TimeGenerated, AppDisplayName, ResultType, ResultDescription
| order by TimeGenerated desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ResultType&lt;/th&gt;
&lt;th&gt;ResultDescription&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;90095&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;AADSTS90095&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Admin consent is required for the requested permissions on Tableau Desktop.&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;The Identity Forensics Rule&lt;/strong&gt;: In distributed identity troubleshooting, &lt;strong&gt;a new error code is definitive proof of forward progress.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;650052&lt;/code&gt;: The identity provider could not locate the required resource definition.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;90095&lt;/code&gt;: The resource was successfully resolved, and execution successfully advanced to the tenant's admin consent and authorization policy evaluation.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Phase1: Connect Attempt
    Phase1 --&amp;gt; Phase2: Instantiate Azure Data Lake SP
    Phase2 --&amp;gt; Phase3: Admin Consent Review
    Phase3 --&amp;gt; Success: ResultType 0 (TDS Connected)

    state Phase1 {
        Tableau_84223ADA --&amp;gt; Entra_650052 : Missing Service Principal
    }
    state Phase2 {
        Entra_90095 : Admin Consent Gateway Triggered
    }
    state Phase3 {
        Entra_Consent : Delegated Scope Granted
    }
    state Success {
        Connected : Authenticated &amp;amp; Fabric RBAC Evaluated
    }&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Once a tenant administrator reviews the generated consent request and grants delegated permissions for the Tableau Desktop application, Entra ID issues the access tokens. The connection succeeds with &lt;strong&gt;&lt;code&gt;ResultType: 0&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ Engineering the Defensible DevSecOps Runbook
&lt;/h2&gt;

&lt;p&gt;Ad-hoc fixes in the cloud console solve single incidents, but enterprise environments demand automated, hardened, and repeatable runbooks.&lt;/p&gt;

&lt;p&gt;Unchecked administrative scripting introduces real operational risks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elevated Session Leakage&lt;/strong&gt;: Leaving interactive write scopes (&lt;code&gt;Application.ReadWrite.All&lt;/code&gt;) open in developer shells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Tenant Misconfiguration&lt;/strong&gt;: Accidentally executing modifications against the wrong tenant in multi-tenant MSP or hybrid environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV Formula Injection (CWE-1236)&lt;/strong&gt;: Unsanitized log exports containing spreadsheet command triggers (&lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;) that can execute arbitrary payloads when opened by audit teams in Microsoft Excel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hardened Remediation and Audit Script
&lt;/h3&gt;

&lt;p&gt;The following script encapsulates our reverse-engineered resolution with strict defensive boundaries:&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="cm"&gt;&amp;lt;#
&lt;/span&gt;&lt;span class="cs"&gt;.SYNOPSIS&lt;/span&gt;&lt;span class="cm"&gt;
    Hardened diagnostic and remediation script for Tableau-to-Fabric OAuth dependencies.
&lt;/span&gt;&lt;span class="cs"&gt;.DESCRIPTION&lt;/span&gt;&lt;span class="cm"&gt;
    Validates tenant context, inventories service principals, applies JIT remediation,
    and exports formula-injection-safe evidence.
#&amp;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;CmdletBinding&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&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;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ExpectedTenantId&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&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;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$EvidenceOutputPath&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;Set-StrictMode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="bp"&gt;$Error&lt;/span&gt;&lt;span class="n"&gt;ActionPreference&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;'Stop'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$TableauAppId&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;'0464ea90-c12f-42a7-b347-c2311ca4413c'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$AzureDataLakeAppId&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;'e9f49c6b-5ce5-44c8-925d-015017e9f7ad'&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 1. Read-Only Pre-flight &amp;amp; Tenant Boundary Check&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[1/4] Connecting to Microsoft Graph (Read-Only)..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cyan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Disconnect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Connect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scopes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Application.Read.All'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoWelcome&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$Context&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;Get-MgContext&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&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;$Context&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TenantId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ExpectedTenantId&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="n"&gt;Disconnect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Security Guardrail: Connected Tenant ID does not match expected target (&lt;/span&gt;&lt;span class="nv"&gt;$ExpectedTenantId&lt;/span&gt;&lt;span class="s2"&gt;). Aborting."&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;# 2. Inventory Required Service Principals&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$Targets&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="w"&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;Name&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;'Tableau Desktop'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AppId&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="nv"&gt;$TableauAppId&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="nx"&gt;Name&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;'Azure Data Lake'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AppId&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="nv"&gt;$AzureDataLakeAppId&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;$Inventory&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;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Targets&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;$Sp&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;Get-MgServicePrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appId eq '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$Target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppId&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pscustomobject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;Name&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="nv"&gt;$Target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;AppId&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="nv"&gt;$Target&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AppId&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;ExistsInTenant&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="err"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Sp&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;ObjectId&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="nv"&gt;$Sp&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;AccountEnabled&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="nv"&gt;$Sp&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AccountEnabled&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;$Inventory&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;Format-Table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AutoSize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Disconnect-MgGraph&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 3. Privileged Remediation (JIT Session with Guaranteed Teardown)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$NeedsDataLakeSp&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;$Inventory&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="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$AzureDataLakeAppId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExistsInTenant&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;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;$NeedsDataLakeSp&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="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[2/4] Instantiating missing Azure Data Lake Service Principal..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Yellow&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Connect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scopes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Application.ReadWrite.All'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoWelcome&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;try&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;$CurrentContext&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;Get-MgContext&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&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;$CurrentContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TenantId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ExpectedTenantId&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;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tenant mismatch during privileged session."&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;$CreatedSp&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;New-MgServicePrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AppId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$AzureDataLakeAppId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Created SP successfully: ObjectId &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$CreatedSp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&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;finally&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;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[3/4] Tearing down privileged Graph session..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cyan&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Disconnect-MgGraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&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="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="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[2/4] Azure Data Lake SP already present. No write action needed."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&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;# 4. Safe Evidence Packaging (Formula Injection Defense - CWE-1236)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ConvertTo-SafeSpreadsheetText&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;AllowNull&lt;/span&gt;&lt;span class="p"&gt;()][&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Value&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="bp"&gt;$null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Value&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;$null&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;$Text&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;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Value&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;$Text&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;'^[=+@-]'&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="nv"&gt;$Text&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="nv"&gt;$Text&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;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[4/4] Generating sanitized audit evidence..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cyan&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$EvidenceOutputPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PathType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Container&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="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$EvidenceOutputPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&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;Out-Null&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;$SanitizedInventory&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="nv"&gt;$Inventory&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;ForEach-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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pscustomobject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;Name&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="nx"&gt;ConvertTo&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;SafeSpreadsheetText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;AppId&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="nx"&gt;ConvertTo&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;SafeSpreadsheetText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AppId&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;ExistsInTenant&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="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ExistsInTenant&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;ObjectId&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="nx"&gt;ConvertTo&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;SafeSpreadsheetText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectId&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;$CsvPath&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;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$EvidenceOutputPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Tableau-Fabric-SP-Inventory.csv'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$SanitizedInventory&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;Export-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CsvPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoTypeInformation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Encoding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UTF8&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Cryptographic Evidence Seal&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$EvidenceOutputPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&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;Get-FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SHA256&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;Export-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$EvidenceOutputPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'SHA256SUMS.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoTypeInformation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Encoding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UTF8&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Audit evidence cryptographically sealed at &lt;/span&gt;&lt;span class="nv"&gt;$EvidenceOutputPath&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📊 Non-Dropping KQL: Auditing Conditional Access Without Telemetry Gaps
&lt;/h2&gt;

&lt;p&gt;When investigating authentication failures across managed and BYOD devices, many standard KQL queries use naive &lt;code&gt;mv-expand&lt;/code&gt; on &lt;code&gt;ConditionalAccessPolicies&lt;/code&gt;. If a sign-in event triggered no CA policies, or if device trust was missing, standard inner expansions drop those rows entirely from the audit report.&lt;/p&gt;

&lt;p&gt;The following production query uses a left-outer expansion pattern to ensure every sign-in attempt is accounted for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let BYODPolicyName = "Example BYOD Web Access Controls";
let BaseSignIns = materialize(
    SigninLogs
    | where TimeGenerated &amp;gt; ago(30d)
    | where AppId == "0464ea90-c12f-42a7-b347-c2311ca4413c" // Tableau Desktop
    | extend 
        TrustType = tostring(DeviceDetail.trustType),
        OS = tostring(DeviceDetail.operatingSystem),
        IsBrowser = (ClientAppUsed =~ "Browser")
    | project 
        TimeGenerated, 
        CorrelationId, 
        UserPrincipalName, 
        AppDisplayName, 
        ClientAppUsed, 
        IsBrowser, 
        TrustType, 
        OS, 
        ConditionalAccessPolicies, 
        ResultType
);

let PolicyResults = BaseSignIns
    | mv-expand kind=left CAPolicy = ConditionalAccessPolicies
    | extend CAName = tostring(CAPolicy.displayName), CAResult = tostring(CAPolicy.result)
    | where CAName == BYODPolicyName
    | summarize PolicyResult = strcat_array(make_set(CAResult), ",") by CorrelationId;

BaseSignIns
| project-away ConditionalAccessPolicies
| join kind=leftouter PolicyResults on CorrelationId
| extend 
    PolicyEnforced = isnotempty(PolicyResult),
    EffectiveResult = iff(isnotempty(PolicyResult), PolicyResult, "No matching policy entry")
| summarize TotalAttempts = count(), SuccessfulAuths = countif(ResultType == 0) 
    by UserPrincipalName, AppDisplayName, TrustType, EffectiveResult
| order by TotalAttempts desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 Key Takeaways for Identity &amp;amp; Data Engineers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client Error Codes Mask Protocol Truth&lt;/strong&gt;: A client-side &lt;code&gt;invalid_client&lt;/code&gt; or &lt;code&gt;84223ADA&lt;/code&gt; error is merely an HTTP 400 wrapper. Always trace the session via &lt;code&gt;CorrelationId&lt;/code&gt; in the Identity Provider's sign-in logs before modifying configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern Lakehouses Abstract Storage, Not Identity&lt;/strong&gt;: Even when connecting over a standard TDS/SQL gateway, modern analytics drivers frequently require explicit delegated token scopes for underlying storage resources (&lt;code&gt;Azure Data Lake&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embrace Error Progression&lt;/strong&gt;: Moving from &lt;code&gt;AADSTS650052&lt;/code&gt; (missing resource) → &lt;code&gt;AADSTS90095&lt;/code&gt; (admin consent) → &lt;code&gt;0&lt;/code&gt; (success) confirms that each identity layer was resolved systematically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Defensible Runbooks&lt;/strong&gt;: Separate read-only diagnostics from privileged writes, enforce programmatic tenant boundaries, and protect audit artifacts against spreadsheet injection.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>microsoftfabric</category>
      <category>tableau</category>
      <category>microsoftentraid</category>
      <category>oauth</category>
    </item>
    <item>
      <title>Modernizing the Academic Knowledge Graph: Canvas Sync Bridge v0.4.0, Hybrid Ingestion, and Official Obsidian Compliance</title>
      <dc:creator>hlldvr</dc:creator>
      <pubDate>Sun, 04 Oct 2026 22:42:04 +0000</pubDate>
      <link>https://dev.to/sixfivemil/modernizing-the-academic-knowledge-graph-canvas-sync-bridge-v040-hybrid-ingestion-and-official-2f3o</link>
      <guid>https://dev.to/sixfivemil/modernizing-the-academic-knowledge-graph-canvas-sync-bridge-v040-hybrid-ingestion-and-official-2f3o</guid>
      <description>&lt;h2&gt;
  
  
  🎓 The Academic Knowledge Management Dilemma
&lt;/h2&gt;

&lt;p&gt;Modern higher education and enterprise training institutions rely almost universally on Learning Management Systems (LMS) like Instructure Canvas to distribute course syllabi, lecture modules, assignments, student discussions, and grading rubrics.&lt;/p&gt;

&lt;p&gt;Yet for technical students, researchers, and security practitioners who build their intellectual workflows inside personal knowledge management (PKM) systems like &lt;strong&gt;Obsidian&lt;/strong&gt;, Canvas represents an isolated, walled-off data silo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ephemerality &amp;amp; Term Expiration&lt;/strong&gt;: Once an academic semester concludes, student access to Canvas courses is routinely archived or revoked. Syllabi, instructor annotations, curated reading lists, and assignment rubrics vanish behind institutional access gates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disconnected Knowledge Graphs&lt;/strong&gt;: Course notes authored in Obsidian remain disconnected from source materials, grading criteria, and module pacing guides living inside the web browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Administrative Token Gating&lt;/strong&gt;: Canvas exposes an extensive REST API, but institutional administrators frequently disable personal access tokens (&lt;code&gt;Account &amp;gt; Settings &amp;gt; + New Access Token&lt;/code&gt;) for student roles due to enterprise compliance policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-Party Cloud Aggregation Risks&lt;/strong&gt;: Traditional third-party scrapers and web integrations require routing student session credentials, course documents, and peer discussions through external SaaS servers. For students handling proprietary lab code or education records governed by &lt;strong&gt;FERPA&lt;/strong&gt; and &lt;strong&gt;GDPR&lt;/strong&gt;, third-party cloud aggregation is an unacceptable privacy compromise.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Third-Party Cloud Scraper (High Risk):
[Canvas LMS] ----(Credentials/Course Data)----&amp;gt; [Cloud Relay / SaaS] ----&amp;gt; [Obsidian Vault]
                                                        ▲
                                                        └── Attack Surface &amp;amp; Data Leakage Risk

Canvas Sync Bridge v0.4.0 (Hybrid Local-First Architecture):
[Canvas LMS REST API] ════(Direct Token / requestUrl)═══════════════════╗
                                                                        ▼
[Canvas LMS Web Tab]  ════(Session Cookies)════&amp;gt; [Browser Ext] ──(127.0.0.1)──&amp;gt; [Obsidian Vault]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To eliminate this friction while upholding strict privacy boundaries and supply-chain integrity, I architected and open-sourced &lt;strong&gt;Canvas Sync Bridge v0.4.0&lt;/strong&gt;—a production-grade, hybrid local-first ecosystem split into &lt;strong&gt;two dedicated, decoupled open-source GitHub repositories&lt;/strong&gt; and audited against official Obsidian Community guidelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Architectural Decoupling: Two Repositories, Two Independent Lifecycles
&lt;/h2&gt;

&lt;p&gt;A central engineering milestone of the &lt;strong&gt;v0.4.0 release&lt;/strong&gt; is the clean decoupling of the codebase into two standalone, single-responsibility open-source repositories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                  ┌─────────────────────────────────────────────────────────┐
                                  │                CANVAS TO OBSIDIAN SUITE                 │
                                  └────────────────────────────┬────────────────────────────┘
                                                               │
                                ┌──────────────────────────────┴──────────────────────────────┐
                                │                                                             │
                                ▼                                                             ▼
┌───────────────────────────────────────────────────────────┐   ┌───────────────────────────────────────────────────────────┐
│              OBSIDIAN DESKTOP &amp;amp; MOBILE PLUGIN             │   │            COMPANION BROWSER WEBEXTENSION                 │
│      (https://github.com/SixFiveMil/obsidian-canvas-sync) │   │ (https://github.com/SixFiveMil/canvas-to-obsidian-extension)│
├───────────────────────────────────────────────────────────┤   ├───────────────────────────────────────────────────────────┤
│ • Native Obsidian requestUrl Direct REST API Client       │   │ • Manifest V3 Service Worker (Chrome, Brave, Edge, Arc)   │
│ • Vault Note Synthesizer &amp;amp; GFM Markdown Converter         │   │ • WebExtensions API Packaging (Mozilla Firefox AMO)       │
│ • Obsidian Community Directory CI (obsidian-workflows)    │   │ • Zero-Token Session Extraction from active Canvas tabs   │
│ • Popout Window &amp;amp; Mobile Scoping (Platform.isDesktop)     │   │ • Automated Store Deployment Pipeline (publish-stores.mjs)│
│ • Cryptographically signed releases with SLSA Provenance  │   │ • Link-local Loopback HTTP Dispatch (127.0.0.1:27125)     │
└───────────────────────────────────────────────────────────┘   └───────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Decouple into Separate Repositories?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Independent Release Lifecycles &amp;amp; Store Governance&lt;/strong&gt;:
The Obsidian plugin and browser extensions target completely different distribution registries with different review timelines. The plugin is submitted to the &lt;a href="https://community.obsidian.md/plugins/canvas-sync-bridge" rel="noopener noreferrer"&gt;Obsidian Community Plugins Directory&lt;/a&gt; and &lt;a href="https://github.com/TfTHacker/obsidian42-brat" rel="noopener noreferrer"&gt;BRAT&lt;/a&gt;, whereas the extension targets the &lt;strong&gt;Chrome Web Store&lt;/strong&gt; and &lt;strong&gt;Firefox Add-ons (AMO)&lt;/strong&gt;. Decoupling ensures that a patch to the Chrome MV3 background worker doesn't trigger unnecessary plugin releases or invalidate Obsidian community reviewer hashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Targeted CI/CD &amp;amp; Validation Pipelines&lt;/strong&gt;:
The Obsidian plugin uses the official &lt;code&gt;obsidianmd/obsidian-workflows&lt;/code&gt; action with &lt;code&gt;eslint-plugin-obsidianmd&lt;/code&gt; and &lt;code&gt;stylelint&lt;/code&gt;. The browser extension repository uses web-extension manifest validators, Chrome Web Store API deployers, and AMO signing scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Dependency Bloat&lt;/strong&gt;:
Users who rely exclusively on the &lt;strong&gt;Direct REST API&lt;/strong&gt; within Obsidian can install the plugin without downloading any browser extension build artifacts, while extension contributors don't need Obsidian Desktop API dependencies.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🏛️ Hybrid Ingestion Architecture
&lt;/h2&gt;

&lt;p&gt;The decoupled ecosystem supports two complementary ingestion pathways that converge into a unified canonical note generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────────────────────┐
│                                   CANVAS LMS CLOUD                                     │
│   ┌────────────────────────────────────────────────────────────────────────────────┐   │
│   │  Canvas REST API (/api/v1/...)                                                 │   │
│   │  - Courses, Modules, Pages, Syllabus                                           │   │
│   │  - Assignments, Rubrics &amp;amp; Student Submissions                                  │   │
│   │  - Discussions &amp;amp; Complete Nested Reply Trees                                   │   │
│   │  - Calendar Events &amp;amp; Due Date Milestones                                       │   │
│   │  - Course Files &amp;amp; Static Asset Downloads                                       │   │
│   └────────────────────────┬───────────────────────────────┬───────────────────────┘   │
└────────────────────────────┼───────────────────────────────┼───────────────────────────┘
                             │                               │
        Mode A: Direct API   │ HTTPS                         │ Mode B: Session-Based
      (Obsidian requestUrl)  │ (Bearer Token)                │ (Browser Session Cookies)
                             │                               ▼
                             │                 ┌───────────────────────────┐
                             │                 │ Companion Web Extension   │
                             │                 │ (Chrome / Firefox MV3)    │
                             │                 │ - Session Extractor       │
                             │                 │ - Extraction Toggles      │
                             │                 └─────────────┬─────────────┘
                             │                               │ Loopback POST (127.0.0.1:27125)
                             │                               │ (Opt-in / Platform.isDesktop)
                             ▼                               ▼
┌────────────────────────────────────────────────────────────────────────────────────────┐
│                              OBSIDIAN PLUGIN RUNTIME                                   │
│  ┌─────────────────────────┐                     ┌──────────────────────────────────┐  │
│  │ CanvasApiClient         │                     │ Loopback Bridge Server           │  │
│  │ (Direct API Connection) │                     │ (conditionally active)           │  │
│  └────────────┬────────────┘                     └────────────────┬─────────────────┘  │
│               │                                                   │                    │
│               └─────────────────────┬─────────────────────────────┘                    │
│                                     │                                                  │
│                                     ▼                                                  │
│                       ┌───────────────────────────┐                                    │
│                       │   Canonical Course Payload│                                    │
│                       │   (CanvasCoursePayload)   │                                    │
│                       └─────────────┬─────────────┘                                    │
│                                     │                                                  │
│                                     ▼                                                  │
│                       ┌───────────────────────────┐                                    │
│                       │ Markdown &amp;amp; Link Engine    │                                    │
│                       │ - GFM Converter (Turndown)│                                    │
│                       │ - Wikilink Transformer    │                                    │
│                       │ - Table Pipe Escaper      │                                    │
│                       └─────────────┬─────────────┘                                    │
│                                     │                                                  │
│                                     ▼                                                  │
│                       ┌───────────────────────────┐                                    │
│                       │ Asset &amp;amp; File Downloader   │                                    │
│                       │ - Binary Streamer         │                                    │
│                       │ - Size/Extension Filters  │                                    │
│                       └─────────────┬─────────────┘                                    │
│                                     │                                                  │
│                                     ▼                                                  │
│                       ┌───────────────────────────┐                                    │
│                       │ Vault Note Generator      │                                    │
│                       │ - Path Sanitization       │                                    │
│                       │ - Templated Course Vault  │                                    │
│                       └─────────────┬─────────────┘                                    │
└─────────────────────────────────────┼──────────────────────────────────────────────────┘
                                      ▼
┌────────────────────────────────────────────────────────────────────────────────────────┐
│                              OBSIDIAN VAULT STORAGE                                    │
│  └── Canvas/CS510 - Network Security/                                                  │
│      ├── Course.md, Home.md, Syllabus.md, Grades.md                                    │
│      ├── Tasks.md, Discussions.md, Calendar.md                                         │
│      ├── Modules/01 - Week 1/01 - Page - Lecture.md                                    │
│      ├── Files/ (PDF, DOCX, XLSX, etc.)                                                │
│      └── Attachments/ (Images, Banners)                                                │
└────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ingestion Pathways Compared
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ingestion Mode&lt;/th&gt;
&lt;th&gt;Repository / Component&lt;/th&gt;
&lt;th&gt;Ingestion Mechanism&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Platform Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mode A: Direct REST API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/SixFiveMil/obsidian-canvas-sync" rel="noopener noreferrer"&gt;&lt;code&gt;obsidian-canvas-sync&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Native &lt;code&gt;requestUrl&lt;/code&gt; adapter using personal Canvas API tokens&lt;/td&gt;
&lt;td&gt;Standard users, automated multi-course batch syncing, historical term archiving&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Obsidian Desktop &amp;amp; Mobile&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mode B: Companion Web Extension&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/SixFiveMil/canvas-to-obsidian-extension" rel="noopener noreferrer"&gt;&lt;code&gt;canvas-to-obsidian-extension&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Manifest V3 service worker extracting active tab session cookies&lt;/td&gt;
&lt;td&gt;Locked-down universities where student API keys are disabled&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Chrome, Firefox, Edge, Brave, Arc&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🛡️ Obsidian Community Compliance &amp;amp; Guidelines Hardening
&lt;/h2&gt;

&lt;p&gt;To ensure enterprise-grade stability and prepare for official listing in the Obsidian Community Plugins directory, the plugin was audited and re-engineered against the complete suite of Obsidian developer guidelines:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Official CI/CD Validation Workflows
&lt;/h3&gt;

&lt;p&gt;The plugin repository integrates the official &lt;code&gt;obsidianmd/obsidian-workflows&lt;/code&gt; GitHub Action on all pull requests and tagged releases. Every commit is validated against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eslint-plugin-obsidianmd&lt;/code&gt;: Enforcing plugin lifecycle contracts and DOM safety.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stylelint&lt;/code&gt;: Ensuring theme neutrality and valid CSS custom properties.&lt;/li&gt;
&lt;li&gt;Dynamic guideline guard tests asserting zero forbidden global overrides.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Multi-Window Popout Safety
&lt;/h3&gt;

&lt;p&gt;In modern Obsidian releases (v1.0+), notes and modals can be dragged into detached, popout operating system windows. The plugin eliminates legacy &lt;code&gt;globalThis&lt;/code&gt; and root &lt;code&gt;document&lt;/code&gt; references, binding event listeners and DOM elements dynamically to context-aware &lt;code&gt;activeWindow&lt;/code&gt; and &lt;code&gt;window&lt;/code&gt; scopes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Declarative Settings Search (&lt;code&gt;getSettingDefinitions&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Canvas Sync Bridge implements &lt;code&gt;getSettingDefinitions()&lt;/code&gt;, allowing users on Obsidian 1.13+ to search and jump directly to specific plugin settings (e.g. &lt;em&gt;Canvas Base URL&lt;/em&gt;, &lt;em&gt;Bridge Port&lt;/em&gt;, &lt;em&gt;Asset Filter Allowlist&lt;/em&gt;) from the global Obsidian settings filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Dynamic Desktop Isolation (&lt;code&gt;Platform.isDesktop&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Node.js core modules (&lt;code&gt;http&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;) required for the local loopback server are dynamically resolved and guarded behind &lt;code&gt;Platform.isDesktop&lt;/code&gt;. This guarantees that the plugin initializes cleanly on &lt;strong&gt;Obsidian Mobile (iOS and Android)&lt;/strong&gt; in Direct REST API mode without throwing module resolution faults.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Supply Chain Security &amp;amp; SLSA Build Provenance
&lt;/h3&gt;

&lt;p&gt;Every release artifact (&lt;code&gt;main.js&lt;/code&gt;, &lt;code&gt;manifest.json&lt;/code&gt;, &lt;code&gt;styles.css&lt;/code&gt;) is cryptographically signed and attested using &lt;strong&gt;SLSA build provenance predicates&lt;/strong&gt; via GitHub OIDC and Sigstore. Build pipelines strictly prune non-essential assets, ensuring transparent, tamper-proof releases.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Comprehensive Coursework Synchronization
&lt;/h2&gt;

&lt;p&gt;Both ingestion modes stream course data into a canonical payload schema (&lt;code&gt;CanvasCoursePayload&lt;/code&gt;), producing complete, interconnected vaults:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Grades &amp;amp; Submissions (&lt;code&gt;Grades.md&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Syncs active grade standing, current course scores, submission status (Submitted, Graded, Missing), points earned vs points possible, submitted file links, and instructor feedback comments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### 📊 Student Gradebook Summary&lt;/span&gt;

| Assignment | Status | Score | Max Points | Feedback |
| :--- | :--- | :--- | :--- | :--- |
| &lt;span class="gs"&gt;**Lab 1: Cryptanalysis**&lt;/span&gt; | Graded | 95.0 | 100.0 | Great implementation of Kasiski examination. |
| &lt;span class="gs"&gt;**Midterm Exam**&lt;/span&gt; | Graded | 88.0 | 90.0 | Solid analysis on forward secrecy. |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Discussions with Nested Reply Trees (&lt;code&gt;Discussions.md&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Preserves instructor discussion prompts along with complete multi-tier nested student replies, formatted as hierarchical callouts with author timestamps and direct link anchors.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Assignments &amp;amp; Rubric Table Normalization (&lt;code&gt;Tasks.md&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Canvas rubrics use complex, nested HTML tables. The engine converts these into clean GitHub Flavored Markdown (GFM) tables, capturing rating descriptions, point distributions, and grading criteria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### 📋 Assignment Grading Rubric&lt;/span&gt;

| Criterion | Ratings | Max Points |
| :--- | :--- | :--- |
| &lt;span class="gs"&gt;**Architecture &amp;amp; Threat Model**&lt;/span&gt; | • Exemplary (20 pts): STRIDE model applied.&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;• Proficient (15 pts): Minor gaps.&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;• Novice (5 pts): Missing loopback controls. | 20 pts |
| &lt;span class="gs"&gt;**Unit Test Coverage**&lt;/span&gt; | • Full Marks (30 pts): 100% Vitest pass rate.&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;• Partial (20 pts): Missing edge cases. | 30 pts |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Local File &amp;amp; Asset Downloader (&lt;code&gt;Files/&lt;/code&gt; &amp;amp; &lt;code&gt;Attachments/&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Directly downloads referenced course documents (&lt;code&gt;.pdf&lt;/code&gt;, &lt;code&gt;.docx&lt;/code&gt;, &lt;code&gt;.pptx&lt;/code&gt;, &lt;code&gt;.xlsx&lt;/code&gt;, &lt;code&gt;.zip&lt;/code&gt;) and embedded images into dedicated vault folders. Includes configurable maximum file size limits (default: 50MB) and extension allowlists.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Wikilink Engine with Table Pipe Escaping
&lt;/h3&gt;

&lt;p&gt;All internal module items and syllabus links are resolved to Obsidian &lt;code&gt;[[wikilinks]]&lt;/code&gt;. Table-embedded links use strict pipe escaping (&lt;code&gt;[[path\|alias]]&lt;/code&gt;) to ensure markdown tables render without broken column boundaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗂️ Generated Vault Structure
&lt;/h2&gt;

&lt;p&gt;Synced courses generate a structured, navigable directory hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Canvas/
└── CS510 - Advanced Network Security/
    ├── Course.md         # Master index note with metadata &amp;amp; instructor contact
    ├── Home.md           # Course landing page &amp;amp; announcement banners
    ├── Syllabus.md       # Complete syllabus text, policies &amp;amp; textbook list
    ├── Tasks.md          # Assignment checklists, due dates &amp;amp; rubric tables
    ├── Grades.md         # Gradebook table with scores, percentages &amp;amp; feedback
    ├── Discussions.md    # Discussion board topics with full student reply trees
    ├── Calendar.md       # Course milestones, due dates &amp;amp; Zoom meeting links
    ├── Modules/
    │   ├── 01 - Week 1 - Applied Cryptography/
    │   │   ├── 01 - Page - Stream Ciphers &amp;amp; Block Ciphers.md
    │   │   └── 02 - Assignment - Breaking Polyalphabetic Ciphers.md
    │   └── 02 - Week 2 - Protocol Vulnerabilities/
    │       ├── 01 - Page - TLS 1.3 &amp;amp; Forward Secrecy.md
    │       └── 02 - Assignment - Wireshark Decryption Lab.md
    ├── Files/            # Downloaded PDFs, DOCX, slides, spreadsheets, and archives
    └── Attachments/      # Embedded images, course banners, and diagrams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🚀 Installation &amp;amp; Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install the Obsidian Plugin
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Method A: Community Plugins (Recommended)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Obsidian Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Community Plugins&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Disable &lt;strong&gt;Restricted mode&lt;/strong&gt; if prompted.&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;Canvas Sync Bridge&lt;/strong&gt;, click &lt;strong&gt;Install&lt;/strong&gt;, and &lt;strong&gt;Enable&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Method B: Obsidian BRAT (Beta Builds)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Install the &lt;a href="https://github.com/TfTHacker/obsidian42-brat" rel="noopener noreferrer"&gt;BRAT Plugin&lt;/a&gt; in Obsidian.&lt;/li&gt;
&lt;li&gt;Under BRAT settings, click &lt;strong&gt;Add Beta plugin&lt;/strong&gt; and enter:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://github.com/SixFiveMil/obsidian-canvas-sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Method C: Manual Release
&lt;/h4&gt;

&lt;p&gt;Download &lt;code&gt;main.js&lt;/code&gt;, &lt;code&gt;manifest.json&lt;/code&gt;, and &lt;code&gt;styles.css&lt;/code&gt; from the &lt;a href="https://github.com/SixFiveMil/obsidian-canvas-sync/releases/latest" rel="noopener noreferrer"&gt;Latest GitHub Release&lt;/a&gt; and place them in &lt;code&gt;&amp;lt;Vault&amp;gt;/.obsidian/plugins/canvas-sync-bridge/&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Choose Your Sync Method
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Option A: Direct REST API (Recommended)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;In Canvas, navigate to &lt;strong&gt;Account&lt;/strong&gt; &amp;gt; &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Approved Integrations&lt;/strong&gt; &amp;gt; &lt;strong&gt;+ New Access Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy your generated access token.&lt;/li&gt;
&lt;li&gt;In Obsidian Settings under &lt;strong&gt;Canvas Sync&lt;/strong&gt;, enter your Canvas Base URL (&lt;code&gt;https://canvas.instructure.com&lt;/code&gt; or your institution domain) and API Token.&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl/Cmd + P&lt;/code&gt; and run &lt;code&gt;Canvas Sync: Select &amp;amp; sync courses&lt;/code&gt; (or click the &lt;code&gt;🎓&lt;/code&gt; ribbon icon) to launch the interactive course selector.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Option B: Browser Extension Bridge (Zero-Token Mode)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;In Obsidian Settings under &lt;strong&gt;Canvas Sync&lt;/strong&gt;, toggle on &lt;strong&gt;Enable browser bridge listener&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Install the companion extension:

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;&lt;a href="https://chromewebstore.google.com/detail/canvas-to-obsidian-sync/oiakmbihplldnhabhnihnekjddenbiom?authuser=0&amp;amp;hl=en" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;&lt;/strong&gt; (Chrome, Edge, Brave, Arc, Opera)&lt;/li&gt;
&lt;li&gt;🦊 &lt;strong&gt;&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/canvas-to-obsidian-sync/" rel="noopener noreferrer"&gt;Firefox Add-ons&lt;/a&gt;&lt;/strong&gt; (Mozilla)&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;&lt;a href="https://github.com/SixFiveMil/canvas-to-obsidian-extension" rel="noopener noreferrer"&gt;Extension Source Repo&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Open any Canvas course tab in your browser, click the extension icon, test the bridge connection, and click &lt;strong&gt;Sync Active Course&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🤝 Open Source &amp;amp; Community RFC
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Canvas Sync Bridge&lt;/strong&gt; and its companion extension are distributed under the &lt;strong&gt;MIT License&lt;/strong&gt; across two dedicated open-source repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💎 &lt;strong&gt;Obsidian Plugin&lt;/strong&gt;: &lt;a href="https://github.com/SixFiveMil/obsidian-canvas-sync" rel="noopener noreferrer"&gt;SixFiveMil/obsidian-canvas-sync&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Browser Extension&lt;/strong&gt;: &lt;a href="https://github.com/SixFiveMil/canvas-to-obsidian-extension" rel="noopener noreferrer"&gt;SixFiveMil/canvas-to-obsidian-extension&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We welcome feedback, issues, and contributions from students, researchers, and educators. Join the architectural discussion on &lt;a href="https://github.com/SixFiveMil/obsidian-canvas-sync/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>canvaslms</category>
      <category>localfirst</category>
      <category>pkm</category>
    </item>
  </channel>
</rss>
