WRKLOG-2026-06-22

Summary

Monday. Fill summary at end of day.

URGENT - All Domains

Carryover Backlog (CRITICAL)

Task Details Origin Days Status

MSCHAPv2 Migration Report

Report due. 6-sheet Standard Report (exec summary, trend, waves, device detail, stale, policy match). Sheet 6 added 05-14: policy match by protocol for removal planning + anonymous identity validation. Migration window 2026-05-04 to 2026-05-30. ~6,227 devices, 5 waves.

2026-04-17

83

P0 - DUE — run report this week

Abnormal Security — ✅ COMPLETE

CR-2026-05-07-abnormal-read-write. CAB approved 2026-05-12. Implemented successfully 2026-05-13. Read/write enabled for pilot group. Post-deployment validation pending.

2026-05-07

63

✅ IMPLEMENTED — post-validation pending

SIEM QRadar → Sentinel Migration

Lead role. Monad console error RESOLVED 2026-05-12 — secrets configured in CHLA production tenant. ISE secure syslog integration in progress — cert imported, remote logging target configured, streaming errors under investigation. Blocking: DCR not created (Rule ID + Stream Name). Azure private network policy unresolved. Victor + Mauricio action.

2026-04-10

90

P0 - ACTIVE — ISE syslog + DCR blocking

Monad Pipeline Evaluation

Sentinel output connector. Console error resolved. 3 of 6 values configured. Remaining: Endpoint URL (have it), Rule ID + Stream Name (need DCR). ISE Remote Logging Target configured 2026-05-18 — TLS cert imported, secure syslog target created. Streaming errors in Monad console under investigation.

2026-03-11

120

P0 - ACTIVE — ISE integration in progress

Guest Redirect ACL

Guest redirect ACL work needed. Related to Mandiant remediation findings.

2026-05-12

58

P0 - TODO

ISE Patch 10 (CVE-2026-20147 CVSS 9.9)

ISE 3.2 Patch 10. Supersedes Patch 9. 61 days on a CVSS 9.9 — schedule maintenance window. Write CR if needed.

2026-03-12

119

P0 - OVERDUE — schedule immediately

k3s NAT verification

NAT rule 170 for 10.42.0.0/16 pod network - test internet connectivity. 64 days — test this week or defer to Q3.

2026-03-09

122

P0 - BLOCKING — TRIAGE: schedule or defer

Wazuh indexer recovery

Restart pod after NAT confirmed working - SIEM visibility blocked. Blocked by k3s NAT — cannot proceed until above resolved.

2026-03-09

122

P0 - Blocked by k3s

Strongline Gateway VLAN fix

8 devices in wrong identity group (David Rukiza assigned)

2026-03-16

115

P0 - TODO

TCP Clocks deployment

ISE identity group validation, query outputs, comms with team. Active d001 data Apr 22-23.

2026-04-22

78

P0 - ACTIVE

IoT Dr. Kim — recurring

Sleep study devices (Apr 15-16), watches recurrence (Apr 22). 5 incident versions in d001. Validate iPSK enrollment.

2026-04-15

85

P0 - RECURRING

Murus Portae (WAF) — Phase 0

FMC cert expired, ACP returns zero rules. d001: zone map, architecture D2, FMC API reference, ops script.

2026-04-16

84

P0 - INVESTIGATING

Vocera EAP-TLS Supplicant Fix

~10 phones failing 802.1X, missing supplicant config. 61 days — schedule with clinical engineering team.

2026-03-12

119

P1 - TODO — schedule

ISE MnT Messaging Service

Enable "Use ISE Messaging Service for UDP syslogs delivery". 61 days — low risk, schedule with ISE Patch 10 maintenance window.

2026-03-12

119

P2 - BUNDLE with Patch 10

Professional backlog remains critical. Check Days column for priorities.

BLOCKERS — Fix Immediately

Task Details Origin Days Impact

Z Fold 7 Termux

gopass and SSH not working

2026-03-10

58

BLOCKER — Cannot access passwords on mobile

gopass v3 organization

Inconsistent structure, poor key-value usage

2026-03-20

48

Inefficient password management, no aggregation

Git history scrub — sensitive personal terms

Plaintext references to personal legal matters in committed worklogs (WRKLOG-2026-03-14, WRKLOG-2026-04-18). Forward-fixed but old commits still contain strings. Requires git filter-repo + force-push. See runbook below.

2026-04-22

15

SECURITY — sensitive terms in public git history

Runbook: Git History Scrub (d000 Personal Terms)

Problem: Two committed worklogs contained plaintext references to personal legal matters. The files have been edited (forward-fix), but git history retains the original text in prior commits.

Affected commits: Any commit touching these files:

# Identify affected commits
git log --oneline -- \
  docs/modules/ROOT/pages/2026/03/WRKLOG-2026-03-14.adoc \
  docs/modules/ROOT/pages/2026/04/WRKLOG-2026-04-18.adoc

Scrub procedure:

# 1. BEFORE: Full backup of the repo
cp -a ~/atelier/_bibliotheca/domus-captures ~/atelier/_bibliotheca/domus-captures.bak

# 2. Install git-filter-repo (if not present)
# Arch: pacman -S git-filter-repo
# pip: pip install git-filter-repo

# 3. Create expressions file for replacement
cat > /tmp/scrub-expressions.txt << 'EXPR'
regex:(?i)divorce==[REDACTED]
regex:(?i)dissolutio(?!n\.adoc\.age)==[REDACTED-LEGAL]
regex:(?i)iliana==[REDACTED-NAME]
regex:(?i)angulo-arreola==[REDACTED-NAME]
regex:legal-divorce-notes\.age==legal-notes.age
regex:1099-NEC-iliana==1099-NEC
EXPR

# 4. Verify before (dry run — count matches in history)
git log -p --all -S 'divorce' -- '*.adoc' | grep -c 'divorce' || echo "0 matches"
git log -p --all -S 'iliana' -- '*.adoc' | grep -c 'iliana' || echo "0 matches"

# 5. Run filter-repo (DESTRUCTIVE — rewrites all commit hashes)
git filter-repo --replace-text /tmp/scrub-expressions.txt --force

# 6. Verify after
git log -p --all -S 'divorce' -- '*.adoc' | grep -c 'divorce' || echo "0 matches — CLEAN"
git log -p --all -S 'iliana' -- '*.adoc' | grep -c 'iliana' || echo "0 matches — CLEAN"

# 7. Re-add remotes (filter-repo removes them)
git remote add origin git@github.com:<user>/domus-captures.git
# Add any other remotes (Gitea, etc.)

# 8. Force-push to all remotes (DESTRUCTIVE — overwrites remote history)
git remote | xargs -I{} git push {} main --force

# 9. Clean up
rm /tmp/scrub-expressions.txt
rm -rf ~/atelier/_bibliotheca/domus-captures.bak  # only after verifying

Post-scrub checklist:

  • Backup created before running

  • git filter-repo installed

  • Expressions file reviewed — no false positives (e.g., Don Quijote "Angulo el Malo" is in segunda-parte/texto/texto-011.adoc — the regex targets angulo-arreola specifically to avoid this)

  • Dry-run counts match expectations

  • Filter-repo executed

  • Post-scrub verification shows 0 matches

  • Remotes re-added

  • Force-pushed to all remotes

  • Cloudflare Pages rebuild verified

  • Local clones on other machines re-cloned or git fetch --all && git reset --hard origin/main

  • Backup removed

URGENT - Requires Immediate Action

Item Details Deadline Status Impact

Housing Search

Granada Hills area - apartments/rooms

TBD

In Progress

Quality of life, commute

2025 Tax — IRS Transcript Review

MFJ filed 2026-04-22. Pull IRS Return Transcript to verify contents. Consult attorney re: Form 8857 (Innocent Spouse Relief). Details in encrypted case file.

Before attorney meeting

In Progress

Financial — liability exposure. See encrypted D000 case file.

Rack Relocation

Physical move of server rack. CR written: CR-2026-04-18 (pending in infra-ops). Borg backup completed. VM XML dumps, switch save, shutdown/startup procedure documented.

TBD

Pending

Infrastructure downtime — all services offline during move

D000 Legal Planning

Encrypted D000 case file. Open: d000 open dissolutio. Close: d000 close dissolutio. 19 partials + assembler. PDF build for attorney handoff. Critical deadline: Jan 2029.

Before Jan 2029

Active — escalating

Life transition — see case file for details

Credit Report Review

Pull reports from all 3 bureaus via annualcreditreport.com. Verify no unknown joint accounts or debts. Credentials in gopass: v3/personal/finance/credit/annual_credit_report

TBD

In Progress

Financial discovery — FL-142 preparation

Gopass Security Audit

Rotate passwords on shared/known accounts. Add 2FA backup codes to v3/personal/recovery/. Create missing government entries (IRS, SSA, VA, DMV). Add last_login field to active entries.

TBD

Pending

Digital security — pre-filing preparation

Subscription Audit

Download 3 months bank/CC statements (Chase, NFCU, USAA). Identify all recurring charges. Cancel unnecessary. Document active subscriptions for FL-150.

TBD

Pending

Financial — expense documentation

401(k) Enrollment

Enroll in CHLA 401(k) immediately. Post-separation contributions are 100% separate property. Reduces gross income for support calculations. Max 2026: $23,500/yr.

In progress (started 5/4)

In Progress

Financial — support calculation + retirement

URGENT — Performance Review Certifications

Certification Provider Deadline Status Impact

CISSP

ISC² — Certified Information Systems Security Professional

July 12, 2026

ACTIVE — Week 2 of 10 (Project)

Required for performance review. 10-week accelerated plan.

RHCSA 9

Red Hat Certified System Administrator

Q3 2026

ACTIVE — 21-phase curriculum (Project)

After CISSP. Required for performance review.

CISSP: 41 days remaining (exam July 12). Domain 1 study in progress. Schedule exam today (06-01).

Early Morning - 5:30am

Regex Training (CRITICAL CARRYOVER)

  • Session 3 - Character classes, word boundaries

  • Practice drills from regex-mastery curriculum

  • Status: 52 days carried over (since 2026-03-16) — CRITICAL

Regex training continues to slip. This is the foundation for all CLI mastery.

Daily Notes

CVE-2026-55200 — libssh2 RCE (CVSS 9.2)

CVE-2026-55200 — libssh2 Out-of-Bounds Write (RCE)

Summary

CVE

CVE-2026-55200

CVSS 4.0

9.2 CRITICAL (AV:N/AC:L/AT:P/PR:N/UI:N)

CVSS 3.1

8.1 HIGH (AV:N/AC:H/PR:N/UI:N)

Published

2026-06-17

Affected

libssh2 through 1.11.1

Fix

Commit 97acf3df — not yet in a tagged release

CWE

CWE-680 (Integer Overflow to Buffer Overflow)

Vector

Remote — crafted SSH packet with oversized packet_length corrupts heap

Mechanism: ssh2_transport_read() in transport.c fails to enforce upper bounds on the packet_length field. A remote attacker sends a crafted SSH packet with an excessively large packet_length, triggering an integer overflow that allocates less memory than expected, causing a heap buffer overflow. No authentication required — the attack occurs during the SSH handshake before authentication completes.

Scope Assessment — Does This Need a Project?

No. This is a triage-and-delegate, not a remediation project.

  • No patch exists yet. The fix commit is not in a tagged release. No distro has shipped an update. There is nothing to deploy.

  • Windows is not affected. Windows does not ship libssh2. Win32-OpenSSH, PuTTY, WinSCP — all use their own SSH implementations.

  • OpenSSH is not affected. ssh, scp, sftp, sshd — all safe. Different codebase entirely.

  • The attack requires outbound client connections to a malicious SSH server via libssh2. This is not remotely exploitable against our SSH listeners.

Your role: Triage analyst. Hand the server team the detection script + one-liner mitigation. They execute. You monitor for the distro patch. When it drops, it’s apt upgrade / dnf update / zypper update — routine patching, not an incident.

What Is and Is NOT Affected
Tool / Platform Affected? Why

Windows (all versions)

NO

Does not ship libssh2. Win32-OpenSSH, PuTTY, WinSCP use their own implementations.

ssh, scp, sftp (CLI binaries)

NO

OpenSSH binaries — completely separate codebase from libssh2

sshd (SSH server)

NO

OpenSSH server — does not use libssh2

curl …​;

NO

HTTPS does not invoke libssh2 — only sftp:// and scp:// protocols trigger it

curl sftp:// or curl scp://

YES

curl links libssh2 for SSH-based protocols

git push/pull over SSH

NO

git CLI shells out to OpenSSH binary, not libssh2

libgit2-based tools (some GUIs, Rust cargo)

YES

libgit2 links libssh2 directly for SSH transport

nmap --script ssh-*

YES

nmap links libssh2 for SSH service probes

Python paramiko

NO

paramiko uses libssh (different project) or pure Python — not libssh2

Ansible over SSH

NO

Uses paramiko or OpenSSH — not libssh2

WSL distros

CHECK

Each WSL instance has its own Linux packages — check each independently

Detection Commands — By Distro

Every command below works over SSH. Run from your workstation or distribute the triage script.

RHEL / Rocky / Alma / CentOS
# Version check
rpm -qa 'libssh2*'
# Example output: libssh2-1.11.0-2.el9.x86_64

# Reverse dependencies — what packages require libssh2
dnf repoquery --installed --whatrequires libssh2

# .so version string (works even if rpm metadata is stale)
strings /usr/lib64/libssh2.so.1 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+'

# What binaries link it
for bin in /usr/bin/curl /usr/bin/git /usr/bin/nmap /usr/bin/ssh /usr/bin/scp; do
  [ -x "$bin" ] && {
    printf "%-20s " "$bin"
    ldd "$bin" 2>/dev/null | grep -q libssh2 \
      && echo "LINKS libssh2 — AFFECTED" \
      || echo "safe — does NOT link libssh2"
  }
done

# Check libcurl.so separately (anything using libcurl inherits)
ldd /usr/lib64/libcurl.so.4 2>/dev/null | grep libssh2

# Patch availability
dnf check-update libssh2

# Apply patch when available
sudo dnf update libssh2
Debian / Ubuntu
# Version check
dpkg -l 'libssh2*' 2>/dev/null | awk '/^ii/{print $2, $3}'
# Example output: libssh2-1:amd64 1.11.0-4

# Reverse dependencies
apt rdepends --installed libssh2-1 2>/dev/null

# .so version string
strings /usr/lib/x86_64-linux-gnu/libssh2.so.1 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+'

# What binaries link it
for bin in /usr/bin/curl /usr/bin/git /usr/bin/nmap /usr/bin/ssh /usr/bin/scp; do
  [ -x "$bin" ] && {
    printf "%-20s " "$bin"
    ldd "$bin" 2>/dev/null | grep -q libssh2 \
      && echo "LINKS libssh2 — AFFECTED" \
      || echo "safe — does NOT link libssh2"
  }
done

# Check libcurl
ldd /usr/lib/x86_64-linux-gnu/libcurl.so.4 2>/dev/null | grep libssh2

# Patch availability
sudo apt update && apt list --upgradable 2>/dev/null | grep libssh2

# Apply patch when available
sudo apt upgrade libssh2-1
openSUSE / SLES
# Version check
rpm -qa 'libssh2*'
# Example output: libssh2-1-1.11.0-150000.4.22.1.x86_64

# SUSE uses libssh2-1 as the package name (note the -1 suffix)
zypper info libssh2-1

# Reverse dependencies
zypper search --requires libssh2-1

# .so version string — SUSE puts libs in /usr/lib64
strings /usr/lib64/libssh2.so.1 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+'

# What binaries link it
for bin in /usr/bin/curl /usr/bin/git /usr/bin/nmap /usr/bin/ssh /usr/bin/scp; do
  [ -x "$bin" ] && {
    printf "%-20s " "$bin"
    ldd "$bin" 2>/dev/null | grep -q libssh2 \
      && echo "LINKS libssh2 — AFFECTED" \
      || echo "safe — does NOT link libssh2"
  }
done

# Check libcurl
ldd /usr/lib64/libcurl.so.4 2>/dev/null | grep libssh2

# Patch availability
zypper list-updates | grep libssh2

# Apply patch when available
sudo zypper update libssh2-1

# SUSE-specific: check SUSE Security Announcements
# https://www.suse.com/security/cve/CVE-2026-55200.html
WSL Detection

WSL instances run their own Linux userland with their own package managers. Each WSL distro must be checked independently.

WSL — From Inside the Instance
# Identify distro
cat /etc/os-release | awk -F= '/^PRETTY_NAME/{print $2}'

# Confirm you're in WSL (not bare metal)
grep -qi microsoft /proc/version && echo "WSL detected" || echo "Bare metal / VM"

# Then run the Debian/Ubuntu or RHEL commands above based on distro

# WSL-specific: check if Windows-side tools are aliased into WSL
which curl && file $(which curl)
# /usr/bin/curl = Linux curl (may link libssh2) — CHECK
# /mnt/c/.../curl.exe = Windows curl (WinHTTP, not libssh2) — SAFE

which ssh && file $(which ssh)
# /usr/bin/ssh = Linux OpenSSH — SAFE (not libssh2)
# /mnt/c/.../ssh.exe = Windows OpenSSH — SAFE (not libssh2)
WSL — From PowerShell (Enumerate All Distros)
# List all WSL instances
wsl -l -v

# Run version check across all WSL distros
foreach ($distro in (wsl -l -q | Where-Object { $_.Trim() -ne '' })) {
    $d = $distro.Trim()
    Write-Host "=== WSL: $d ==="
    wsl -d $d -- bash -c '
      echo "  OS: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d \")"
      VER=$(find /usr/lib /usr/lib64 /usr/lib/x86_64-linux-gnu -name "libssh2.so*" -type f 2>/dev/null | head -1 | xargs strings 2>/dev/null | grep -oP "libssh2_\K[0-9.]+")
      if [ -z "$VER" ]; then
        echo "  libssh2: NOT INSTALLED — not affected"
      elif [ "$(printf "%s\n" "1.11.1" "$VER" | sort -V | tail -1)" = "1.11.1" ]; then
        echo "  libssh2: $VER — VULNERABLE"
      else
        echo "  libssh2: $VER — PATCHED"
      fi
      printf "  curl: " && (ldd /usr/bin/curl 2>/dev/null | grep -q libssh2 && echo "links libssh2" || echo "clean")
    '
    Write-Host ""
}
Windows — Check for Bundled libssh2.dll (Run in PowerShell as Admin)
# Windows does NOT ship libssh2 natively
# But some third-party tools may bundle libssh2.dll
Get-ChildItem -Path "C:\Program Files","C:\Program Files (x86)","C:\ProgramData" `
  -Filter "libssh2*" -Recurse -ErrorAction SilentlyContinue |
  Select-Object FullName, Length, LastWriteTime

# Check Git for Windows (uses its own OpenSSH, but verify)
& "C:\Program Files\Git\usr\bin\ssh.exe" -V 2>&1
# Should show OpenSSH — not libssh2

# Check if any installed app bundles a vulnerable libssh2
# If Get-ChildItem returns nothing — Windows is clean
Universal .so Detection (Any Distro)

When you don’t know the distro or package manager, these work everywhere:

# Find the library on disk — covers all lib paths
find / -name 'libssh2.so*' -type f 2>/dev/null

# Extract version from whatever .so you find
find / -name 'libssh2.so*' -type f 2>/dev/null \
  | head -1 | xargs strings 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+'

# Find every binary and library on the system that links it
find /usr/bin /usr/sbin /usr/lib /usr/lib64 /usr/lib/x86_64-linux-gnu \
  -type f 2>/dev/null | xargs ldd 2>/dev/null | grep -B1 'libssh2' \
  | awk '/^\// {gsub(/:$/,""); print}' | sort -u

# Find running processes with libssh2 loaded (no lsof needed)
grep -rl 'libssh2' /proc/*/maps 2>/dev/null \
  | awk -F/ '{print $3}' | while read pid; do
    printf "  PID %-8s %s\n" "$pid" "$(cat /proc/$pid/comm 2>/dev/null)"
  done | sort -u
Mitigation Commands (Pre-Patch, All Distros)
Block curl sftp/scp protocols — per user
printf '%s\n' '--proto -sftp -scp' >> ~/.curlrc

# Verify
curl sftp://localhost 2>&1 | head -1
# Expected: "Protocol "sftp" not supported or disabled in libcurl"

# Undo after patching
sed -i '/--proto -sftp -scp/d' ~/.curlrc
Block curl sftp/scp protocols — system-wide (all users on a host)
printf '%s\n' '--proto -sftp -scp' | sudo tee -a /etc/curlrc

# Verify as another user
sudo -u nobody curl sftp://localhost 2>&1 | head -1
Deploy curlrc mitigation across multiple hosts in one shot
HOSTS=(
  "server1.example.com"
  "server2.example.com"
  # Add all Linux hosts here
)

for host in "${HOSTS[@]}"; do
  echo "=== $host ==="
  ssh "$host" 'grep -q "proto.*-sftp" /etc/curlrc 2>/dev/null \
    || printf "%s\n" "--proto -sftp -scp" | sudo tee -a /etc/curlrc && echo "  curlrc applied"' \
    2>/dev/null || echo "  FAILED"
done
Deploy curlrc mitigation across WSL instances (from PowerShell)
foreach ($distro in (wsl -l -q | Where-Object { $_.Trim() -ne '' })) {
    $d = $distro.Trim()
    Write-Host "=== WSL: $d ==="
    wsl -d $d -- bash -c 'printf "%s\n" "--proto -sftp -scp" >> ~/.curlrc && echo "  curlrc applied for $(whoami)"'
}
Build patched libssh2 from source (if urgency exceeds distro package wait)
# Install build deps first
# Debian/Ubuntu: sudo apt install cmake gcc libssl-dev zlib1g-dev
# RHEL/Rocky:    sudo dnf install cmake gcc openssl-devel zlib-devel
# openSUSE:      sudo zypper install cmake gcc libopenssl-devel zlib-devel

cd /tmp
git clone https://github.com/libssh2/libssh2.git
cd libssh2
git log --oneline -1 97acf3df   # Verify fix commit exists

mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_SHARED_LIBS=ON \
  -DCRYPTO_BACKEND=OpenSSL
make -j$(nproc)

# Verify the fix — bounds check should be present in transport.c
grep -A5 'packet_length' ../src/transport.c | head -20

# Back up current library
SOFILE=$(find /usr/lib /usr/lib64 /usr/lib/x86_64-linux-gnu \
  -name 'libssh2.so.1.0.*' -type f 2>/dev/null | head -1)
sudo cp "$SOFILE" "${SOFILE}.bak.$(date +%F)"

# Install
sudo make install
sudo ldconfig

# Verify
strings /usr/lib64/libssh2.so.1 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+'
strings /usr/lib/x86_64-linux-gnu/libssh2.so.1 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+'
Infrastructure Triage Script (d001 — Run On Each Host)

This script auto-detects distro, package manager, and library paths. Works on RHEL, Debian, Ubuntu, openSUSE, SLES, and WSL.

Save to /tmp/cve-2026-55200-check.sh
#!/usr/bin/env bash
# cve-2026-55200-check.sh — portable triage for any Linux distro + WSL
# Usage: scp cve-2026-55200-check.sh user@host:/tmp/ && ssh user@host 'bash /tmp/cve-2026-55200-check.sh'
set -euo pipefail

HOST=$(hostname -f 2>/dev/null || hostname)
OS=$(cat /etc/os-release 2>/dev/null | awk -F= '/^PRETTY_NAME/{print $2}' | tr -d '"')
echo "=== CVE-2026-55200 triage: $HOST ==="
echo "    OS: $OS"

# WSL detection
if grep -qi microsoft /proc/version 2>/dev/null; then
  echo "    Environment: WSL"
else
  echo "    Environment: Bare metal / VM"
fi

# 1. Version check — try all package managers
echo -e "\n[1] libssh2 version:"
if command -v dpkg &>/dev/null; then
  dpkg -l 'libssh2*' 2>/dev/null | awk '/^ii/{print "  "$2, $3}' || echo "  Not installed (dpkg)"
elif command -v rpm &>/dev/null; then
  rpm -qa 'libssh2*' 2>/dev/null | sed 's/^/  /' | grep . || echo "  Not installed (rpm)"
elif command -v pacman &>/dev/null; then
  pacman -Qi libssh2 2>/dev/null | awk '/^Version/' | sed 's/^/  /' || echo "  Not installed (pacman)"
fi

# .so version string — works regardless of package manager
# Covers: /usr/lib64 (RHEL, SUSE), /usr/lib/x86_64-linux-gnu (Debian/Ubuntu), /usr/lib (Arch)
SOPATH=$(find /usr/lib /usr/lib64 /usr/lib/x86_64-linux-gnu -name 'libssh2.so*' -type f 2>/dev/null | head -1)
if [[ -n "$SOPATH" ]]; then
  echo "  .so path: $SOPATH"
  strings "$SOPATH" 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+' | head -1 | sed 's/^/  .so version: /'
else
  echo "  libssh2.so not found on disk"
fi

# 2. What links it
echo -e "\n[2] What uses libssh2:"
for bin in /usr/bin/curl /usr/bin/git /usr/bin/nmap /usr/bin/ssh /usr/bin/scp /usr/bin/sftp; do
  if [ -x "$bin" ]; then
    printf "  %-20s " "$(basename "$bin")"
    ldd "$bin" 2>/dev/null | grep -q libssh2 \
      && echo "LINKS libssh2 — AFFECTED" \
      || echo "does NOT link libssh2 — safe"
  fi
done

# Check shared libraries — cover all distro lib paths
for lib in \
  /usr/lib64/libcurl.so.4 \
  /usr/lib/x86_64-linux-gnu/libcurl.so.4 \
  /usr/lib/libcurl.so.4 \
  /usr/lib64/libgit2.so* \
  /usr/lib/x86_64-linux-gnu/libgit2.so* \
  /usr/lib/libgit2.so*; do
  [ -f "$lib" ] 2>/dev/null && {
    printf "  %-50s " "$lib"
    ldd "$lib" 2>/dev/null | grep -q libssh2 \
      && echo "LINKS libssh2" || echo "clean"
  }
done 2>/dev/null

# 3. Running processes with libssh2 loaded
echo -e "\n[3] Processes with libssh2 loaded:"
if command -v lsof &>/dev/null; then
  PROCS=$(lsof 2>/dev/null | awk '/libssh2\.so/{print "  "$1, "(PID "$2")"}' | sort -u)
  [[ -n "$PROCS" ]] && echo "$PROCS" || echo "  None"
else
  # Fallback — works without lsof
  FOUND=0
  grep -rl 'libssh2' /proc/*/maps 2>/dev/null \
    | awk -F/ '{print $3}' | sort -u | while read pid; do
      printf "  PID %-8s %s\n" "$pid" "$(cat /proc/$pid/comm 2>/dev/null)"
      FOUND=1
    done
  [[ $FOUND -eq 0 ]] && echo "  None"
fi

# 4. Active outbound SSH connections
echo -e "\n[4] Outbound SSH connections (port 22):"
CONNS=$(ss -tnp 2>/dev/null | awk '/:22 /{print "  "$0}')
[[ -n "$CONNS" ]] && echo "$CONNS" || echo "  None"

# 5. curl protocol check
echo -e "\n[5] curl SSH protocol support:"
if command -v curl &>/dev/null; then
  PROTOS=$(curl -V 2>/dev/null | grep -oE 'sftp|scp' | tr '\n' ' ')
  [[ -n "$PROTOS" ]] && echo "  Enabled: $PROTOS" || echo "  sftp/scp NOT in curl protocols"
  # Check curlrc mitigation
  for rc in ~/.curlrc /etc/curlrc; do
    [ -f "$rc" ] && grep -q 'proto.*-sftp' "$rc" \
      && echo "  $rc mitigation: ACTIVE" \
      || echo "  $rc mitigation: NOT SET"
  done
else
  echo "  curl not installed"
fi

# 6. Cron/systemd check — any automated curl sftp/scp usage?
echo -e "\n[6] Automated sftp/scp usage (cron, systemd):"
CRON_HITS=$(grep -rn 'curl.*\(sftp\|scp\)://' /etc/cron* /var/spool/cron 2>/dev/null | head -5)
[[ -n "$CRON_HITS" ]] && echo "$CRON_HITS" | sed 's/^/  /' || echo "  None found in cron"

# 7. Patch availability
echo -e "\n[7] Patch availability:"
if command -v apt &>/dev/null; then
  apt list --upgradable 2>/dev/null | grep -q libssh2 \
    && echo "  PATCH AVAILABLE — run: sudo apt upgrade libssh2-1" \
    || echo "  No Debian/Ubuntu patch yet"
elif command -v dnf &>/dev/null; then
  dnf check-update libssh2 2>/dev/null | grep -q libssh2 \
    && echo "  PATCH AVAILABLE — run: sudo dnf update libssh2" \
    || echo "  No RHEL/Rocky patch yet"
elif command -v zypper &>/dev/null; then
  zypper list-updates 2>/dev/null | grep -q libssh2 \
    && echo "  PATCH AVAILABLE — run: sudo zypper update libssh2-1" \
    || echo "  No SUSE patch yet"
elif command -v checkupdates &>/dev/null; then
  checkupdates 2>/dev/null | grep -q libssh2 \
    && echo "  PATCH AVAILABLE — run: sudo pacman -Syu" \
    || echo "  No Arch patch yet"
else
  echo "  Unknown package manager — check manually"
fi

# 8. Verdict
echo -e "\n[8] VERDICT:"
VER=$(strings "${SOPATH:-/dev/null}" 2>/dev/null | grep -oP 'libssh2_\K[0-9.]+' | head -1)
if [[ -z "$SOPATH" || -z "$VER" ]]; then
  echo "  NOT AFFECTED — libssh2 not found on $HOST"
elif printf '%s\n' "1.11.2" "$VER" | sort -V | head -1 | grep -q "1.11.2"; then
  echo "  PATCHED — libssh2 $VER on $HOST (> 1.11.1)"
else
  echo "  VULNERABLE — libssh2 $VER on $HOST (≤ 1.11.1)"
  echo "  Action required: apply curlrc mitigation + patch when available"
fi

echo -e "\n=== End triage: $HOST ==="
Deployment Commands
Deploy and run across multiple Linux hosts
HOSTS=(
  # Work servers — add your actual hostnames
  "rhel-server-01"
  "ubuntu-jump-01"
  "suse-db-01"
  "debian-build-01"
)

for host in "${HOSTS[@]}"; do
  scp /tmp/cve-2026-55200-check.sh "$host":/tmp/ 2>/dev/null \
    && ssh "$host" 'bash /tmp/cve-2026-55200-check.sh' 2>/dev/null \
    || echo "=== FAILED to reach $host ==="
  echo
done | tee /tmp/cve-2026-55200-results-$(date +%F).txt

# Review results — just the verdicts
awk '/VERDICT/,/End triage/' /tmp/cve-2026-55200-results-$(date +%F).txt
Run across all WSL instances (from PowerShell)
foreach ($distro in (wsl -l -q | Where-Object { $_.Trim() -ne '' })) {
    $d = $distro.Trim()
    Write-Host "=== WSL: $d ==="
    wsl -d $d -- bash /tmp/cve-2026-55200-check.sh
    Write-Host ""
}
Deploy curlrc mitigation + run triage in one pass
HOSTS=(
  "rhel-server-01"
  "ubuntu-jump-01"
  "suse-db-01"
)

for host in "${HOSTS[@]}"; do
  echo "=== $host ==="
  scp /tmp/cve-2026-55200-check.sh "$host":/tmp/ 2>/dev/null
  ssh "$host" '
    # Apply mitigation (idempotent)
    grep -q "proto.*-sftp" /etc/curlrc 2>/dev/null \
      || printf "%s\n" "--proto -sftp -scp" | sudo tee -a /etc/curlrc
    echo "  curlrc mitigation applied"
    # Run triage
    bash /tmp/cve-2026-55200-check.sh
  ' 2>/dev/null || echo "  FAILED to reach $host"
  echo
done | tee /tmp/cve-2026-55200-mitigated-$(date +%F).txt
Teams Message

Copy from /tmp/cve-2026-55200-teams-message.txt or from the block below:

CVE-2026-55200 — libssh2 RCE (CVSS 9.2) — Triage Complete

WHAT: Heap buffer overflow in libssh2 ≤ 1.11.1. RCE via crafted SSH packet
during handshake. No auth required.

NOT AFFECTED:
  - Windows (all versions) — does not ship libssh2
  - OpenSSH (ssh, scp, sftp, sshd) — different codebase
  - PuTTY, WinSCP — own SSH implementations
  - curl over HTTPS — only sftp:// and scp:// protocols trigger it
  - Ansible — uses paramiko/OpenSSH, not libssh2

AFFECTED:
  - Linux hosts with libssh2 ≤ 1.11.1 installed (most ship it as curl dep)
  - Only when curl sftp:// or curl scp:// is used — not normal curl/ssh
  - WSL instances — each has its own packages, check independently

CURRENT STATUS:
  - No distro has shipped a patch yet (RHEL, Ubuntu, Debian, SUSE — all pending)
  - Fix commit exists upstream but no tagged release

IMMEDIATE MITIGATION (one-liner, all Linux hosts):
  echo '--proto -sftp -scp' | sudo tee -a /etc/curlrc

DETECTION:
  Triage script attached — run on each host:
    scp cve-2026-55200-check.sh user@host:/tmp/
    ssh user@host 'bash /tmp/cve-2026-55200-check.sh'
  Auto-detects distro (RHEL, Debian, Ubuntu, SUSE, WSL).
  Checks version, maps dependencies, gives VULNERABLE/PATCHED/NOT AFFECTED verdict.

RISK CONTEXT:
  This is NOT remotely exploitable against SSH servers. The attacker must get
  the victim to connect outbound to a malicious SSH server via libssh2
  (not OpenSSH). Practical risk is low unless hosts run automated curl sftp://
  transfers to external endpoints.

ACTION ITEMS:
  1. Server team: run triage script on Linux hosts + WSL instances
  2. Server team: apply curlrc mitigation on affected hosts
  3. All: monitor distro security trackers for patch
  4. Cisco team: check PSIRT for ISE/FMC/ASA advisories
  5. No Windows action needed

REFERENCES:
  - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-55200
  - Fix commit: https://github.com/libssh2/libssh2/commit/97acf3dfda80c91c3a8c9f2372546301d4a1a7a8
  - VulnCheck: https://www.vulncheck.com/advisories/libssh2-out-of-bounds-write-via-unchecked-packet-length-in-transport-c
  - Red Hat: https://access.redhat.com/security/cve/CVE-2026-55200
  - SUSE: https://www.suse.com/security/cve/CVE-2026-55200.html
  - Debian: https://security-tracker.debian.org/tracker/CVE-2026-55200
  - Ubuntu: https://ubuntu.com/security/CVE-2026-55200
  - Cisco PSIRT: https://sec.cloudapps.cisco.com/security/center/publicationListing.x
Local Workstation Note (modestus-aw — Arch)

Personal workstation runs libssh2 1.11.1-1 — vulnerable but exposure is low. No curl sftp:// usage in history. OpenSSH used for all SSH sessions. Apply ~/.curlrc mitigation and monitor checkupdates for Arch patch.

Status
Item Status

Triage complete

■■■■■■■■■■ 10/10

Blast radius mapped

■■■■■■■■■■ 10/10

Actual exposure assessed — LOW

■■■■■■■■■■ 10/10

Triage script written (all distros)

■■■■■■■■■■ 10/10

Teams message drafted

■■■■■■■■■■ 10/10

Teams message sent

□□□□□□□□□□ 0/10 — copy from /tmp/cve-2026-55200-teams-message.txt

curlrc mitigation on personal workstation

□□□□□□□□□□ 0/10 — printf '%s\n' '--proto -sftp -scp' >> ~/.curlrc

d001 server team runs triage script

□□□□□□□□□□ 0/10 — hand off script

WSL instances checked

□□□□□□□□□□ 0/10 — hand off PowerShell command

Cisco PSIRT check (ISE, FMC, ASA)

□□□□□□□□□□ 0/10

Distro patches available

□□□□□□□□□□ 0/10 — RHEL, Debian, Ubuntu, SUSE all pending

YubiKey Provisioning — age Key Rotation

No — do not reveal the contents of ~/.age/identities.

Directory names and permissions are fine. This is safe:

ls -la ~/.age
find ~/.age -maxdepth 2 -type f -printf '%M %u %g %p\n'

This is not safe to paste:

cat ~/.age/identities/*
cat ~/.age/recipients/*

Especially never paste anything that starts like:

AGE-SECRET-KEY-...

Age’s model is: encrypt to recipients, decrypt with identities; an identity is the private side. age-plugin-yubikey adds identities stored on YubiKeys, so the YubiKey performs the private-key operation instead of leaving a normal private key as your daily-use decryptor. ([GitHub][1])

Safe plan

0. Freeze first

Before changing anything:

chmod 700 ~/.age ~/.age/identities ~/.age/recipients
find ~/.age -type f -exec chmod 600 {} \;

Do a metadata-only inventory:

find ~/.age -maxdepth 2 -type f -printf '%M %u %g %s %TY-%Tm-%Td %TH:%TM %p\n'

Safe. This shows filenames, sizes, and permissions, not secrets.

1. Check for dangerous raw private keys

This does not print the key. It only prints filenames that contain raw age private keys:

grep -RIl '^AGE-SECRET-KEY-' ~/.age/identities ~/.age/recipients 2>/dev/null

If anything appears under ~/.age/identities, treat it as crown-jewel material.

2. Set up the YubiKey safely first

Your earlier output showed FIDO PIN not set and PIV defaults still active. Fix those before using it for serious secrets.

ykman fido access change-pin
ykman piv access change-pin
ykman piv access change-puk
ykman piv access change-management-key --generate --protect

Yubico documents that YubiKey 5 supports separate applications including FIDO2, OATH, OpenPGP, OTP, PIV, and U2F, and ykman is the CLI for managing them. ([Yubico Documentation][2]) Your PIV warnings matter because the PIV PIN/PUK/management key are the administrative controls for PIV operations. ([Yubico Documentation][3])

3. Create a YubiKey-backed age identity

Install if needed:

sudo pacman -S age age-plugin-yubikey

Create a new identity file:

mkdir -p ~/.age/identities ~/.age/recipients
chmod 700 ~/.age ~/.age/identities ~/.age/recipients

age-plugin-yubikey --generate > ~/.age/identities/yubikey-main.txt
chmod 600 ~/.age/identities/yubikey-main.txt

Then get the recipient line without exposing private key material:

age-plugin-yubikey --list > ~/.age/recipients/yubikey-main.txt
chmod 600 ~/.age/recipients/yubikey-main.txt

gopass’s age backend docs also explicitly note that using age with a YubiKey requires age-plugin-yubikey, and that a decrypt may wait for your YubiKey touch if touch policy is enabled. ([GitHub][4])

4. Test on a fake file

Do not test first on your real password store.

tmp="$(mktemp -d)"
printf 'hello yubikey\n' > "$tmp/plain.txt"

age -R ~/.age/recipients/yubikey-main.txt \
  -o "$tmp/plain.txt.age" \
  "$tmp/plain.txt"

age -d -i ~/.age/identities/yubikey-main.txt \
  "$tmp/plain.txt.age"

Expected output:

hello yubikey

5. Add backup before migration

Do not make one Nano your only decryptor.

Minimum safe recipient set:

daily YubiKey recipient
offline recovery age recipient
existing old age recipient temporarily

Better:

daily YubiKey recipient
backup YubiKey recipient
offline recovery age recipient

Generate an offline recovery identity and do not keep it on the daily machine:

age-keygen -o /tmp/offline-recovery-age-key.txt
grep '^# public key:' /tmp/offline-recovery-age-key.txt

The line after # public key: is safe to use as a recipient. The file itself is private.

6. Re-encrypt gradually

For standalone age files, re-encrypt one test secret first. Pattern:

old_id="$HOME/.age/identities/personal.key"
yk_id="$HOME/.age/identities/yubikey-main.txt"
yk_recips="$HOME/.age/recipients/yubikey-main.txt"

age -d -i "$old_id" secret.txt.age > /tmp/secret.txt

age -R "$yk_recips" \
  -r 'age1_YOUR_OFFLINE_RECOVERY_RECIPIENT_HERE' \
  -o /tmp/secret.txt.age.new \
  /tmp/secret.txt

age -d -i "$yk_id" /tmp/secret.txt.age.new >/dev/null &&
mv /tmp/secret.txt.age.new secret.txt.age

shred -u /tmp/secret.txt 2>/dev/null || rm -f /tmp/secret.txt

Do this slowly. Verify each decrypt before replacing.

7. For gopass

First identify backend and recipients without printing secrets:

gopass config
gopass recipients
gopass mounts

If your gopass store is already using age, follow the gopass age backend flow and add the YubiKey identity/recipient. If it is GPG-backed, do not mix changes casually; migrate one mount/store deliberately.

Safe approach:

1. Export/backup current gopass store.
2. Add YubiKey-backed age recipient.
3. Re-encrypt store.
4. Test show/copy on several entries.
5. Sync.
6. Only then remove old daily-use disk identity.

8. Remove the old disk key from daily use only after testing

Do not delete it immediately.

Move it to an offline encrypted backup:

mkdir -p ~/age-key-retirement
chmod 700 ~/age-key-retirement

mv ~/.age/identities/personal.key ~/age-key-retirement/personal.key.RETIRED
chmod 600 ~/age-key-retirement/personal.key.RETIRED

Then test that your normal workflow still works with only the YubiKey identity.

Final target:

~/.age/identities/yubikey-main.txt     okay, daily use
~/.age/recipients/yubikey-main.txt     okay
raw AGE-SECRET-KEY file                not on daily machine
gopass encrypted store                 still on disk/Git
YubiKey                                required to decrypt

The simple rule: recipient files can be public-ish, identity files are private, and anything containing AGE-SECRET-KEY must not be revealed.

Monad Pipeline v3 Deployment

Monad Pipeline v3 Deployment

Kenneth lifted billing 3T/3O limit (2026-06-19). Full v3 graph deployed via PATCH to chlxsyslog-pipeline (2016ea3e). 18 nodes, 8 transforms, 17 edges. ISE 3-tier classification, ASA perimeter routing, per-source enrichment.

Deployment Sequence
Step 1 — Load credentials
dsource d001 lab/network
API="${MONAD_API_URL}/v2/${MONAD_ORG_ID}"
Step 2 — POST v3 pipeline (billing limit test)
curl -sS -X POST -H "X-API-Key: ${MONAD_API_KEY}" -H "Content-Type: application/json" \
  -d @/tmp/chlxsyslog-v3-full.json \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines" | \
  tee /tmp/monad-v3-response.json | \
  jq '{id, name, nodes: (.nodes | length), edges: (.edges | length)}'
Output
id: 7244d3bb-c785-4318-87a9-8376abb84ad1
name: chlxsyslog-pipeline-v3
nodes: 18, edges: 17
Step 3 — Verify v3 architecture
bash data/d001/projects/siem-qradar-to-sentinel/scripts/verify-v3.sh | \
  tee /tmp/monad-v3-verify.txt
Output — all checks passed
✓ asa-vpn-to-perimeter → hot-sentinel-perimeter
✓ asa-failover-to-perimeter → hot-sentinel-perimeter
✓ asa-acl-to-perimeter → hot-sentinel-perimeter
Nodes: 18 (expected 18)
Edges: 17 (expected 17)
✓ ALL CHECKS PASSED
Step 4 — Input binding discovery
v3 pipeline created but received zero traffic.
v2 re-enabled: traffic flows immediately.
Monad input components bind per pipeline instance — POST does not inherit TLS connection.
Step 5 — PATCH v2 with v3 graph (preserves input binding)
V2_PID="2016ea3e-3571-4e42-aacf-b459effc2a64"
curl -sS -X PATCH -H "X-API-Key: ${MONAD_API_KEY}" -H "Content-Type: application/json" \
  -d @/tmp/chlxsyslog-v2-patch.json \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines/${V2_PID}" | \
  jq '{id, name, nodes: (.nodes | length), edges: (.edges | length)}'
Output
id: 2016ea3e, nodes: 18, edges: 17 — traffic flowing
Step 6 — Disable v3 orphan via API
V3_PID="7244d3bb-c785-4318-87a9-8376abb84ad1"
jq '.enabled = false' /tmp/monad-v3-response.json > /tmp/monad-v3-disable.json
curl -sS -X PATCH -H "X-API-Key: ${MONAD_API_KEY}" -H "Content-Type: application/json" \
  -d @/tmp/monad-v3-disable.json \
  "${MONAD_API_URL}/v2/${MONAD_ORG_ID}/pipelines/${V3_PID}" | \
  jq '{id, name, enabled}'
Transform Fixes

Three transforms had tag mismatches discovered during audit.

Fix all three + full audit
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-fix-transforms.sh fix-ise-cold
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-fix-transforms.sh fix-win-tier
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-fix-transforms.sh fix-asa-acl
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-fix-transforms.sh audit-all | tee /tmp/monad-transform-audit.txt
Transform Fix Before → After Status

ISE cold

environment

lab → production

PASS

Windows

retention_tier, severity_tier

hot/high → cold/medium

PASS

ASA ACL

retention_tier, severity_tier

cold/medium → hot/high

PASS

Final Transform Audit
Transform source_type environment retention severity

ise-hot

ise-auth

production

hot

high

ise-cold

ise

production

cold

asa-drop-noise

asa-vpn-extract

asa-vpn

production

hot

high

asa-acl-extract

asa-acl

production

hot

high

asa-failover-extract

asa-failover

production

hot

high

win-logon-extract

windows-security

production

cold

medium

switch-extract

switch

production

cold

medium

Traffic Verification (post-PATCH)
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh traffic v2
Output
input-syslog               in=3991  out=4619
ise-hot-extract            in=203   out=203
ise-hot-to-sentinel        in=203   out=203
ise-cold-extract           in=11    out=11
ise-cold-to-datalake       in=11    out=11
ise-noise-to-discard       in=65    out=65
win-logon-extract          in=1924  out=1924
catch-all-to-datalake      in=531   out=531
asa-drop-noise             in=30    out=30
Live Event Observation
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh observe v2 input-syslog
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh observe v2 ise-hot-extract
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh observe v2 win-logon-extract

Confirmed live: ISE auth (CISE_Authentication_Flow, CISE_RADIUS_Accounting), Windows Security logon events, Monad agent MetricsExtension heartbeats in catch-all.

Pipeline Final State
Pipeline Status Purpose

chlxsyslog-pipeline (2016ea3e)

Enabled — v3 graph via PATCH

ISE 3-tier + ASA perimeter + per-source enrichment

chlxsyslog-pipeline-v3 (7244d3bb)

Disabled — orphan

Reference only

saas-pipeline (7c7a96b0)

Enabled

Mind DLP hot/cold

Lesson Learned

Monad input components bind per pipeline instance. POST creates a new instance with no TLS connection inheritance. To modify a live pipeline: PATCH the existing pipeline, do not POST a replacement.

Scripts Created
Script Purpose

scripts/monad-ops.sh

Pipeline operations toolkit (pipelines, traffic, edges, outputs, observe, deploy)

scripts/monad-output-config.sh

3-freeway output conversion (dev-null → azure-monitor when DCR values arrive)

scripts/monad-fix-transforms.sh

Transform tag fixes + full audit

scripts/verify-v3.sh

v3 graph validation (ASA routing, node count, edge count)

scripts/sentinel-values.env.template

DCR values template for Victor + Mauricio

Next Steps
  • Verify ASA output nodes receiving traffic (Monday — no ASA traffic on Sunday)

  • Investigate catch-all content (531 records — Monad agent metrics, confirm no misrouted logs)

  • Victor + Mauricio: create 3 DCRs (identity, perimeter, datalake) for Sentinel output

  • When DCR values arrive: bash scripts/monad-output-config.sh check-prereqsbuild-payloadsupdate-outputs

  • d001 close before commit

Monad Pipeline — Clean Graph Architecture

Monad Pipeline — Clean Graph Architecture

Restructured chlxsyslog-pipeline (2016ea3e) for clarity. 1 edge per log source from CHLXSYSLOG01. ISE fans out via router to hot/cold/discard. Engineers and management see a clean tree, not spaghetti.

Architecture
CHLXSYSLOG01 (input-syslog)
  │
  ├── [ise-route] → ise-router (pass-through)
  │     ├── [ise-hot-route] → ise-hot-extract → hot-sentinel-identity
  │     │     8 categories: Passed_Auth, Failed_Attempts, Auth_Flow,
  │     │     RADIUS_Accounting, TACACS_Accounting, Administrator,
  │     │     Threat_Centric, Guest
  │     │
  │     ├── [ise-cold-route] → ise-cold-extract → cold-datalake
  │     │     9 categories: Profiler, Posture, Policy_Diagnostics,
  │     │     Identity_Stores, AD_Connector, Administrative,
  │     │     PassiveID, ACI, MyDevices
  │     │
  │     └── [ise-noise-route] → discarded
  │           10 categories: System_Statistics, Internal_Operations,
  │           External_MDM, Internal_MDM, Distributed_Management,
  │           Licensing, RADIUS_Diagnostics, TACACS_Diagnostics,
  │           MDM_Diagnostics, IPSec
  │
  ├── [asa-route] → asa-extract → hot-sentinel-perimeter
  │     condition: msg contains "%ASA-" OR hostname starts_with "USC-TEST"
  │
  ├── [win-route] → win-logon-extract → cold-datalake
  │     condition: app-name contains "MSWinEventLog"
  │
  ├── [switch-route] → switch-extract → cold-datalake
  │     condition: hostname contains "10.193.144.249"
  │
  └── [all-other-logs] → cold-datalake (catch-all)
        condition: NOR (excludes CISE_, MSWinEventLog, %ASA-, USC-TEST, switch IP)
3 Freeways (Output Destinations)
Freeway Sources Component ID Type

hot-sentinel-identity

ISE auth events (8 categories)

4f0d8d17

dev-null (→ Sentinel DCR when ready)

hot-sentinel-perimeter

ASA VPN/ACL/failover

8d63ad70

dev-null (→ Sentinel DCR when ready)

cold-datalake

ISE cold + Windows + Switch + catch-all

99777f5c

dev-null (→ Sentinel DCR when ready)

discarded

ISE internals (10 categories)

147a5271

dev-null (permanent)

Before / After
Metric Before After

Nodes

18

14

Edges

17

13

Edges from input

9 (3 duplicate ASA)

5 (1 per source)

ISE paths from input

3

1 (via router)

ASA paths

3 (same condition)

1

Verification
Graph — node slugs and edge names
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh graph v2
Edges from input (should be exactly 5)
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh edges v2
Traffic
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh traffic v2
Output mapping
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-ops.sh outputs v2
Full verify (node status + ISE fan-out + outputs + traffic)
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-clean-graph-v2.sh verify
Transform Tags (per source)
Transform source_type retention severity environment

ise-hot-extract

ise-auth

hot

high

production

ise-cold-extract

ise

cold

production

ise-router

— (pass-through, 0 ops)

asa-extract

asa-vpn

hot

high

production

win-logon-extract

windows-security

cold

medium

production

switch-extract

switch

cold

medium

production

Scripts
Script Purpose

monad-ops.sh graph v2

Show current node slugs and edge names

monad-ops.sh traffic v2

Per-node ingress/egress counts

monad-ops.sh edges v2

Edge routing table

monad-ops.sh outputs v2

Output component mapping

monad-ops.sh observe v2 <slug>

Live stream events from a node

monad-clean-graph-v2.sh verify

Full verification suite

monad-fix-transforms.sh audit-all

Transform tag audit

monad-output-config.sh status

3-freeway output type check

Show Colleagues

Build the PDF from the d001 assembler:

bash data/d001/projects/siem-qradar-to-sentinel/scripts/build-monad-doc.sh

Or export just this daily note:

make pdf FILE=docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-22.adoc

For a live demo: load creds (ds d001 lab/network), then run monad-ops.sh traffic v2 and monad-clean-graph-v2.sh verify to show the architecture in real time.

Monad KV Lookup + Security Audit

Monad KV Lookup + Security Audit

KV Lookup Store — Created

Reference pipeline for dynamic ISE category→tier routing. Eliminates hardcoded CISE_ edge conditions — new categories added to KV table, not pipeline graph.

Create KV output + HTTP input + reference pipeline + populate
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-kv-setup.sh create-output
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-kv-create-input.sh
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-kv-setup.sh create-pipeline
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-kv-setup.sh populate
bash data/d001/projects/siem-qradar-to-sentinel/scripts/monad-kv-setup.sh verify
Table 1. Components created
Component ID Type

kv-ise-tiers (output)

777116b1-feea-48b7-adb0-4098cfad544e

kv-lookup

ise-tier-reference (input)

e0ad6b25-d732-4dff-bd4d-ab7eba20ad3d

monad-http

ise-tier-reference (pipeline)

07f0e641-a677-49b9-a1d5-0f78edc58c6a

2 nodes, 1 edge

KV store contents — 27 ISE categories
hot (8):     Passed_Authentications, Failed_Attempts, Authentication_Flow,
             RADIUS_Accounting, TACACS_Accounting, Administrator,
             Threat_Centric, Guest

cold (9):    Profiler, Posture, Policy_Diagnostics, Identity_Stores,
             AD_Connector, Administrative, PassiveID, ACI, MyDevices

discard (10): System_Statistics, Internal_Operations, External_MDM,
              Internal_MDM, Distributed_Management, Licensing,
              RADIUS_Diagnostics, TACACS_Diagnostics, MDM_Diagnostics, IPSec
Table 2. API field discovery (learned during call with Kenneth)
What we tried Error Correct field

type: "kv-lookup" in output POST

invalid output type

output_type: "kv-lookup"

input_type: "monad-http" in input POST

invalid input type

type: "monad-http"

config.key_field flat

key_field is required

config.settings.key_field

Pending — KV Enrichment Integration
  • Attach KV Lookup enrichment node to chlxsyslog-pipeline (API or GUI — ask Kenneth)

  • Join key: app-namecategory field in KV store

  • Test: observe ise-enrich node, confirm tier field appears on events

  • Replace 27 CISE_ edge conditions with 3 tier equals checks

  • Set up cron to re-run monad-kv-setup.sh populate before 24h TTL expires

Security Audit — Shell Expansion Vulnerability

Kenneth (Monad) identified potential command injection via unquoted heredocs during screen share. Audit confirmed 4 vulnerable files.

Attack Vector

Unquoted heredocs (<< EOF instead of << 'EOF') allow shell expansion inside the document body. If a variable contains backticks or $() syntax — for example, from an API response stored in a variable — the shell executes it during heredoc expansion.

Vulnerable pattern
# DANGEROUS — variable expansion active inside heredoc
api_response=$(curl -s ... | jq -r '.id')
cat > /tmp/payload.json << EOF
{"component_id": "${api_response}"}
EOF
# If api_response contains: test$(rm -rf /)
# The command inside $() EXECUTES
Safe pattern
# SAFE — jq -n --arg escapes all values, no shell expansion possible
api_response=$(curl -s ... | jq -r '.id')
jq -n --arg id "$api_response" \
    '{component_id: $id}' > /tmp/payload.json
Findings
File Vulnerability Severity Fix Applied

monad-output-config.sh

3 unquoted heredocs interpolating DCE URLs, Rule IDs, client_secret

HIGH

Replaced with jq -n --arg

monad-update-descriptions.sh

Unquoted heredoc with function parameters ($name, $desc)

MEDIUM

Replaced with jq -n --arg

monad-kv-setup.sh

Unquoted heredoc with API-sourced component IDs ($input_id, $output_id)

MEDIUM

Replaced with jq -n --arg

monad-clean-graph-v2.sh

Unquoted PAYLOAD heredoc with $ISE_ROUTER_ID from API response

MEDIUM

Quoted heredoc + jq --arg post-substitution

Rule

Never interpolate variables inside heredocs for JSON construction. Use jq -n --arg — it escapes values properly and cannot execute shell commands regardless of input content.

Before (vulnerable — 4 files)
cat > /tmp/payload.json << EOF
{"id": "${api_var}", "secret": "${credential}"}
EOF
After (safe — all 4 files fixed)
jq -n --arg id "$api_var" --arg secret "$credential" \
    '{id: $id, secret: $secret}' > /tmp/payload.json
Files verified safe (no issues)

21 other scripts in the project use quoted heredocs (<< 'EOF') or jq -n correctly. All curl commands properly quote variable expansions. All jq pipelines handle output safely.

INC — Transform Operations Wiped by Rename

INC — Transform Operations Wiped by Rename

Incident

All 6 pipeline transform operations were wiped to 0 ops by monad-rename-transforms.sh. The script sent a PATCH with {name, description} only — no config field. Monad’s transform PATCH replaces the entire object. Missing config = all operations deleted.

Timeline
Time Event

~12:20

monad-rename-transforms.sh executed — renamed 6 transforms

~12:23

Dashboard showed all transforms with 0 operations

~12:25

Root cause identified — PATCH payload missing config

~12:27

monad-restore-transforms.sh executed — all 6 transforms restored from known-good state

~12:27

Verified: ise-enrich=3, ise-hot=2, ise-cold=1, asa-enrich=5, win-enrich=5, switch-enrich=8

Root Cause

The rename script built PATCH payloads with only name and description:

Vulnerable code (monad-rename-transforms.sh)
# WRONG — missing config, wipes all operations
jq -n --arg n "$new" --arg d "$desc" \
    '{name: $n, description: $d}' > /tmp/monad-rename.json

Monad API constraint (documented in session-2026-06-18.adoc, constraint #5): Transform PATCH replaces entire object — always include name and description. This also means always include config.

Fix Applied

The rename script now GETs the existing config before PATCHing:

Fixed code (monad-rename-transforms.sh)
# SAFE — preserves existing config.operations
curl -sS -m 30 -H "X-API-Key: ${MONAD_API_KEY}" \
    "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/transforms/${id}" | \
    jq --arg n "$new" --arg d "$desc" \
    '{name: $n, description: $d, config: .config}' > /tmp/monad-rename.json
Prevention

Rule: Every Monad transform PATCH must include the full object — name, description, AND config.operations. A PATCH without config is destructive.

Verify-before pattern for transform changes
# 1. GET current state
curl -sS -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/transforms/${ID}" | \
  jq '{name, ops: (.config.operations | length)}'

# 2. Build payload preserving config
curl -sS -H "X-API-Key: ${MONAD_API_KEY}" \
  "${MONAD_API_URL}/v1/${MONAD_ORG_ID}/transforms/${ID}" | \
  jq --arg n "new-name" '{name: $n, description: .description, config: .config}' \
  > /tmp/patch.json

# 3. Verify payload has operations
jq '.config.operations | length' /tmp/patch.json
# Must be > 0 for transforms with operations

# 4. Apply
curl -sS -X PATCH ... -d @/tmp/patch.json

# 5. Verify after
curl -sS ... | jq '{name, ops: (.config.operations | length)}'
Severity

P2 — Data loss (recoverable). Operations were known from scripts executed minutes before. No data loss in pipeline traffic — transforms with 0 ops pass data through without enrichment, so events were still routed correctly. Enrichment tags were missing for ~5 minutes.

  • Constraint #5 from session-2026-06-18.adoc: Transform PATCH replaces entire object

  • Kenneth’s security audit (same session): shell expansion in heredocs — same root cause pattern of insufficient verification before destructive API calls

Monad Script Architecture — CLI Toolkit Path

Monad Script Architecture — CLI Toolkit Path

Current State: 14 Shell Scripts
Script Purpose

monad-ops.sh

Pipeline operations (graph, traffic, edges, outputs, observe, deploy)

monad-output-config.sh

3-freeway output conversion (dev-null → azure-monitor)

monad-fix-transforms.sh

Transform tag fixes + full audit

monad-lean-all-transforms.sh

Trim ASA/Windows/Switch to lean pattern

monad-lean-ise-tiers.sh

Trim ISE hot/cold to tier-specific only

monad-lean-router.sh

ISE enrich — facility, environment, drop pri

monad-fix-router.sh

ISE router shared enrichment setup

monad-rename-transforms.sh

Clean transform names (ise-hot, asa-enrich, etc.)

monad-clean-graph.sh

ASA collapse (18→13 nodes)

monad-clean-graph-v2.sh

1 edge per source + ISE router fan-out

monad-update-descriptions.sh

Pipeline + output descriptions

monad-kv-setup.sh

KV lookup store + reference pipeline + populate

monad-kv-create-output.sh

KV output creation (API field discovery)

monad-kv-create-input.sh

HTTP input creation (API field discovery)

verify-v3.sh

v3 graph validation (ASA routing, node count)

Pattern

Every script follows the same skeleton:

check_creds          # dsource d001 lab/network
build_payload        # jq -n --arg (injection-safe, never heredoc)
curl_with_timeout    # -m 30, save to output/
parse_response       # jq, tee to timestamped file
Why This Scales
  • Credential separationdsource loads env vars, scripts consume ${VARS}, no hardcoded secrets

  • Payload safetyjq -n --arg for all JSON construction, no shell expansion possible

  • Audit trail — every API response saved to output/ with timestamps

  • Subcommand dispatchcase pattern maps directly to Go cobra commands

  • Reusable across APIs — swap MONAD_* for ISE_*, FMC_*, CLAROTY_* — same structure

Evolution Path
Bash scripts (now)
  → prove API patterns, validate payloads, document edge cases
  → discover errors (output_type vs type, input binding, heredoc injection)

Go CLI (when API interface stabilizes)
  → each cmd_* function → cobra command
  → each jq -n --arg → Go struct + json.Marshal
  → each curl → http.Client
  → same architecture, compiled binary, tab completion, cross-platform

ISE ERS and DataConnect are stable — ready for Go port. Monad KV enrichment is still being discovered — stays in bash until the interface solidifies.

Pending Architecture: 3-Tier Fan-Out for All Sources

ISE has the full hot/cold/discard fan-out via ise-enrich router. ASA, Windows, and Switch currently go to a single output each. The architecture should support 3-tier routing for every source — even if all traffic initially goes one direction. When sub-classification is needed, only edge conditions change, not the graph structure.

Monad Pipeline — Phase 2 Roadmap

Monad Pipeline — Phase 2 Roadmap

Phase 1 complete: clean graph (14 nodes, 13 edges), ISE 3-tier fan-out, lean transforms, KV lookup store, security audit. Phase 2: source-specific enrichment and 3-tier fan-out for all sources.

ASA Observations
Raw event sample
hostname: USC-TEST-RAVPN-5515
msg: %ASA-1-717055: The <ID> certificate in the trustpoint <TrustPoint_Test> has expired.
  • Severity encoded in msg: %ASA-{severity}-{msgid} (1=critical, 6=info)

  • Message ID determines event type: 717055=cert expired, 113039=VPN session, 106023=ACL deny

  • ASA logging confirmed: logging host FW_MGMT 10.248.13.254, trap informational (0-6)

  • Low volume on Sunday — Monday will show VPN/ACL/failover traffic

Phase 2 transform design
Parse %ASA-X-NNNNNN from msg:
  - Extract severity (1-7) → dynamic severity_tier (not hardcoded "high")
  - Extract message ID → route to hot/cold/discard based on ID ranges
  - VPN sessions (113xxx, 716xxx) → hot
  - ACL denies (106xxx) → hot
  - Connection lifecycle (302xxx) → discard
  - Cert/config (717xxx) → cold
Windows Observations
Raw event samples
hostname: L-N41999.LA.AD.CHLA.ORG
msg: ...4670...Success Audit...     (Event ID 4670 — object permissions changed)
msg: ...4688...Success Audit...     (Event ID 4688 — process creation)
msg: ...4673...Failure Audit...     (Event ID 4673 — privileged service called)
  • Event ID is tab-delimited (position 4 in #011-separated msg)

  • Success Audit vs Failure Audit visible in raw msg

  • Only 1 host sending: L-N41999.LA.AD.CHLA.ORG

Phase 2 transform design
Parse Event ID from tab-delimited msg:
  - 4625 (failed logon) → hot
  - 4624 (successful logon) → cold
  - 4688 (process creation) → cold
  - 4673 (Failure Audit on privileged service) → hot
  - 4670 (object permissions) → cold
Switch Observations
  • Zero traffic on Sunday — switch may not be generating events

  • switch_name and location are hardcoded for Duque-TrackA-9300 — breaks when second switch onboarded

  • Phase 2: derive switch identity from hostname field, not static tag

Catch-All Observations
  • 2008 records but 0 live events observed — historical Monad agent MetricsExtension heartbeats

  • NOR exclusion is working — all classified sources excluded correctly

Phase 2 Tasks
  • Test Monad jq transform operation — parse %ASA-X- severity from msg field

  • Test Monad rename_key / create_key_value_if_key_value for conditional tagging

  • Build ASA 3-tier fan-out: asa-enrich → hot (VPN/ACL) / cold (cert/config) / discard (connection lifecycle)

  • Build Windows 3-tier fan-out: win-enrich → hot (failed logon, priv escalation) / cold (success logon, process) / discard (noise)

  • Build Switch fan-out: derive switch identity from hostname, remove hardcoded switch_name/location

  • Attach KV Lookup enrichment to chlxsyslog-pipeline (API — ask Kenneth)

  • Replace 27 ISE edge conditions with 3 tier-based conditions via KV enrichment

  • Monday: verify ASA volume with weekday traffic, sample VPN/ACL/failover events

  • Set up cron for KV store repopulation (24h TTL)

VM Inventory and State

List all VMs (state summary)
sudo virsh list --all
sudo virsh list --all | awk 'NR>2 {print $2": "$3}'
Filter by role
sudo virsh list --all | grep -i k3s
sudo virsh list --all | grep -i WLC
sudo virsh list --all | grep -i ipsk
sudo virsh list --all | grep -i ise
Show only autostart-enabled VMs
sudo virsh list --all --autostart
sudo virsh list --all --autostart | awk 'NR>2 {print $2}'
Full domain info (CPU, memory, state)
sudo virsh dominfo <vm>
sudo virsh domstate <vm>

# CPU time delta trick — confirm VM is actively running
t1=$(sudo virsh dominfo <vm> | awk '/CPU time/{print $3}')
sleep 5
t2=$(sudo virsh dominfo <vm> | awk '/CPU time/{print $3}')
echo "CPU time delta: $(echo "$t2 - $t1" | bc)s"
vCPU placement and affinity
sudo virsh vcpuinfo <vm>

# Compare two VMs side-by-side
sudo virsh vcpuinfo vyos-01; sudo virsh vcpuinfo home-dc01 2>/dev/null

VM Lifecycle

Start / stop / reboot
sudo virsh start <vm>
sudo virsh destroy <vm>          # hard stop (SIGKILL equivalent)
sudo virsh shutdown <vm>         # ACPI graceful shutdown
sudo virsh reboot <vm>           # ACPI reboot
Cycle with hook verification
sudo systemctl restart libvirtd && sudo virsh destroy <vm> && sudo virsh start <vm>
Define / undefine
sudo virsh define /path/to/vm.xml
sudo virsh undefine <vm>
sudo virsh undefine <vm> --remove-all-storage   # destructive — removes disk images
Enable autostart
sudo virsh autostart <vm>
sudo virsh autostart --disable <vm>
Bulk start in dependency order
for vm in vyos-01 bind-01 vault-01 ipa-01 ipsk-mgr-01 9800-WLC-01 k3s-master-01; do
  sudo virsh start "$vm" && echo "$vm: started" || echo "$vm: FAILED"
  sleep 5
done
Enable autostart for all production VMs
for vm in vyos-01 bind-01 vault-01 ipa-01 ipsk-mgr-01 9800-WLC-01 k3s-master-01; do
  sudo virsh autostart "$vm" && echo "$vm: autostart enabled"
done

Console and Remote Access

Serial console (requires console=ttyS0 in guest kernel args)
sudo virsh console <vm>
sudo virsh console <vm> --force    # steal an existing console session
Escape from console
Ctrl+]
VNC display port (connect with a VNC client to kvm-01:<port>)
sudo virsh vncdisplay <vm>

XML Configuration

Dump live XML
sudo virsh dumpxml <vm>
sudo virsh dumpxml <vm> > /tmp/<vm>.xml
Edit XML in $EDITOR (validates on save)
sudo virsh edit <vm>
Diff interface blocks between two VMs
diff \
  <(sudo virsh dumpxml <vm-a> | grep -A15 '<interface') \
  <(sudo virsh dumpxml <vm-b> | grep -A15 '<interface')
Rewrite disk path in a saved XML (common after storage migration)
sed -i 's|/var/lib/libvirt/images/|/mnt/onboard-ssd/libvirt/images/|g' /tmp/<vm>.xml
Rewrite bridge attachment in saved XML
sed -i 's|virbr0|br-mgmt|g' /tmp/<vm>.xml
grep "source bridge" /tmp/<vm>.xml    # verify
Eject CD-ROM after OS install
sudo virsh change-media <vm> sda --eject --config

Network Interface Inspection

List all vnet interfaces for a VM
sudo virsh domiflist <vm>
Get the vnet device name for a VM on br-mgmt
VNET=$(sudo virsh domiflist <vm> | awk '/br-mgmt/{print $1}')
echo "vnet: $VNET"
Get IP address (requires guest agent or DHCP lease visibility)
sudo virsh domifaddr <vm>
MAC address from XML
sudo grep 'mac address' /etc/libvirt/qemu/<vm>.xml

Bridge and VLAN Trunk Management

Show full VLAN membership table for all ports
bridge vlan show
Show VLAN membership for a specific vnet
bridge vlan show dev <vnet>

# Dynamic — resolve vnet from VM name first
bridge vlan show dev $(sudo virsh domiflist <vm> | awk '/vnet/{print $1}')
Show bridge members and their link state
bridge link show
bridge link show | grep br-mgmt
Enumerate bridges and their physical ports
ip link show type bridge
ip link show master br-mgmt
ip link show master br-mgmt | awk -F'[ :]+' '/vnet/{print $2}'
Apply standard VLAN trunk to a vnet (PVID 100 untagged, tagged 10/20/30/40/110/120)
VNET=<vnet>
sudo bridge vlan add vid 100 dev "$VNET" pvid untagged
for vid in 10 20 30 40 110 120; do
  sudo bridge vlan add vid "$vid" dev "$VNET"
done
sudo bridge vlan del vid 1 dev "$VNET" pvid untagged
sudo bridge vlan show dev "$VNET"
Apply trunk to a vnet resolved from a VM name
VNET=$(sudo virsh domiflist <vm> | awk '/br-mgmt/{print $1}')
sudo bridge vlan add vid 100 dev "$VNET" pvid untagged
for vid in 10 20 30 40 110 120; do
  sudo bridge vlan add vid "$vid" dev "$VNET"
done
sudo bridge vlan del vid 1 dev "$VNET" pvid untagged
sudo bridge vlan show dev "$VNET"
Verify VLAN consistency between two vnets (e.g. ise-01 vs certmgr-01)
diff \
  <(bridge vlan show dev vnet16) \
  <(bridge vlan show dev vnet17)
Port speed / state / bridge membership summary (all eno* ports)
for port in eno{1..8}; do
  state=$(cat /sys/class/net/$port/operstate 2>/dev/null || echo "missing")
  speed=$(cat /sys/class/net/$port/speed    2>/dev/null || echo "?")
  master=$(ip link show "$port" 2>/dev/null | awk -F'master ' '{print $2}' | awk '{print $1}')
  printf "%-6s  %-6s  %5sMbps  %s\n" "$port" "$state" "$speed" "${master:-[none]}"
done

libvirt Hook (VLAN Auto-Enforcement)

The /etc/libvirt/hooks/qemu hook fires on VM start and applies the correct VLAN trunk to the VM’s vnet automatically. It uses MAC address matching to identify the interface.

Inspect the hook
sudo cat /etc/libvirt/hooks/qemu
Test the hook manually without cycling a VM
sudo /etc/libvirt/hooks/qemu <vm> started begin -
Cycle libvirtd and a VM to force hook re-execution
sudo systemctl restart libvirtd
sudo virsh destroy <vm> && sudo virsh start <vm>
sleep 5
journalctl -t "libvirt-hook[<vm>]" --no-pager -n 30
Watch hook log output (all VMs)
journalctl -t "libvirt-hook" --no-pager -n 50
journalctl -t "libvirt-hook" -f
Watch hook log output for a specific VM
journalctl -t "libvirt-hook[<vm>]" --since "10 minutes ago" --no-pager
Backup before editing the hook
sudo cp /etc/libvirt/hooks/qemu /etc/libvirt/hooks/qemu-$(date +%Y-%m-%d)-bak
sudo ls /etc/libvirt/hooks/

NFS Storage Mounts

Mount NAS shares manually
sudo mkdir -p /mnt/nas/{vms,isos,backups}
sudo mount -t nfs nas-01:/volume1/vms     /mnt/nas/vms
sudo mount -t nfs nas-01:/volume1/isos    /mnt/nas/isos
sudo mount -t nfs nas-01:/volume1/backups /mnt/nas/backups
Persistent mounts in /etc/fstab (with netdev + nofail)
cat << 'EOF' | sudo tee -a /etc/fstab
nas-01:/volume1/vms      /mnt/nas/vms     nfs  defaults,_netdev,nofail,x-systemd.device-timeout=10  0 0
nas-01:/volume1/isos     /mnt/nas/isos    nfs  defaults,_netdev,nofail,x-systemd.device-timeout=10  0 0
nas-01:/volume1/backups  /mnt/nas/backups nfs  defaults,_netdev,nofail,x-systemd.device-timeout=10  0 0
EOF
grep nfs /etc/fstab
sudo systemctl daemon-reload
Verify mounts
mount | grep nas
df -h /mnt/nas/vms /mnt/nas/isos /mnt/nas/backups

VM Provisioning

Import from Existing Disk Image

sudo virt-install \
  --name <vm> \
  --memory <MB> \
  --vcpus <n> \
  --import \
  --disk /path/to/<vm>.qcow2,bus=virtio \
  --os-variant rocky9 \
  --network bridge=br-mgmt,model=virtio \
  --graphics vnc,listen=0.0.0.0 \
  --noautoconsole

ISE (Cisco ISE — bare metal install from ISO)

sudo qemu-img create -f qcow2 /mnt/onboard-ssd/vms/ise-01.qcow2 300G

sudo virt-install \
  --name ise-01 \
  --memory 16384 \
  --vcpus 4,cpuset=6-9 \
  --cpu host-passthrough \
  --machine q35 \
  --cdrom /mnt/onboard-ssd/vms/Cisco-ISE-3.5.0.527.SPA.x86_64.iso \
  --disk path=/mnt/onboard-ssd/vms/ise-01.qcow2,format=qcow2,bus=virtio \
  --os-variant rhel8.0 \
  --network bridge=br-mgmt,model=virtio \
  --graphics vnc,listen=0.0.0.0 \
  --video virtio \
  --noautoconsole

Cloud-Init VM (Ubuntu Noble)

# Download base image
curl -LO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

# Create overlay disk
sudo qemu-img create -f qcow2 -F qcow2 \
  -b /mnt/onboard-ssd/isos/noble-server-cloudimg-amd64.img \
  /mnt/onboard-ssd/libvirt/images/<vm>.qcow2 20G

# Generate cloud-init seed ISO
cat > /tmp/cloud-init.yml << 'CLOUDINIT'
#cloud-config
hostname: <vm>
fqdn: <vm>.inside.domusdigitalis.dev
users:
  - name: evanusmodestus
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - <your-pubkey>
package_update: true
CLOUDINIT

cloud-localds /tmp/cloud-init.iso /tmp/cloud-init.yml

# Install
sudo virt-install \
  --name <vm> \
  --memory 4096 \
  --vcpus 2 \
  --import \
  --disk path=/mnt/onboard-ssd/libvirt/images/<vm>.qcow2,format=qcow2 \
  --disk path=/tmp/cloud-init.iso,device=cdrom \
  --network bridge=br-mgmt,model=virtio \
  --os-variant ubuntu24.04 \
  --graphics vnc,listen=0.0.0.0 \
  --noautoconsole

Bulk Define from Saved XML Definitions

for xml in /mnt/nas/backups/kvm-01-rebuild/vm-definitions/*.xml; do
  vm=$(basename "$xml" .xml)
  echo "Importing: $vm"
  sudo virsh define "$xml"
done

Bulk Re-path and Define (after storage migration)

for vm in vault-01 bind-01; do
  sed -i 's|/var/lib/libvirt/images/|/mnt/onboard-ssd/libvirt/images/|g' /tmp/$vm.xml
  sudo virsh define /tmp/$vm.xml
  sudo virsh start "$vm"
done

Vault SSH CA Bootstrap (new hypervisor node)

# Pull Vault SSH CA public key and install as TrustedUserCAKeys
sudo curl -sk https://vault-01.inside.domusdigitalis.dev:8200/v1/ssh/public_key \
  > /etc/ssh/vault-ca.pub

# Add to sshd_config if not already present
grep -q vault-ca.pub /etc/ssh/sshd_config || \
  echo "TrustedUserCAKeys /etc/ssh/vault-ca.pub" | sudo tee -a /etc/ssh/sshd_config

sudo systemctl reload sshd

NetworkManager Bridge VLAN Filtering

Enable VLAN filtering on br-mgmt with PVID 100
sudo nmcli connection modify br-mgmt \
  bridge.vlan-filtering yes \
  bridge.vlan-default-pvid 0 \
  bridge.vlans "100 pvid untagged, 10, 20, 30, 40, 110, 120"

nmcli c s br-mgmt | grep bridge.vlan
Create br-wan (ISP-facing, no VLAN filtering, STP disabled)
sudo nmcli connection add type bridge con-name br-wan ifname br-wan
sudo nmcli connection modify br-wan ipv4.method disabled ipv6.method disabled
sudo nmcli connection modify br-wan bridge.stp no bridge.forward-delay 0
sudo nmcli connection add type bridge-slave con-name br-wan-port ifname eno7 master br-wan
sudo nmcli connection delete eno7 2>/dev/null || true
sudo nmcli connection up br-wan
ip link show br-wan
bridge link show | grep eno7

AsciiDoc Dedup Utility: virsh list Commands

When a command log accumulates duplicate virsh list invocations, use awk to normalize and deduplicate while preserving one instance of each semantically distinct form.

Count duplicates before cleaning
file="$(find . -name 'kvm-virsh-vm-terminal-management.adoc' -print -quit)"

awk '
  /^[[:space:]]*sudo virsh list/ {
    cmd=$0
    sub(/^[[:space:]]+/, "", cmd)
    count[cmd]++
  }
  END {
    for (cmd in count)
      printf "%3d  %s\n", count[cmd], cmd
  }
' "$file" | sort -nr
Deduplicate in-place (keeps first occurrence of each unique command)
cp "$file" "$file.bak"

awk '
  /^[[:space:]]*sudo virsh list/ {
    cmd=$0
    sub(/^[[:space:]]+/, "", cmd)
    if (seen[cmd]++) next
  }
  { print }
' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
Verify result
grep -nF 'sudo virsh list' "$file"

libvirt Hook — How It Works

The /etc/libvirt/hooks/qemu script is the mechanism that automatically enforces the correct VLAN trunk on every vnet the moment a VM starts. Without it, a vnet comes up as a plain access port on VLAN 1 — wrong for every VM on br-mgmt.

Hook Invocation Signature

libvirt calls the hook like this:

/etc/libvirt/hooks/qemu <vm-name> <action> <sub-action> <extra>

The arguments you care about:

$1 — VM name

e.g. k3s-master-01, ise-01

$2 — action

prepare, start, started, stopped, release

$3 — sub-action

begin, end, -

$4 — extra

usually -

Act on started (not start) — the vnet interface exists in the kernel at that point. On start, libvirt hasn’t attached the tap yet.

Hook Structure (annotated)

This is the pattern the hook was built around during the kvm-01 rebuild:

#!/usr/bin/env bash
# /etc/libvirt/hooks/qemu
# Enforces VLAN trunk on br-mgmt vnets at VM start.

DOMAIN="$1"
ACTION="$2"
BRIDGE="br-mgmt"
TAG="libvirt-hook[$DOMAIN]"

# Only act on the 'started' event
[[ "$ACTION" != "started" ]] && exit 0

# --- Resolve vnet from MAC address in the VM's XML ---
# libvirt stores XML at /etc/libvirt/qemu/<vm>.xml
XML="/etc/libvirt/qemu/${DOMAIN}.xml"

MAC=$(grep -oP "(?<=<mac address=[\"'])[0-9a-f:]+" "$XML" | head -1)
if [[ -z "$MAC" ]]; then
  logger -t "$TAG" "ERROR: could not extract MAC from $XML"
  exit 1
fi
logger -t "$TAG" "MAC=$MAC"

# --- Find vnet whose kernel MAC matches ---
VNET=""
for iface in $(ip link show master "$BRIDGE" | awk -F'[ :]+' '/^[0-9]/{print $3}'); do
  iface_mac=$(cat /sys/class/net/"$iface"/address 2>/dev/null)
  if [[ "$iface_mac" == "$MAC" ]]; then
    VNET="$iface"
    break
  fi
done

if [[ -z "$VNET" ]]; then
  logger -t "$TAG" "ERROR: no vnet on $BRIDGE matched MAC=$MAC"
  exit 1
fi
logger -t "$TAG" "VNET=$VNET matched MAC=$MAC"

# --- Apply VLAN trunk ---
bridge vlan add vid 100 dev "$VNET" pvid untagged
for vid in 10 20 30 40 110 120; do
  bridge vlan add vid "$vid" dev "$VNET"
done
bridge vlan del vid 1 dev "$VNET" pvid untagged

logger -t "$TAG" "PVID set to 100 untagged, tagged: 10 20 30 40 110 120 on $VNET"
The MAC resolution loop (ip link show master br-mgmt | awk) was the fix that made the hook reliable. Earlier attempts used awk '{print $3}' and got the wrong field — the working version uses '{print $3}' on lines starting with a digit (interface index lines), then reads /sys/class/net/<iface>/address to compare MACs directly.

Why MAC Matching, Not VM Name

libvirt assigns vnet names non-deterministically (vnet0, vnet1, …​) based on tap creation order at boot. The name changes across reboots if other VMs start in a different order. The MAC is stable — it’s fixed in the domain XML — so MAC matching is the correct anchor.

Troubleshooting the Hook

Confirm the hook is executable
ls -la /etc/libvirt/hooks/qemu
Run the hook manually (safe — only touches bridge VLANs, no VM state changes)
sudo /etc/libvirt/hooks/qemu <vm> started begin -
Watch what libvirt logged about the hook
# For a specific VM
journalctl -t "libvirt-hook[<vm>]" --since "10 minutes ago" --no-pager

# All hook events
journalctl -t "libvirt-hook" --no-pager -n 50 -f

# libvirtd-level hook errors (hook not found, permission denied, etc.)
journalctl -u libvirtd --since "5 minutes ago" | grep -i hook
Smoke-test the logger tag format before editing the hook
logger -t "libvirt-hook[test]" "smoke test with brackets"
journalctl -t "libvirt-hook[test]" --no-pager -n 3

logger -t "libvirt-hook" "smoke test without brackets"
journalctl -t "libvirt-hook" --no-pager -n 3
journalctl -t matches the syslog identifier exactly. A hook that logs with logger -t "libvirt-hook[$DOMAIN]" will NOT appear when you query -t "libvirt-hook" — the brackets make them different identifiers. Use both patterns when unsure.
Force a clean hook re-run
sudo systemctl restart libvirtd
sudo virsh destroy <vm> && sudo virsh start <vm>
sleep 5
journalctl -t "libvirt-hook[<vm>]" --no-pager -n 20
bridge vlan show dev $(sudo virsh domiflist <vm> | awk '/br-mgmt/{print $1}')
Extract the MAC libvirt will use for a given VM
sudo grep -oP "(?<=<mac address=[\"'])[0-9a-f:]+" /etc/libvirt/qemu/<vm>.xml
Confirm MAC matches the live vnet
cat /sys/class/net/<vnet>/address

libvirt Virtual Networks

libvirt manages its own NAT network (virbr0) separately from the host’s bridge interfaces. Use virsh net-* to inspect it — do not confuse it with the br-mgmt Linux bridge.

# List all libvirt-managed networks (active and inactive)
sudo virsh net-list --all

# Full XML of the default NAT network
sudo virsh net-dumpxml default

# Start / stop a libvirt network
sudo virsh net-start default
sudo virsh net-destroy default
br-mgmt and br-wan are plain Linux bridges managed by NetworkManager — they are invisible to virsh net-list. Only virbr0 (and any networks you define with virsh net-define) appear there.

ARP / Neighbor Table — Finding VM IPs Without Guest Agent

When virsh domifaddr returns nothing (no guest agent, DHCP lease not visible), use the host’s ARP/neighbor cache after pinging the broadcast or a known range:

# Check kernel neighbor table (most reliable)
ip neigh show dev br-mgmt
ip neigh | grep -i "52:54:00"      # all KVM VMs share this OUI

# Filter by specific MAC
ip neigh | grep -i "52:54:00:f3"

# Classic arp table
arp -an | grep 10.50.1

# Ping to populate the neighbor cache first if needed
ping -c1 -b 10.50.1.255 2>/dev/null || true
ping -c1 10.50.1.<n>

# Then check
ip neigh show dev br-mgmt | grep "52:54:00"
Quick MAC-to-IP lookup for a specific VM
VM=<vm>
MAC=$(sudo grep -oP "(?<=<mac address=[\"'])[0-9a-f:]+" /etc/libvirt/qemu/${VM}.xml | head -1)
echo "Looking for MAC: $MAC"
ip neigh | grep -i "$MAC"

brctl Legacy Commands

brctl is deprecated in favour of bridge (iproute2) but still works on Rocky 9. Prefer bridge for new runbooks; brctl is useful when reading older documentation.

# Show bridge members (legacy)
sudo brctl show br-mgmt
sudo brctl show virbr0

# iproute2 equivalents (preferred)
bridge link show
ip link show master br-mgmt

Host Resource Snapshot

Quick one-liner to capture host capacity before provisioning a new VM:

echo "=== Kernel ===" && uname -r
echo "=== CPU ===" && nproc && lscpu | grep -E 'Socket|Core|Thread|NUMA'
echo "=== Memory ===" && free -h
echo "=== Disk ===" && df -h /mnt/onboard-ssd /var/lib/libvirt/images 2>/dev/null
echo "=== VMs ===" && sudo virsh list --all
echo "=== vCPU load ===" && sudo virsh vcpuinfo vyos-01 2>/dev/null | grep -E 'VCPU|CPU:'
Storage layout
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
df -h /mnt/onboard-ssd
Confirm a VM is actually consuming CPU (not just listed as running)
VM=<vm>
t1=$(sudo virsh dominfo "$VM" | awk '/CPU time/{print $3}')
sleep 5
t2=$(sudo virsh dominfo "$VM" | awk '/CPU time/{print $3}')
echo "$VM CPU time delta: $(echo "$t2 - $t1" | bc)s over 5s"

VyOS ISO — Fetch Latest Nightly Build

VYOS_LATEST=$(curl -s \
  https://api.github.com/repos/vyos/vyos-nightly-build/releases/latest \
  | grep -oP '"tag_name":\s*"\K[^"]+')
echo "Latest tag: $VYOS_LATEST"

curl -Lo /mnt/onboard-ssd/isos/vyos-rolling-latest.iso \
  "https://github.com/vyos/vyos-nightly-build/releases/download/${VYOS_LATEST}/vyos-${VYOS_LATEST}-generic-amd64.iso"

SSH Config — Propagate to Other Machines

SSH Config — Propagate Modular Architecture to Other Machines

The modular SSH config (built 2026-06-20, activated 2026-06-21) is live on modestus-aw. Other machines still have the old monolithic symlink or no config at all.

What Changed in dots-quantum

Monolithic ~/.ssh/config replaced with Include-based modular architecture:

config                              → global defaults + Include config.d/*
config.d/00-exceptions.conf         → legacy cipher overrides (Azure)
config.d/10-git-services.conf       → GitHub, GitLab, Codeberg, Bitbucket
config.d/20-d000-key-hierarchy.conf → shared key chain (no IPs)
config.d/30-cisco-defaults.conf     → Cisco/ISE auth (9800-wlc-*, 9300-*, 3560cx-*)
config.d/40-k3s-defaults.conf       → k3s auth settings
config.d/50-vm-defaults.conf        → VM auth settings
config.d/80-d000-hosts.conf         → host→IP mappings (generated from DNS, encrypted in git)
Git Tracks vs Gitignored
File Tracked? Why

config.age

✅ Tracked (encrypted)

Main config contains Include directive + global defaults

config.d/00-exceptions.conf through 50-vm-defaults.conf

✅ Tracked (plaintext)

No secrets — patterns, usernames, auth settings only

config.d/80-d000-hosts.conf.age

✅ Tracked (encrypted)

Contains host→IP mappings

config (plaintext)

❌ Gitignored

Decrypted locally on each machine

config.d/80-d000-hosts.conf (plaintext)

❌ Gitignored

Generated fresh from DNS on each machine

Propagation Runbook — Home Environment (d000)

For machines on the home network with access to BIND at 10.50.1.90.

Step 1: Pull latest dots-quantum
git -C ~/atelier/_projects/personal/dots-quantum pull
Step 2: Create directory structure (skip if exists)
mkdir -p ~/.ssh/{sockets,config.d} && chmod 700 ~/.ssh ~/.ssh/sockets ~/.ssh/config.d
Step 3: Remove stale dotfiles-optimus symlink (if present)
# CHECK FIRST — does ~/.ssh/config point to dotfiles-optimus?
ls -la ~/.ssh/config

# If it shows dotfiles-optimus → must remove before dots-quantum can claim it
# stow won't overwrite another package's symlinks — it treats them as conflicts

# Option A: unstow the old package cleanly
stow -D -d ~/atelier/_projects/personal/dotfiles-optimus -t ~ base

# Option B: if dotfiles-optimus directory structure has changed or unstow fails
rm ~/.ssh/config
Step 4: Stow dots-quantum SSH module
stow -R -d ~/atelier/_projects/personal/dots-quantum -t ~ ssh

# Verify ALL symlinks point to dots-quantum (not dotfiles-optimus)
ls -la ~/.ssh/config ~/.ssh/config.age ~/.ssh/config.d/
# config     → dots-quantum/ssh/.ssh/config        ← NOT dotfiles-optimus
# config.age → dots-quantum/ssh/.ssh/config.age
# config.d/* → dots-quantum/ssh/.ssh/config.d/*
Step 5: Decrypt main config
age -d -i ~/.secrets/.metadata/keys/master.age.key ~/.ssh/config.age > \
    ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config

# Verify Include directive is present
head -5 ~/.ssh/config
# Should show: Include config.d/*
Step 6: Generate host→IP mappings from DNS
for name in kvm-0{1,2} nas-0{1,2} vyos-0{1,2} vyos-vip bind-0{1,2} ipa-0{1,2} \
  keycloak-0{1,2} ipsk-mgr-0{1,2} vault-0{1,2,3} certmgr-01 \
  home-dc0{1,2} 9800-wlc-0{1,2} wlc-0{1,2} k3s-master-0{1,2,3} \
  ise-0{1,2} gitea-01 3560cx-01 9300-01 ipmi-0{1,2} \
  wazuh wazuh-indexer wazuh-dashboard wazuh-workers wazuh-api \
  prometheus alertmanager grafana modestus-{aw,razer}; do
  ip=$(dig +short ${name}.inside.domusdigitalis.dev @10.50.1.90)
  [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && printf "Host %s\n    HostName %s\n\n" "$name" "$ip"
done >| ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf
Step 7: Verify all hosts resolve
for name in kvm-0{1,2} nas-0{1,2} vyos-0{1,2} bind-0{1,2} ipa-0{1,2} \
  vault-0{1,2,3} ise-0{1,2} gitea-01 9300-01 3560cx-01; do
  ip=$(ssh -G "$name" | awk '/^hostname/{print $2}')
  if [[ "$ip" == "$name" ]]; then
    printf "BAD  %-20s ← no mapping found\n" "$name"
  else
    printf "GOOD %-20s → %s\n" "$name" "$ip"
  fi
done
Step 8: Test a connection
ssh kvm-01
Propagation Runbook — Work Environment (d001 / WSL)

DO NOT run the d000 dig loop on d001 machines. The d000 loop targets 10.50.1.90 (home BIND primary) which is unreachable from the work network. dig writes error messages to stdout, the guard passes them through, and SSH breaks trying to parse error text as hostnames. See [Session Output] below for the incident.

Work machines cannot reach home BIND (10.50.1.90). The d000 host list is irrelevant — those are home lab hosts. Work hosts come from gopass tier d001.

Steps 1–5: Same as home, but check for stale dotfiles-optimus
# Pull
git -C ~/atelier/_projects/personal/dots-quantum pull

# Directories
mkdir -p ~/.ssh/{sockets,config.d} && chmod 700 ~/.ssh ~/.ssh/sockets ~/.ssh/config.d

# Remove stale dotfiles-optimus symlink — MUST do before stow
ls -la ~/.ssh/config | grep -q dotfiles-optimus && rm ~/.ssh/config

# Stow
stow -R -d ~/atelier/_projects/personal/dots-quantum -t ~ ssh

# Verify — CRITICAL: config must point to dots-quantum
ls -la ~/.ssh/config
# GOOD: -> ../atelier/_projects/personal/dots-quantum/ssh/.ssh/config
# BAD:  -> ../atelier/_projects/personal/dotfiles-optimus/base/ssh/.ssh/config

# Decrypt
age -d -i ~/.secrets/.metadata/keys/master.age.key ~/.ssh/config.age > \
    ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config
Step 6: d001 host list — NOT the d000 dig loop
# DO NOT run the d000 dig loop here. 10.50.1.90 is unreachable.
# dig will write error messages that break SSH config parsing.

# Option A: Build d001 host list from gopass
cat >| ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf << 'EOF'
# d001 work environment host mappings
# Source: gopass tier d001 — NOT d000 DNS
# Generated: 2026-06-22

# Add your work hosts here:
# Host jump-01
#     HostName 10.x.x.x

# Host build-server
#     HostName 10.x.x.x
EOF

# Option B: Leave empty — rely on FQDN or DNS resolution directly
# The modular config still works — git services, key hierarchy, Cisco
# defaults all load from config.d/10-50. Only host shortcuts are missing.
printf '# d001 work environment — no d000 hosts, use FQDN\n' \
  >| ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf
Step 7: Verify the config loads correctly
# Check that Include is working (config.d files are being read)
ssh -G github.com | awk '/^hostname|^user|^identityfile/'
# Should show github.com settings from 10-git-services.conf

# Check that Cisco defaults load
ssh -G 9800-wlc-01 | awk '/^user|^passwordauthentication/'
# Should show user=admin, passwordauthentication=yes from 30-cisco-defaults.conf
The dotfiles-optimus Problem — Explained

On the work machine, stow -R from dots-quantum created config.d/* and config.age correctly but left ~/.ssh/config still pointing to dotfiles-optimus:

~/.ssh/config     → dotfiles-optimus/base/ssh/.ssh/config   ← OLD (monolithic)
~/.ssh/config.age → dots-quantum/ssh/.ssh/config.age         ← NEW
~/.ssh/config.d/* → dots-quantum/ssh/.ssh/config.d/*          ← NEW

Why stow didn’t fix it: Stow tracks ownership per package. The config symlink was created by the dotfiles-optimus package. Stow considers it a conflict — it won’t silently overwrite another package’s files.

The fix: rm ~/.ssh/config then stow -R again. Or stow -D the old package first to unstow cleanly. Either way, the old symlink must go before dots-quantum can claim it.

After the fix:

~/.ssh/config     → dots-quantum/ssh/.ssh/config              ← NEW (modular, Include config.d/*)
~/.ssh/config.age → dots-quantum/ssh/.ssh/config.age
~/.ssh/config.d/* → dots-quantum/ssh/.ssh/config.d/*

Is dotfiles-optimus still needed? If dots-quantum now manages all the stow packages that dotfiles-optimus used to (ssh, zsh, git, etc.), you can unstow dotfiles-optimus entirely on each machine and archive the repo. Check what else it manages:

# See what dotfiles-optimus has stowed
ls ~/atelier/_projects/personal/dotfiles-optimus/base/

# Compare with what dots-quantum manages
ls ~/atelier/_projects/personal/dots-quantum/

# If dots-quantum covers everything, unstow the old repo completely:
stow -D -d ~/atelier/_projects/personal/dotfiles-optimus -t ~ base
Why Generate Instead of Decrypt 80-d000-hosts.conf?

The .age file exists as a backup in git. But the correct workflow is regenerating from DNS because:

  • DNS is the authoritative source — if an IP changes, the .age snapshot is stale

  • The dig loop is verifiable against BIND primary in real time

  • Each machine gets current IPs, not a frozen snapshot from whenever the last age -e ran

  • On work machines, the d000 .age is useless — those hosts don’t exist in that environment

Prerequisites on Each Machine
Requirement Check Command

age

command -v age

stow

command -v stow

Age private key

ls ~/.secrets/.metadata/keys/master.age.key

Network access to BIND primary (d000 only)

dig +short kvm-01.inside.domusdigitalis.dev @10.50.1.90

OpenSSH ≥ 9.0 (Post-Quantum KEX)

ssh -V

dots-quantum cloned

ls ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/

No stale dotfiles-optimus symlinks

ls -la ~/.ssh/config | grep -v dots-quantum && echo "STALE — fix first"

Machines to Propagate
Machine Status Notes

modestus-aw (Arch, home)

■■■■■■■■■■ 10/10

Activated 2026-06-21

modestus-razer (Arch, home)

□□□□□□□□□□ 0/10

d000 runbook: git pull + unstow optimus + restow + decrypt + dig loop

Work WSL — Arch instance

■■■■■■■■■■ 10/10

Fixed: removed dotfiles-optimus symlink, restowed, recovered from dig loop incident. 80-d000-hosts.conf set to empty (correct — d001 environment, d000 hosts irrelevant)

Work WSL — other distros

□□□□□□□□□□ 0/10

d001 runbook: same flow, d001 host list from gopass. DO NOT run d000 dig loop.

Session Output — Work WSL (2026-06-22)
Ran propagation on work Arch WSL. Stow created config.d/* correctly but config still points to dotfiles-optimus.
$ mkdir -p ~/.ssh/{sockets,config.d} && chmod 700 ~/.ssh ~/.ssh/sockets ~/.ssh/config.d

$ stow -R -d ~/atelier/_projects/personal/dots-quantum -t ~ ssh

$ ls -la ~/.ssh/config ~/.ssh/config.age ~/.ssh/config.d/

lrwxrwxrwx - evanusmodestus 18 Dec  2025 /home/evanusmodestus/.ssh/config -> ../atelier/_projects/personal/dotfiles-optimus/base/ssh/.ssh/config
lrwxrwxrwx - evanusmodestus  6 May 09:30 /home/evanusmodestus/.ssh/config.age -> ../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.age

/home/evanusmodestus/.ssh/config.d/:
lrwxrwxrwx - evanusmodestus 22 Jun 13:00 00-exceptions.conf -> ../../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/00-exceptions.conf
lrwxrwxrwx - evanusmodestus 22 Jun 13:00 10-git-services.conf -> ../../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/10-git-services.conf
lrwxrwxrwx - evanusmodestus 22 Jun 13:00 20-d000-key-hierarchy.conf -> ../../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/20-d000-key-hierarchy.conf
lrwxrwxrwx - evanusmodestus 22 Jun 13:00 30-cisco-defaults.conf -> ../../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/30-cisco-defaults.conf
lrwxrwxrwx - evanusmodestus 22 Jun 13:00 40-k3s-defaults.conf -> ../../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/40-k3s-defaults.conf
lrwxrwxrwx - evanusmodestus 22 Jun 13:00 50-vm-defaults.conf -> ../../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/50-vm-defaults.conf
lrwxrwxrwx - evanusmodestus 22 Jun 13:00 80-d000-hosts.conf.age -> ../../atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf.age
Decrypted main config — Include directive present
$ age -d -i ~/.secrets/.metadata/keys/master.age.key ~/.ssh/config.age > \
    ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config

$ head -5 ~/.ssh/config
# ═══════════════════════════════════════════════════════════════════════════════
# SSH Client Configuration - Post-Quantum Ready
# ═══════════════════════════════════════════════════════════════════════════════
# document_uuid: "b9805b2e-3e63-4231-ae94-6df5551b8522"
# id: "ARS-SEC-SSH-001"
d000 dig loop BROKE SSH — wrote error messages as HostName values
$ for name in kvm-0{1,2} nas-0{1,2} ...; do
  ip=$(dig +short ${name}.inside.domusdigitalis.dev @10.50.1.90)
  [ -n "$ip" ] && printf "Host %s\n    HostName %s\n\n" "$name" "$ip"
done >| ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf

# RESULT: dig returned error messages instead of empty output:
#   Host kvm-01
#       HostName ;; communications error to 10.50.1.90#53: host unreachable
#
# SSH then failed on every operation:
#   /home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 2:
#   keyword hostname extra arguments at end of line
#
# ROOT CAUSE: The d000 dig loop targets 10.50.1.90 (home BIND primary).
# That IP is unreachable from the d001 work network. `dig +short` printed
# the error to stdout. `[ -n "$ip" ]` only checks non-empty — the error
# string passed the guard and was written as HostName values.
#
# LESSON: NEVER run the d000 dig loop on d001 machines. The d000 host list
# is home-lab-only. Work machines need a d001 host list from gopass, or
# leave 80-d000-hosts.conf empty.
#
# FIX APPLIED: regex guard in runbook:
#   [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]
# This rejects anything that isn't an IPv4 address.
Recovery (applied — verified working 2026-06-22)
# Wiped the garbage
printf '# d001 work environment — no d000 hosts\n' \
  >| ~/.ssh/config.d/80-d000-hosts.conf

# Verified SSH works
ssh -G github.com | awk '/^hostname/'
# hostname github.com ← GOOD

# Verified git works
ssh -T git@github.com
Fix stale dotfiles-optimus symlink (applied — verified working 2026-06-22)
rm ~/.ssh/config
stow -R -d ~/atelier/_projects/personal/dots-quantum -t ~ ssh
ls -la ~/.ssh/config  # Now points to dots-quantum

2026-06-22 13:11 - updates to configuration and status

❯ hostnamectl
Failed to query product UUID, ignoring: Access denied
Failed to query hardware serial, ignoring: Access denied
 Static hostname: L-N62420
       Icon name: computer-container
         Chassis: container 📦
      Machine ID: e6bfa315292e4771bef94d8e4cf17fdf
         Boot ID: d683db94eadc4ff2aa2335e89350eb2f
  Virtualization: wsl
Operating System: Arch Linux
          Kernel: Linux 6.6.87.2-microsoft-standard-WSL2
    Architecture: x86-64
 evanusmodestus   domus-captures   main   26.2  ✓
❯ ssh -G github.com | awk '/^hostname|^user|^identityfile/'
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 2: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 3: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 4: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 5: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 8: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 9: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 10: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 11: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 14: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 15: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 16: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 17: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 20: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 21: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 22: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 23: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 26: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 27: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 28: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 29: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 32: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 33: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 34: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 35: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 38: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 39: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 40: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 41: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 44: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 45: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 46: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 47: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 50: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 51: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 52: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 53: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 56: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 57: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 58: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 59: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 62: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 63: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 64: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 65: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 68: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 69: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 70: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 71: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 74: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 75: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 76: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 77: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 80: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 81: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 82: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 83: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 86: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 87: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 88: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 89: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 92: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 93: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 94: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 95: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 98: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 99: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 100: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 101: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 104: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 105: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 106: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 107: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 110: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 111: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 112: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 113: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 116: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 117: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 118: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 119: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 122: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 123: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 124: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 125: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 128: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 129: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 130: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 131: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 134: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 135: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 136: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 137: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 140: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 141: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 142: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 143: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 146: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 147: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 148: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 149: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 152: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 153: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 154: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 155: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 158: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 159: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 160: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 161: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 164: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 165: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 166: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 167: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 170: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 171: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 172: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 173: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 176: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 177: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 178: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 179: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 182: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 183: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 184: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 185: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 188: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 189: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 190: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 191: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 194: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 195: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 196: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 197: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 200: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 201: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 202: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 203: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 206: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 207: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 208: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 209: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 212: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 213: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 214: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 215: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 218: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 219: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 220: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 221: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 224: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 225: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 226: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 227: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 230: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 231: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 232: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 233: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 236: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 237: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 238: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 239: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 242: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 243: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 244: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 245: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 248: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 249: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 250: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 251: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 254: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 255: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 256: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 257: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 260: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 261: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 262: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 263: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 266: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 267: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 268: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 269: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: terminating, 180 bad configuration options
❯ ssh -G github.com | awk '/^hostname|^user|^identityfile/'
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 2: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 3: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 4: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 5: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 8: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 9: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 10: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 11: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 14: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 15: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 16: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 17: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 20: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 21: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 22: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 23: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 26: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 27: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 28: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 29: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 32: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 33: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 34: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 35: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 38: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 39: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 40: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 41: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 44: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 45: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 46: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 47: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 50: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 51: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 52: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 53: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 56: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 57: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 58: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 59: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 62: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 63: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 64: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 65: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 68: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 69: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 70: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 71: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 74: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 75: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 76: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 77: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 80: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 81: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 82: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 83: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 86: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 87: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 88: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 89: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 92: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 93: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 94: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 95: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 98: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 99: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 100: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 101: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 104: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 105: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 106: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 107: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 110: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 111: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 112: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 113: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 116: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 117: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 118: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 119: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 122: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 123: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 124: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 125: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 128: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 129: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 130: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 131: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 134: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 135: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 136: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 137: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 140: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 141: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 142: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 143: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 146: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 147: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 148: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 149: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 152: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 153: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 154: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 155: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 158: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 159: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 160: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 161: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 164: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 165: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 166: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 167: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 170: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 171: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 172: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 173: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 176: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 177: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 178: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 179: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 182: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 183: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 184: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 185: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 188: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 189: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 190: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 191: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 194: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 195: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 196: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 197: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 200: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 201: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 202: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 203: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 206: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 207: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 208: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 209: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 212: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 213: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 214: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 215: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 218: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 219: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 220: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 221: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 224: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 225: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 226: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 227: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 230: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 231: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 232: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 233: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 236: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 237: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 238: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 239: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 242: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 243: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 244: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 245: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 248: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 249: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 250: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 251: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 254: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 255: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 256: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 257: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 260: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 261: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 262: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 263: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 266: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 267: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 268: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 269: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: terminating, 180 bad configuration options
❯ ssh -G github.com | awk '/^hostname|^user|^identityfile/'
❯ ssh -T git@github.com
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 2: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 3: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 4: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 5: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 8: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 9: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 10: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 11: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 14: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 15: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 16: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 17: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 20: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 21: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 22: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 23: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 26: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 27: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 28: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 29: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 32: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 33: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 34: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 35: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 38: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 39: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 40: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 41: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 44: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 45: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 46: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 47: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 50: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 51: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 52: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 53: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 56: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 57: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 58: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 59: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 62: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 63: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 64: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 65: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 68: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 69: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 70: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 71: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 74: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 75: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 76: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 77: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 80: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 81: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 82: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 83: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 86: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 87: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 88: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 89: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 92: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 93: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 94: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 95: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 98: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 99: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 100: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 101: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 104: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 105: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 106: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 107: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 110: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 111: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 112: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 113: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 116: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 117: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 118: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 119: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 122: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 123: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 124: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 125: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 128: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 129: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 130: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 131: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 134: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 135: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 136: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 137: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 140: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 141: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 142: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 143: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 146: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 147: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 148: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 149: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 152: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 153: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 154: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 155: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 158: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 159: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 160: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 161: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 164: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 165: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 166: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 167: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 170: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 171: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 172: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 173: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 176: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 177: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 178: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 179: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 182: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 183: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 184: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 185: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 188: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 189: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 190: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 191: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 194: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 195: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 196: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 197: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 200: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 201: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 202: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 203: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 206: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 207: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 208: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 209: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 212: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 213: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 214: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 215: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 218: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 219: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 220: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 221: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 224: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 225: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 226: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 227: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 230: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 231: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 232: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 233: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 236: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 237: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 238: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 239: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 242: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 243: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 244: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 245: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 248: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 249: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 250: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 251: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 254: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 255: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 256: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 257: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 260: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 261: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 262: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 263: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf line 266: keyword hostname extra arguments at end of line
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 267: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 268: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: line 269: Bad configuration option: ;;
/home/evanusmodestus/.ssh/config.d/80-d000-hosts.conf: terminating, 180 bad configuration options
 evanusmodestus   domus-captures   main   26.2  ✗
❯

Conditional Attributes — Reference & Implementation Plan

Conditional Attributes — Reference Capture & Implementation Plan

What Was Built

Created partials/reference/docs-as-code/conditional-attributes.adoc via heredoc:

tee docs/modules/ROOT/partials/reference/docs-as-code/conditional-attributes.adoc << 'EOF'
... (355 lines)
EOF

This is a complete AsciiDoc conditional preprocessor reference covering:

  • ifdef / ifndef / ifeval directives with OR/AND logic

  • Environment-switched attributes (env-d001 vs env-d000 with safe fallback)

  • Master partials/attributes.adoc pattern — single source of truth for both prose and diagrams

  • Kroki diagram integration — attributes resolve before the diagram renderer sees them

  • Attribute precedence chain: page < antora.yml < playbook < CLI flag

  • @ soft-set vs hard-set in Antora

  • Build commands for each target environment

  • Debug blocks that strip from production builds

Why This Matters

This solves the hardcoded-values-in-diagrams problem we identified today. The resolution order is:

antora-playbook.yml / CLI -a flag
        ↓
partials/attributes.adoc  (ifdef selects env-specific values)
        ↓
page content              ({attribute} renders in prose)
        ↓
Kroki block               (sees resolved literals, not tokens)
        ↓
SVG output                (diagram contains real values)

For Antora builds (docs/): Kroki blocks with subs=attributes+ resolve attributes before rendering. No hardcoded IPs, hostnames, or UUIDs in diagram source.

For standalone builds (data/): build-adoc -a env-d000 passes the flag at CLI. Same ifdef conditionals work. Diagrams in external .dot/.d2 files still need the README attribute map or envsubst — but inline Kroki blocks in the assembler would resolve correctly.

Implementation Plan — 3 Active Projects
Project Tier What Changes Status

monad-pipeline

d001 (Antora + standalone)

  • Add ifdef::env-d001[] to partials/attributes.adoc — all pipeline IDs, hostnames, pricing already there

  • Convert Antora diagram pages to Kroki blocks with subs=attributes+

  • Standalone assembler already works — build-adoc -a env-d001 passes the flag

□□□□□□□□□□ 0/10

keylogger

d000 (standalone only)

  • Add ifdef::env-d000[] wrapper to partials/attributes.adoc

  • Add fallback ifndef block with safe defaults

  • Standalone: build-adoc -a env-d000 keylogger.adoc html --variant light-cyan

  • Inline Kroki blocks optional — external .dot/.d2 files are fine for education projects

□□□□□□□□□□ 0/10

thinkpad-t16g

d000 (standalone + Antora dual)

  • Add ifdef::env-d000[] wrapper to partials/attributes.adoc

  • Existing Antora pages can use Kroki blocks for diagrams that already exist as .dot in output/

  • Standalone assembler: build-adoc -a env-d000 thinkpad-t16g.adoc html --variant light-cyan

□□□□□□□□□□ 0/10

How to Use the Reference

The reference partial is not meant to be included directly into project pages. It documents the patterns — copy the relevant ifdef blocks into your project’s partials/attributes.adoc.

Quick-start: wrap existing attributes in ifdef
# Before (no conditional — always sets values)
:ise-hostname: ise-02.inside.domusdigitalis.dev

# After (environment-switched)
:ise-hostname: ise.example.internal
Quick-start: use attribute in Kroki diagram (Antora)
[graphviz,subs=attributes+]
....
digraph {
  ise [label="{ise-hostname}"]
}
....
Quick-start: pass flag at build time (standalone)
build-adoc $(find data/d000/projects/keylogger -maxdepth 1 -name 'keylogger.adoc') html --variant light-cyan -a env-d000
Security Benefit

The ifdef + fallback pattern is defense in depth at the rendering layer:

Scenario What Renders

build-adoc -a env-d001

Real production values — pipeline IDs, rsyslog IP, billing account, hostnames

build-adoc -a env-d000

Real homelab values — domus hostnames, local IPs

build-adoc (no flag)

Zeroed UUIDs (00000000-…​), 0.0.0.0, UNDEFINED — nothing operational

Even if an attacker obtains the .adoc source files, building without the correct -a env-* flag produces a document with no usable infrastructure data. This complements the existing layers:

  • Layer 1: Encryptiond001 close encrypts partials at rest (.age)

  • Layer 2: Gitignore — plaintext data files blocked from tracking

  • Layer 3: Conditional rendering — production values require explicit build flag

  • Layer 4: Scrub rules — personal terms blocked by pre-push audit

No single layer is sufficient. Together they ensure that source, storage, and output are all protected independently.

Standard Established

This reference joins STD-004 (AsciiDoc Conventions) and STD-015 (Antora Attributes). The rule:

  • Every partials/attributes.adoc MUST use ifdef environment blocks — no naked attribute definitions that assume a single environment

  • Every Antora diagram SHOULD use Kroki blocks with subs=attributes+ — no hardcoded values in diagram source

  • Every standalone project SHOULD accept -a env-* at build time — same attributes, same conditionals, same source

Keylogger Project — Init

Keylogger Project — Init

Project Scaffolded
mkdir -p data/d000/projects/keylogger/{src,output,docs,include} \
  && touch data/d000/projects/keylogger/{Makefile,README.adoc,src/keylogger.c}
data/d000/projects/keylogger/
├── docs/          # Build notes, partials as you learn
├── include/       # Header files (.h)
├── Makefile       # Build rules
├── output/        # Compiled binary (gitignored)
├── README.adoc    # Project overview
└── src/
    └── keylogger.c  # Main source
What This Project Teaches
Concept How the Keylogger Uses It

open() syscall

Open /dev/input/eventN — raw device file access

read() syscall

Read struct input_event from the device — blocking I/O

C structs

struct input_event { struct timeval time; u16 type; u16 code; __s32 value; } — kernel data mapped directly to C types

ioctl()

EVIOCGNAME to get device name, EVIOCGRAB for exclusive access

File permissions

/dev/input/event* requires root or input group — why device permissions matter

select() / poll()

Monitor multiple input devices simultaneously

strace

Trace your own program: strace ./keylogger shows every syscall

/proc

/proc/bus/input/devices lists all input devices — find the keyboard

Makefile

gcc -Wall -Wextra -o output/keylogger src/keylogger.c

Signal handling

trap SIGINT for clean shutdown — release the grabbed device

First Milestone — 50 Lines

Read /dev/input/event*, print key press/release events to stdout. No filtering, no mapping, no features. Just prove you can read kernel input events from C.

# Find your keyboard device
cat /proc/bus/input/devices | awk '/keyboard/I,/^$/'

# Find the event number
ls -la /dev/input/by-id/ | grep -i kbd

# Run with sudo (needs input device access)
sudo ./output/keylogger /dev/input/eventN
thinkpad-t16g — Fixed Assembler (Same Session)

The thinkpad-t16g assembler in data/d000/projects/ was using {antora-partials} — a relative path reaching into docs/modules/ROOT/partials/. That’s Antora’s tree. Standalone build-adoc projects must be self-contained.

Fix applied:

  • Copied 28 partials from docs/modules/ROOT/partials/projects/thinkpad-t16g/ into data/d000/projects/thinkpad-t16g/partials/

  • Rewrote assembler: include::{antora-partials}/…​include::partials/…​

  • Added :toc: left and :toclevels: 3 for standalone rendering

  • xref: cross-references to Antora pages remain — render as plain text in standalone build, not broken

Test the fix
build-adoc data/d000/projects/thinkpad-t16g/thinkpad-t16g.adoc html --variant light-cyan \
  && firefox data/d000/projects/thinkpad-t16g/output/thinkpad-t16g.html &
Status
Item Status

Keylogger directory scaffolded

■■■■■■■■■■ 10/10

Keylogger assembler + partials

■■■■■■■■■■ 10/10

keylogger.c stub created

■■■■■■■■■■ 10/10

thinkpad-t16g assembler fixed

■■■■■■■■■■ 10/10 — standalone partials, no Antora cross-reference

First struct input_event read loop

□□□□□□□□□□ 0/10

Key code → character mapping

□□□□□□□□□□ 0/10

Multi-device select()

□□□□□□□□□□ 0/10

strace analysis of own program

□□□□□□□□□□ 0/10

Project Compliance Audit — STD-001 + STD-025

Project Compliance Audit — STD-001 + STD-025

Inventory of all standalone projects against the new standard: assembler, ifdef attributes, metadata, summary, diagrams.

Gold Standard (fully compliant)
Project Tier Build Command

keylogger

d000

build-adoc $(find data/d000/projects/keylogger -maxdepth 1 -name 'keylogger.adoc') html --variant light-cyan -a env-d000

thinkpad-t16g

d000

build-adoc $(find data/d000/projects/thinkpad-t16g -maxdepth 1 -name 'thinkpad-t16g.adoc') html --variant light-cyan -a env-d000

linux-research

d001

build-adoc $(find data/d001/projects/linux-research -maxdepth 1 -name 'linux-research.adoc') html --variant light-cyan -a env-d001

monad-pipeline

d001

build-adoc $(find data/d001/projects/monad-pipeline -maxdepth 1 -name 'monad-pipeline.adoc') html --variant light-cyan -a env-d001

research-segmentation

d001

build-adoc $(find data/d001/projects/research-segmentation -maxdepth 1 -name 'research-segmentation.adoc') html --variant light-cyan -a env-d001

Has Assembler — Missing Pieces (high priority)
Project Tier asm attr meta sum diag

bms-controller-segmentation

d001

PASS

FAIL

PASS

PASS

ise-acme-cert-automation

d001

PASS

PASS

FAIL

siem-qradar-to-sentinel

d001

PASS

FAIL

PASS

abnormal-security

d001

PASS

FAIL

PASS

bms-device-inventory

d001

PASS

FAIL

PASS

PASS

ise-annual-cert-renewal

d001

PASS

FAIL

FAIL

mandiant-remediation

d001

PASS

FAIL

FAIL

mschapv2-migration

d001

PASS

FAIL

FAIL

lingua-ponte

d000

PASS

FAIL

PASS

PASS

ender3-v3se

d000

PASS

FAIL

PASS

FAIL

FAIL

backups

d000

PASS

FAIL

PASS

FAIL

FAIL

Has Assembler — Needs Full Restructure
Project Tier

asa-vpn-okta-to-entra

d001

disaster-recovery

d001

dmz-migration

d001

downtime-computers

d001

ed-sewer-pump

d001

firewall-audit

d001

ipsk-manager-ha

d001

ise-34-migration

d001

ise-hardware-refresh

d001

isolation-malicious-hosts

d001

murus-portae

d001

network-diagram-library

d001

rad-projects

d001

tcp-clocks

d001

No Assembler — Needs Full Scaffold
Project Tier

resume-workshop

d000

security-assessment

d000

subscription-tracker

d000

terminal-warfare

d000

Antora Projects with Data Twins (dual-build)

Projects that exist in both docs/modules/ROOT/pages/projects/ (Antora) and data/d00x/projects/ (standalone). Both builds should work independently.

Project Antora Pages Data Twin Aligned?

thinkpad-t16g

25

d000

■■■■■■■■■■ 10/10

linux-research

4

d001

■■■■■■■■■■ 10/10

abnormal-security

10

d001

□□□□□□□□□□ 0/10

asa-vpn-okta-to-entra

13

d001

□□□□□□□□□□ 0/10

bms-device-inventory

3

d001

□□□□□□□□□□ 0/10

siem-qradar-to-sentinel

8

d001

□□□□□□□□□□ 0/10

ender3-v3se

11

d000

□□□□□□□□□□ 0/10

mandiant-remediation

9

d001

□□□□□□□□□□ 0/10

mschapv2-migration

6

d001

□□□□□□□□□□ 0/10

disaster-recovery

3

d001

□□□□□□□□□□ 0/10

downtime-computers

4

d001

□□□□□□□□□□ 0/10

firewall-audit

4

d001

□□□□□□□□□□ 0/10

ipsk-manager-ha

4

d001

□□□□□□□□□□ 0/10

ise-34-migration

4

d001

□□□□□□□□□□ 0/10

ise-annual-cert-renewal

10

d001

□□□□□□□□□□ 0/10

ise-hardware-refresh

3

d001

□□□□□□□□□□ 0/10

murus-portae

3

d001

□□□□□□□□□□ 0/10

network-diagram-library

1

d001

□□□□□□□□□□ 0/10

research-segmentation

4

d001

■■■■■■■■■■ 10/10

lingua-ponte

3

d000

□□□□□□□□□□ 0/10

Antora-Only — High-Value Data Twin Candidates

Projects that only exist in Antora but would benefit from a standalone data/ twin for offline PDF, field use, or ifdef security.

Project Antora Pages Why

ise-home-rotation

12

Active ISE work — standalone PDF for field use

eve-ng-lab

11

Lab topology — standalone for offline reference

rhel9-workstation

17

Deployment record — same pattern as thinkpad-t16g

zfold7-mobile

15

Device setup — portable reference

domus-mail-lab

13

Email config — sensitive, benefits from ifdef

edu-rhcsa

27

Largest education project — PDF handouts

edu-cissp

15

Exam prep — portable study material

Statistics
Metric Count

Total Antora projects

76

Total data/ projects

34

Gold standard (fully compliant)

5

Has assembler, missing pieces

11

Has assembler, needs full restructure

14

No assembler at all

4

Dual-build (Antora + data)

20

Antora-only, high-value twin candidates

7

Audit Command
# Re-run this audit anytime
for dir in data/d00{0,1}/projects/*/; do
  slug=$(basename "$dir")
  asm=$(find "$dir" -maxdepth 1 -name '*.adoc' -not -name 'README.adoc' -not -name '*.age' | head -1)
  attr=$(find "$dir/partials" -name 'attributes.adoc*' 2>/dev/null | head -1)
  meta=$(find "$dir/partials" -name 'metadata.adoc*' 2>/dev/null | head -1)
  sum=$(find "$dir/partials" -name 'summary.adoc*' 2>/dev/null | head -1)
  diag=$([ -d "$dir/diagrams" ] && echo "Y" || echo "")
  printf "%-35s asm:%-3s attr:%-3s meta:%-3s sum:%-3s diag:%-3s\n" \
    "$slug" \
    "$([ -n "$asm" ] && echo '✅' || echo '❌')" \
    "$([ -n "$attr" ] && echo '✅' || echo '❌')" \
    "$([ -n "$meta" ] && echo '✅' || echo '❌')" \
    "$([ -n "$sum" ] && echo '✅' || echo '❌')" \
    "$([ -n "$diag" ] && echo '✅' || echo '❌')"
done

Data Tier Audit

Data Tier Audit — Encryption & Structure

Ran a full audit of data/ as an AsciiDoc-adjacent system to docs/modules/. Three categories of findings: security gaps, missing tooling, and structural drift.

Security Findings
Finding Detail Severity

Plaintext residue on disk

260 files exist in both plaintext and .age form. All gitignored — no git exposure — but unencrypted on local disk. Heaviest in d001/comms/, d001/projects/*/partials/, d000/cursus/.

HIGH

BMS diagram sources tracked in plaintext

data/d001/projects/bms-controller-segmentation/diagrams/ — 11 files (.dot, .d2, .puml, .py) tracked in git without encryption. These contain switch hostnames, IP addressing, VLAN assignments, building names, device models. Rendered outputs are .age-encrypted, but the source that generates them is not.

HIGH

d000/personal/ directory permissions

World-readable (755). Combined with plaintext residue, any local user can read decrypted personal legal/medical documents.

MEDIUM

d002 has no shell function

d000 and d001 have open/close for encrypt/decrypt workflow. d002 does not exist — no automation for sensitive-work tier.

MEDIUM

Remediation Plan
Action Command / Approach Status

Bulk-clean plaintext residue

find data -name '*.age' -exec sh -c \
  'base="${1%.age}"; [ -f "$base" ] && echo "$base"' _ {} \; | wc -l

Review, then rm plaintext where .age is current.

□□□□□□□□□□ 0/10

Encrypt BMS diagram sources

Move .dot/.d2/.puml/.py to age-encrypted tracking. git rm plaintext, encrypt-file each, re-add .age. Update render-all.sh to decrypt-in-place before rendering.

□□□□□□□□□□ 0/10

Fix d000/personal permissions

chmod 700 data/d000/personal

□□□□□□□□□□ 0/10

Create d002 shell function

Model on d000/d001open/close/build subcommands targeting data/d002/. Add to dots-quantum zsh functions.

□□□□□□□□□□ 0/10

Pre-commit hook for residue detection

Hook that runs find data -name '*.age' -exec …​ and warns on plaintext+age pairs before allowing commit.

□□□□□□□□□□ 0/10

Structural Drift — d000

CLAUDE.md documents 5 subdirectories. Actual: 16. Undocumented directories:

  • commands/, correspondencia/, creative/, cursus/, daily-issues/

  • infrastructure/ (duplicates infra/), investigations/, lpl-study/

  • palaestra/, scripts/, shell-history/, templates/

Decision needed: update CLAUDE.md to reflect reality, or consolidate directories.

STD-001 Non-Compliance in d001 Projects

22 of 24 projects lack metadata.adoc. 23 lack summary.adoc. 21 lack root assembler .adoc. Only bms-controller-segmentation is fully compliant. This is technical debt — not a security issue — but blocks build-antora-page.sh for most d001 projects.

Root Cause

The d001 close workflow targets partials/*.adoc only. It does not:

  • Scan diagrams/, output/, scripts/, config-snapshots/ for non-.age sensitive files

  • Clean up plaintext after re-encryption

  • Warn when plaintext residue exists

The d000 close function has the same limitation — .adoc-only scope.

Both functions need a broader sweep: any non-README, non-.age file in a d00x project directory should be flagged during close.

Project Tracker Reconciliation

Project Tracker Reconciliation

Cross-referenced data/project-master.yaml, project-master.adoc, and codes.adoc to ensure all work projects are tracked and billable.

Query Toolkit

Full script with 5 sections (57 drills): indexes, billing, filters, deps, reports.

Run all queries
bash docs/modules/ROOT/examples/yq-drills/06-project-master.sh
Run a single section
bash docs/modules/ROOT/examples/yq-drills/06-project-master.sh indexes
bash docs/modules/ROOT/examples/yq-drills/06-project-master.sh billing
bash docs/modules/ROOT/examples/yq-drills/06-project-master.sh filters
bash docs/modules/ROOT/examples/yq-drills/06-project-master.sh deps
bash docs/modules/ROOT/examples/yq-drills/06-project-master.sh reports
Via drill runner
bash docs/modules/ROOT/examples/yq-drills/00-run-drill.sh 06
Script Source
#!/usr/bin/env bash
# YQ DRILL 06: PROJECT MASTER — REAL DATA QUERIES
# Usage: bash 06-project-master.sh [section]
#   bash 06-project-master.sh            # Run all sections
#   bash 06-project-master.sh indexes    # Pre-built index queries only
#   bash 06-project-master.sh billing    # PeopleSoft / time submission only
#   bash 06-project-master.sh filters    # Dynamic yq filters on projects[]
#   bash 06-project-master.sh deps       # Dependency chains and blockers
#   bash 06-project-master.sh reports    # Aggregate reports
#
# Data source: data/project-master.yaml (48 projects, 4 operational codes)
# Companion: partials/worklog/daily-notes/2026-06-22/project-tracker-reconciliation.adoc

set -euo pipefail

readonly YAML="data/project-master.yaml"
readonly DIVIDER="=================================================================="
readonly SUBDIV="------------------------------------------------------------------"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

die() { printf "FATAL: %s\n" "$1" >&2; exit 1; }

header() {
    printf "\n%s\n  %s\n%s\n\n" "$DIVIDER" "$1" "$DIVIDER"
}

drill() {
    local label="$1"; shift
    local cmd="$*"
    printf "%s\n" "$SUBDIV"
    printf "  %s\n" "$label"
    printf "  CMD: %s\n" "$cmd"
    printf "%s\n\n" "$SUBDIV"
    eval "$cmd"
    printf "\n"
}

# ---------------------------------------------------------------------------
# Validate
# ---------------------------------------------------------------------------

command -v yq > /dev/null || die "yq not found — install with: pacman -S yq"

if [[ ! -f "$YAML" ]]; then
    # Try from repo root
    if [[ -f "$HOME/atelier/_bibliotheca/domus-captures/$YAML" ]]; then
        cd "$HOME/atelier/_bibliotheca/domus-captures"
    else
        die "$YAML not found. Run from domus-captures root."
    fi
fi

yq '.' "$YAML" > /dev/null || die "$YAML is invalid YAML"

# ---------------------------------------------------------------------------
# Section: INDEXES — pre-built arrays, no filters needed
# ---------------------------------------------------------------------------

run_indexes() {
    header "SECTION 1: PRE-BUILT INDEX QUERIES"

    drill "1.01 — All P0 projects" \
        "yq '.by_priority.P0[]' $YAML"

    drill "1.02 — All P1 projects" \
        "yq '.by_priority.P1[]' $YAML"

    drill "1.03 — All P2 projects" \
        "yq '.by_priority.P2[]' $YAML"

    drill "1.04 — Overdue projects" \
        "yq '.by_status.overdue[]' $YAML"

    drill "1.05 — Blocked projects" \
        "yq '.by_status.blocked[]' $YAML"

    drill "1.06 — Active projects" \
        "yq '.by_status.active[]' $YAML"

    drill "1.07 — Behind schedule" \
        "yq '.by_status.behind[]' $YAML"

    drill "1.08 — TODO (not started)" \
        "yq '.by_status.todo[]' $YAML"

    drill "1.09 — Complete" \
        "yq '.by_status.complete[]' $YAML"

    drill "1.10 — Security auth projects" \
        'yq '"'"'.by_category["security-auth"][]'"'"' '"$YAML"

    drill "1.11 — Security compliance projects" \
        'yq '"'"'.by_category["security-compliance"][]'"'"' '"$YAML"

    drill "1.12 — Network infrastructure projects" \
        'yq '"'"'.by_category["network-infra"][]'"'"' '"$YAML"

    drill "1.13 — SIEM projects" \
        "yq '.by_category.siem[]' $YAML"

    drill "1.14 — Platform/cloud projects" \
        'yq '"'"'.by_category["platform-cloud"][]'"'"' '"$YAML"

    drill "1.15 — Deployments" \
        "yq '.by_category.deployments[]' $YAML"

    drill "1.16 — Documentation projects" \
        "yq '.by_category.documentation[]' $YAML"

    drill "1.17 — Evan's projects" \
        "yq '.by_owner.evan[]' $YAML"

    drill "1.18 — Vanessa Ahn's projects (bracket notation for hyphens)" \
        'yq '"'"'.by_owner["vanessa-ahn"][]'"'"' '"$YAML"

    drill "1.19 — Tony Sun's projects" \
        'yq '"'"'.by_owner["tony-sun"][]'"'"' '"$YAML"

    drill "1.20 — Alexander Urasaki's projects" \
        'yq '"'"'.by_owner["alexander-urasaki"][]'"'"' '"$YAML"

    drill "1.21 — Blocker map (what blocks what)" \
        "yq '.blockers' $YAML"

    drill "1.22 — Aging past 90 days" \
        "yq '.aging.critical[]' $YAML"

    drill "1.23 — Dependency chains" \
        "yq '.dependency_chains[]' $YAML"
}

# ---------------------------------------------------------------------------
# Section: BILLING — PeopleSoft codes, time submission
# ---------------------------------------------------------------------------

run_billing() {
    header "SECTION 2: PEOPLESOFT / TIME SUBMISSION"

    drill "2.01 — Admin codes (account, fund, dept, PC unit)" \
        "yq '.metadata.peoplesoft' $YAML"

    drill "2.02 — Projects WITH PeopleSoft codes (billable today)" \
        "yq -r '.projects[] | select(.peoplesoft.code != null) | \"\(.slug)\t\(.peoplesoft.code)\t\(.peoplesoft.combo // \"-\")\"' $YAML | column -t"

    drill "2.03 — Billable index (shortcut)" \
        "yq '.billable.with_code[]' $YAML"

    drill "2.04 — Needs code assignment" \
        "yq '.billable.needs_code[]' $YAML"

    drill "2.05 — Uses parent code (bill to parent)" \
        "yq '.billable.uses_parent[]' $YAML"

    drill "2.06 — Not billable yet (blocked/todo with no code)" \
        "yq '.billable.not_billable_yet[]' $YAML"

    drill "2.07 — BMS sub-projects with their own codes" \
        "yq '.projects[] | select(.sub_projects) | .sub_projects[]' $YAML"

    drill "2.08 — Operational codes (admin/meetings/support)" \
        "yq '.operational_codes[]' $YAML"

    drill "2.09 — Count: billable vs needs-code vs not-yet" \
        "printf 'billable:       %d\nneeds_code:     %d\nuses_parent:    %d\nnot_billable:   %d\n' \
         \$(yq '.billable.with_code | length' $YAML) \
         \$(yq '.billable.needs_code | length' $YAML) \
         \$(yq '.billable.uses_parent | length' $YAML) \
         \$(yq '.billable.not_billable_yet | length' $YAML)"

    drill "2.10 — Projects with PM assigned" \
        "yq -r '.projects[] | select(.peoplesoft.pm != null) | \"\(.slug)\t\(.peoplesoft.pm)\"' $YAML | column -t"

    drill "2.11 — Projects with combo codes" \
        "yq -r '.projects[] | select(.peoplesoft.combo != null) | \"\(.slug)\t\(.peoplesoft.code)\t\(.peoplesoft.combo)\"' $YAML | column -t"

    drill "2.12 — Blank timesheet weeks" \
        "for f in docs/modules/ROOT/partials/trackers/work/peoplesoft-codes/timesheet-*.adoc; do
           blank=\$(grep -c '_____' \"\$f\" 2>/dev/null || true)
           printf '%s\tblank: %d\n' \"\$(basename \"\$f\" .adoc)\" \"\$blank\"
         done"
}

# ---------------------------------------------------------------------------
# Section: FILTERS — dynamic yq queries on projects[]
# ---------------------------------------------------------------------------

run_filters() {
    header "SECTION 3: DYNAMIC FILTERS ON projects[]"

    drill "3.01 — Total project count" \
        "yq '.projects | length' $YAML"

    drill "3.02 — All slugs (sorted)" \
        "yq -r '.projects[].slug' $YAML | sort"

    drill "3.03 — All unique statuses" \
        "yq -r '.projects[].status' $YAML | sort -u"

    drill "3.04 — All unique categories" \
        "yq -r '.projects[].category' $YAML | sort -u"

    drill "3.05 — All unique priorities" \
        "yq -r '.projects[].priority' $YAML | sort -u"

    drill "3.06 — All unique owners (flattened)" \
        "yq -r '.projects[].owners[]' $YAML | sort -u"

    drill "3.07 — All unique tools in use" \
        "yq -r '.projects[].tools[]' $YAML | sort -u"

    drill "3.08 — Projects with d001 encrypted data" \
        "yq -r '.projects[] | select(.d001 != null) | \"\(.slug)\t\(.d001)\"' $YAML | column -t"

    drill "3.09 — Projects with Antora docs" \
        "yq -r '.projects[] | select(.docs != null) | \"\(.slug)\t\(.docs)\"' $YAML | column -t"

    drill "3.10 — Projects with NO d001 AND no docs (undocumented)" \
        "yq -r '.projects[] | select(.d001 == null and .docs == null) | .slug' $YAML"

    drill "3.11 — Projects by status count" \
        "yq -r '.projects[].status' $YAML | sort | uniq -c | sort -rn"

    drill "3.12 — Projects by category count" \
        "yq -r '.projects[].category' $YAML | sort | uniq -c | sort -rn"

    drill "3.13 — Projects by priority count" \
        "yq -r '.projects[].priority // \"unset\"' $YAML | sort | uniq -c | sort -rn"

    drill "3.14 — Projects with milestones" \
        "yq '.projects[] | select(.milestones) | {\"slug\": .slug, \"milestones\": .milestones}' $YAML"

    drill "3.15 — Projects originated in June 2026 (test() regex, not glob)" \
        "yq -r '.projects[] | select(.origin != null and (.origin | test(\"^2026-06\"))) | \"\(.slug)\t\(.origin)\"' $YAML | column -t"

    drill "3.16 — Projects with days > 90 (aging)" \
        "yq -r '.projects[] | select(.days != null and .days > 90) | \"\(.days)d\t\(.priority)\t\(.slug)\"' $YAML | sort -rn | column -t"

    drill "3.17 — Projects with days > 60 and no PeopleSoft code" \
        "yq -r '.projects[] | select(.days != null and .days > 60 and .peoplesoft.code == null) | \"\(.days)d\t\(.slug)\"' $YAML | sort -rn | column -t"

    drill "3.18 — P0 projects that are NOT active (stuck)" \
        "yq -r '.projects[] | select(.priority == \"P0\" and .status != \"active\") | \"\(.status)\t\(.slug)\"' $YAML | column -t"

    drill "3.19 — Projects that block others (from projects[] depends_on)" \
        "yq -r '.projects[] | select(.blocks | length > 0) | \"\(.slug) blocks: \(.blocks | join(\", \"))\"' $YAML"

    drill "3.20 — Projects that depend on something" \
        "yq -r '.projects[] | select(.depends_on | length > 0) | \"\(.slug) needs: \(.depends_on | join(\", \"))\"' $YAML"

    drill "3.21 — Full detail for a single project (e.g. ise-patch-10)" \
        "yq '.projects[] | select(.slug == \"ise-patch-10\")' $YAML"

    drill "3.22 — JSON output (Python yq already outputs JSON)" \
        "yq '.projects[] | select(.slug == \"abnormal-security\")' $YAML"

    drill "3.23 — TSV export: slug, priority, status, days, code" \
        "yq -r '.projects[] | \"\(.slug)\t\(.priority // \"-\")\t\(.status)\t\(.days // \"-\")\t\(.peoplesoft.code // \"-\")\"' $YAML | column -t | head -20"

    drill "3.24 — Projects using ISE tools" \
        "yq -r '.projects[] | select(.tools[] == \"ise-ers\" or .tools[] == \"ise-dataconnect\" or .tools[] == \"ise-radius\") | .slug' $YAML 2>/dev/null || echo '(none match — tools[] empty on some entries)'"

    drill "3.25 — Multi-owner projects" \
        "yq -r '.projects[] | select(.owners | length > 1) | \"\(.slug)\t\(.owners | join(\", \"))\"' $YAML | column -t"
}

# ---------------------------------------------------------------------------
# Section: DEPS — dependency chains and blocker analysis
# ---------------------------------------------------------------------------

run_deps() {
    header "SECTION 4: DEPENDENCIES & BLOCKERS"

    drill "4.01 — All dependency chains" \
        "yq '.dependency_chains[] | \"\(.label): \(.chain | join(\" → \"))\"' $YAML"

    drill "4.02 — Blocker map" \
        "yq '.blockers' $YAML"

    drill "4.03 — What does ise-patch-10 block?" \
        'yq '"'"'.blockers["ise-patch-10"][]'"'"' '"$YAML"

    drill "4.04 — What does k3s-nat block?" \
        'yq '"'"'.blockers["k3s-nat"][]'"'"' '"$YAML"

    drill "4.05 — What does linux-research block?" \
        'yq '"'"'.blockers["linux-research"][]'"'"' '"$YAML"

    drill "4.06 — What does monad-pipeline block?" \
        'yq '"'"'.blockers["monad-pipeline"][]'"'"' '"$YAML"

    drill "4.07 — What does ise-annual-cert-renewal block?" \
        'yq '"'"'.blockers["ise-annual-cert-renewal"][]'"'"' '"$YAML"

    drill "4.08 — Projects with zero blockers and zero dependencies (independent)" \
        "yq -r '.projects[] | select((.depends_on | length == 0) and (.blocks | length == 0)) | .slug' $YAML"

    drill "4.09 — Blocked projects with their blockers" \
        "yq -r '.projects[] | select(.status == \"blocked\") | \"\(.slug)\tblocked by: \(.depends_on | join(\", \"))\"' $YAML | column -t"

    drill "4.10 — Chain depth: projects that block something that blocks something else" \
        "yq -r '.dependency_chains[] | select(.chain | length > 2) | \"\(.chain | join(\" → \"))\t(\(.label))\"' $YAML | column -t"
}

# ---------------------------------------------------------------------------
# Section: REPORTS — aggregate views
# ---------------------------------------------------------------------------

run_reports() {
    header "SECTION 5: AGGREGATE REPORTS"

    drill "5.01 — Portfolio summary" \
        "printf 'Total projects:    %d\nWith PS code:      %d\nNeeds code:        %d\nOverdue:           %d\nBlocked:           %d\nActive:            %d\nComplete:          %d\nOperational codes: %d\n' \
         \$(yq '.projects | length' $YAML) \
         \$(yq '.billable.with_code | length' $YAML) \
         \$(yq '.billable.needs_code | length' $YAML) \
         \$(yq '.by_status.overdue | length' $YAML) \
         \$(yq '.by_status.blocked | length' $YAML) \
         \$(yq '.by_status.active | length' $YAML) \
         \$(yq '.by_status.complete | length' $YAML) \
         \$(yq '.operational_codes | length' $YAML)"

    drill "5.02 — Metadata" \
        "yq '.metadata' $YAML"

    drill "5.03 — Weekly time submission checklist" \
        "printf '=== Billable (have codes) ===\n'
         yq -r '.projects[] | select(.peoplesoft.code != null) | \"  ✓ \(.name) [\(.peoplesoft.code)]\"' $YAML
         printf '\n=== Bill to parent ===\n'
         yq -r '.billable.uses_parent[]' $YAML | while read -r slug; do printf '  → %s\n' \"\$slug\"; done
         printf '\n=== NEEDS CODE (hours are lost) ===\n'
         yq -r '.billable.needs_code[]' $YAML | while read -r slug; do printf '  ✗ %s\n' \"\$slug\"; done"

    drill "5.04 — Risk register: overdue + aging + blocked P0s" \
        "printf '%s\n' '=== OVERDUE ==='
         yq -r '.projects[] | select(.status == \"overdue\") | \"  \(.days)d  \(.slug)  \(.details)\"' $YAML
         printf '\n%s\n' '=== AGING > 90d ==='
         yq -r '.projects[] | select(.days != null and .days > 90) | \"  \(.days)d  \(.slug)\"' $YAML | sort -rn
         printf '\n%s\n' '=== BLOCKED P0 ==='
         yq -r '.projects[] | select(.priority == \"P0\" and .status == \"blocked\") | \"  \(.slug)  needs: \(.depends_on | join(\", \"))\"' $YAML"

    drill "5.05 — Full TSV export (pipe to file or column)" \
        "printf 'slug\tpriority\tstatus\tdays\tcategory\tcode\n'
         yq -r '.projects[] | \"\(.slug)\t\(.priority // \"-\")\t\(.status)\t\(.days // \"-\")\t\(.category)\t\(.peoplesoft.code // \"-\")\"' $YAML | column -t"

    drill "5.06 — YAML validation" \
        "yq '.' $YAML > /dev/null && echo 'YAML valid — 0 errors' || echo 'YAML BROKEN'"

    drill "5.07 — Index consistency check (with_code count vs actual)" \
        "printf 'Index says:  %d billable\n' \$(yq '.billable.with_code | length' $YAML)
         printf 'Actual:      %d have .peoplesoft.code\n' \$(yq '[.projects[] | select(.peoplesoft.code != null)] | length' $YAML)
         index=\$(yq '.billable.with_code | length' $YAML)
         actual=\$(yq '[.projects[] | select(.peoplesoft.code != null)] | length' $YAML)
         if [[ \"\$index\" -eq \"\$actual\" ]]; then
             echo 'CONSISTENT ✓'
         else
             echo 'DRIFT DETECTED — indexes out of sync with projects[]'
         fi"
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

main() {
    printf "%s\n" "$DIVIDER"
    printf "  PROJECT MASTER — YQ QUERY TOOLKIT\n"
    printf "  Data: %s (%d projects)\n" "$YAML" "$(yq '.projects | length' "$YAML")"
    printf "  Date: %s\n" "$(date +%Y-%m-%d)"
    printf "%s\n" "$DIVIDER"

    local section="${1:-all}"

    case "$section" in
        indexes)  run_indexes ;;
        billing)  run_billing ;;
        filters)  run_filters ;;
        deps)     run_deps ;;
        reports)  run_reports ;;
        all)
            run_indexes
            run_billing
            run_filters
            run_deps
            run_reports
            ;;
        *)
            printf "Unknown section: %s\n\n" "$section"
            printf "Sections: indexes | billing | filters | deps | reports | all\n"
            exit 1
            ;;
    esac

    printf "\n%s\n  DONE — %d drills executed\n%s\n" "$DIVIDER" "$(grep -c '^    drill ' "$0")" "$DIVIDER"
}

main "$@"
Quick Reference — Most Used Queries
Billable projects (have PeopleSoft codes)
yq '.billable.with_code[]' data/project-master.yaml
Needs code assignment (hours are lost)
yq '.billable.needs_code[]' data/project-master.yaml
All P0 projects
yq '.by_priority.P0[]' data/project-master.yaml
What’s blocking other work?
yq '.blockers' data/project-master.yaml
Aging past 90 days
yq '.aging.critical[]' data/project-master.yaml
By owner — bracket notation for hyphenated keys
yq '.by_owner["vanessa-ahn"][]' data/project-master.yaml
Portfolio summary
bash docs/modules/ROOT/examples/yq-drills/06-project-master.sh reports
Changes Made
  1. data/project-master.yaml — added peoplesoft: fields to every project. 7 pre-built index arrays (by_status, by_priority, by_category, by_owner, billable, blockers, aging). 11 new projects. Total: 48 projects + 4 operational codes.

  2. project-master.adoc — added 11 missing projects + new Deployments & Inventory section.

  3. codes.adoc — expanded to all 48 projects. Flagged NEEDS CODE where hours are billed without a PeopleSoft code.

  4. examples/yq-drills/06-project-master.sh — 57 drills across 5 sections. Registered in drill runner.

Gap Summary
Metric Value Notes

Projects with PS codes

8

Can submit time today

Active projects without codes

28

Hours can’t land in PeopleSoft

Estimated unbillable hours/week

~19 (~47%)

Change Mgmt, ISE Policy, Cert Renewal, Tube System, Admin

Stale priority files (p0/p1/p2.adoc)

3

Last updated April/May — contradict master

Next Steps
  • Get PeopleSoft codes for ISE Annual Cert Renewal, Tube System, Change Management, ISE Policy / General Support

  • Confirm which parent codes cover sub-projects (Monad → SIEM 000018281? rsyslog → SIEM?)

  • Create missing timesheets for weeks of 06-08, 06-15, 06-22

  • Decide: deprecate p0.adoc/p1.adoc/p2.adoc or sync with master

Git Glob Pathspec — Filter by File Type

Git Glob Pathspec — Filter by File Type

List only modified SVG and PNG files
git status --short -- ':(glob)**/*.svg' ':(glob)**/*.png'
Open all modified diagrams in browser at once
firefox $(git status --short -- ':(glob)**/*.svg' ':(glob)**/*.png' | awk '{print $2}')
:(glob) is a git pathspec magic — it tells git to interpret /*.svg as a glob pattern, not a literal path. Without it, doesn’t recurse. The --short flag gives clean M path/to/file output that awk '{print $2}' can extract.
Other useful pathspec patterns
# All modified AsciiDoc files
git status --short -- ':(glob)**/*.adoc'

# All modified shell scripts
git status --short -- ':(glob)**/*.sh'

# All modified files in a specific project
git status --short -- ':(glob)data/d001/projects/monad-pipeline/**'

# Diff only diagram sources (dot + d2)
git diff --stat -- ':(glob)**/*.dot' ':(glob)**/*.d2'

# Stage only SVG outputs (after diagram render)
git add -- ':(glob)**/*.svg'

The power: git pathspec lets you slice your working tree by file type without find | grep. One command, no pipes, git-native.

CLI — Searching Daily Notes & Partials

CLI — Searching Daily Notes & Partials

The Problem

Needed to find build-antora-page.sh invocations and --theme usage across daily note partials. The worklog tree has hundreds of .adoc files across date-organized directories — manual browsing doesn’t scale.

Pattern Escalation — Narrow to Broad
Level 1: Which files mention a command? (filenames only)
grep -rl 'build-antora-page' docs/modules/ROOT/partials/worklog/
Level 2: See the match in context (without opening the file)
grep -rn -C3 'build-antora-page' docs/modules/ROOT/partials/worklog/
Level 3: PCRE alternation — search for any theme flag
grep -rPn 'theme\s+(light-cyan|catppuccin|mocha)' docs/modules/ROOT/partials/
Level 4: find + grep — multi-pattern across the daily notes tree
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec grep -l 'build-antora-page\|build-adoc\|--theme\|pdf' {} \;
Level 5: find + awk — filename:matching_line (no -r needed)
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec awk '/build-antora-page|build-adoc|--theme/{print FILENAME": "$0}' {} \;
Level 6: awk state machine — extract entire [source,bash] blocks containing a command
find docs/modules/ROOT/partials -name '*.adoc' -exec \
  awk '/^\[source,bash\]/{block=1; buf=""} \
       block{buf=buf"\n"$0} \
       /^----$/ && block>1{if(buf~/build-antora-page/) print FILENAME":"buf; block=0} \
       block{block++}' {} \;

How the state machine works: [source,bash] sets block=1 and starts buffering. Each line appends to buf. When the closing ---- arrives (block>1 skips the opening fence), check if the buffer contains the target. Print if yes. Reset. One pass, arbitrary block size, no temp files.

Level 7: sed address range — lines between code fences matching a pattern
find docs/modules/ROOT/partials/worklog -name '*.adoc' -exec \
  sed -n '/\[source,bash\]/,/^----$/{/build-antora-page/p}' {} +
Level 8: search data/ too (standalone adoc outside Antora pipeline)
grep -rn 'build-antora-page\|build-adoc' data/ --include='*.adoc'
Level 9: every invocation sorted chronologically (path = date)
grep -rn 'build-antora-page' docs/modules/ROOT/partials/worklog/daily-notes/ \
  | awk -F: '{print $1, $3}' | sort
Tool Affordances — When to Reach for What
Tool Affordance When to Use

grep -rl

"Which files contain this?"

First pass — locate the files before reading them

grep -rn -C3

"What’s around the match?"

Quick context without opening the file

grep -rP

"Match complex patterns"

PCRE alternation, lookaheads, \K value extraction

find -exec grep

"Search a specific tree with fine-grained file filters"

When -r --include isn’t expressive enough (date dirs, depth limits)

find -exec awk

"Extract structured content around a match"

Code blocks, tables, multi-line patterns — awk tracks state

sed -n '/start/,/end/'

"Print between delimiters"

AsciiDoc fences, config sections, log ranges

awk state machine

"Extract variable-length blocks by content"

When sed address ranges aren’t enough — buffer + conditional print

Lesson Learned: tree Takes Paths, Not Rendered Text
What was tried (broken)
tree $(find -type d -name 'daily-notes' | xargs tree | grep 2026-06-22)
# Error: "└──": No such file or directory
# Error: "2026-06-22.adoc": No such file or directory

Why it broke: The inner xargs tree renders a tree diagram with unicode box-drawing characters (├──, └──). grep filters those text lines. The outer tree $(…​) tries to open └── and 2026-06-22.adoc as literal filesystem paths — they’re not paths, they’re display text.

The fix: feed paths to tree, not rendered text
# Direct — you know the path
tree docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/

# find locates, tree renders — one step
find -type d -name '2026-06-22' -exec tree {} \;

# find locates, command substitution feeds the path to tree
tree $(find -type d -name '2026-06-22')

The principle: tree consumes paths. find produces paths. tree output is display text — never feed it back into another command that expects paths. The pipeline is find → tree, never tree → grep → tree.

Antora vs Standalone Build

Antora vs Standalone Build

Antora builds the entire site to HTML. There is no single-page PDF export through the Antora pipeline. antora generate is all or nothing.

Antora Pipeline (HTML only, full site)
Build full site
cd ~/atelier/_bibliotheca/domus-docs
make
Build and serve locally
make serve

Output: docs.domusdigitalis.dev (via Cloudflare Pages on push) or localhost:8000 (local).

Single-Page PDF (build-antora-page.sh)

build-antora-page.sh resolves partial$ and example$ includes into a flat .adoc, then passes to build-adoc for PDF rendering. This is the only way to get a PDF of an Antora page.

Today’s worklog — all formats
make export-today
Just the PDF
make pdf FILE=docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-22.adoc
Direct script call with theme
./scripts/build-antora-page.sh docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-22.adoc pdf --theme light-cyan
All formats for a specific file
make export FILE=docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-22.adoc
Monthly summary
make export-month
How the Three-Layer Resolution Works
make pdf FILE=pages/.../WRKLOG-2026-06-22.adoc
  → build-antora-page.sh
    → rewrites partial$ → docs/modules/ROOT/partials/...
    → rewrites example$ → docs/modules/ROOT/examples/...
    → assembles flat .adoc (all includes inlined)
    → passes to build-adoc
      → asciidoctor-pdf with light-cyan theme
      → output/ directory

Layer 1 (worklog shell) includes Layer 2 (daily notes index), which includes Layer 3 (concern partials). build-antora-page.sh flattens the entire chain.

What Does NOT Work
Antora cannot export single-page PDF
# WRONG — no such option
antora generate --page WRKLOG-2026-06-22.adoc

# WRONG — build-adoc cannot resolve partial$ syntax
build-adoc docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-22.adoc pdf
Available Themes
Theme Use

light-cyan

Default — worklogs, daily notes, project docs

catppuccin

Domus Digitalis branded

base

Minimal

dark

Requires Noto Sans font dir

Override theme
make pdf FILE=docs/modules/ROOT/pages/2026/06/WRKLOG-2026-06-22.adoc PDF_THEME=catppuccin

Language ↔ Project Map

Language ↔ Project Relationship Map

What You Already Have

21 projects, 34 documentation repos, 55 education directories. Six languages already in active use. This isn’t a plan — it’s an inventory of what exists.

The Language Map
LANGUAGE        WHERE IT LIVES                              WHAT IT DOES FOR YOU
──────────────  ──────────────────────────────────────────  ─────────────────────────────────
Bash/Shell      dots-quantum (200+ functions in .zshrc)     Your daily driver — d000/d001,
                email-config (OAuth2 automation)             encryption workflows, dig loops,
                ansible-linux-automation (backup/restore)    git push patterns, pipeline
                ollama-local (GPU model scripts)             composition. ALREADY STRONG.
                data/d000/education/c/ (build scripts)
                data/shared/scripts/

Python          netapi (23 vendor integrations)             Work deliverables — ISE ERS,
                netapi-tui (terminal automation toolkit)     DataConnect, Monad pipeline,
                domus-cli (SSH orchestration)                API automation, data processing.
                domus-api (FastAPI backend)                  ALREADY PRODUCING.
                ise-automation (Ansible modules)
                terminal-warfare (raycasting engine)
                association-engine (knowledge graph)
                ollama-local (LLM query scripts)

Lua             domus-nvim (~90 plugins, 5-layer arch)     Your editor IS Lua. Your terminal
                instrumentum-nvim (reusable components)     config IS Lua. Wireshark
                domus-minimus (lightweight nvim)             dissectors ARE Lua. Every time
                wezterm config                               you edit init.lua, you're
                tmux-quantum (shell integration)             programming. NOT A BEGINNER.

SQL             ISE DataConnect queries (d001)              Every database you touch — ISE,
                Monad pipeline transforms                    QRadar, Sentinel, inventory.
                data/shared/ (query libraries)               ALREADY WRITING QUERIES.
                data/d001/projects/*/scripts/*.sql

C               data/d000/education/c/ (11-phase curriculum) The foundation beneath every
                Keylogger project (evdev, /proc)             tool you use. strace, /proc,
                                                             syscalls, buffer overflows.
                                                             COMMITTED — 11 PHASES.

JavaScript/TS   domus-digitalis (Next.js 15 + React 19)    The web app. Full stack with
                obsidian-asciidoc-viewer (React plugin)      PostgreSQL + Redis. EXISTS
                                                             but not your primary domain.

HCL             domus-terraform                              Infrastructure as Code. EXISTS.

Rust            crypta (educational roguelike)               Embryonic. After Go.

Go              (minimal — 13 lines)                         FUTURE — after C.
How the Languages Connect Through Your Projects
                            ┌─────────────┐
                            │   You (CLI)  │
                            └──────┬───────┘
                                   │
                    ┌──────────────┼──────────────┐
                    │              │              │
              ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
              │   Bash    │ │  Python   │ │    Lua    │
              │ (compose) │ │ (deliver) │ │(configure)│
              └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
                    │              │              │
    ┌───────────────┤    ┌────────┤    ┌─────────┤
    │               │    │        │    │         │
dots-quantum   d000/d001 netapi  Monad domus-  wezterm
email-config   workflows netapi- pipeln nvim    tmux-
ollama-local   dig loops  tui    domus- instru- quantum
backup/restore           ISE-auto api   mentum
                         assoc-eng
                    │              │              │
                    └──────────────┼──────────────┘
                                   │
                            ┌──────┴───────┐
                            │    SQL       │
                            │  (query)     │
                            └──────┬───────┘
                                   │
                    ISE DataConnect, Monad transforms,
                    QRadar, Sentinel, inventory systems
                                   │
                            ┌──────┴───────┐
                            │      C       │
                            │ (understand) │
                            └──────┬───────┘
                                   │
              strace, /proc, syscalls, evdev (keylogger),
              buffer overflows (CVE-2026-55200 makes sense),
              implement grep/wc/find, kernel modules
                                   │
                            ┌──────┴───────┐
                            │     Go       │
                            │   (build)    │
                            └──────┬───────┘
                                   │
              Static CLI binaries, infrastructure tools,
              Vault/Consul/Docker plugins, network servers
The Keylogger — Where It Fits

The keylogger project connects C, the kernel, and your security work:

Keyboard HW → IRQ → kernel input subsystem → /dev/input/eventN → evdev → your C program
                                                                    ↓
                                                              struct input_event {
                                                                struct timeval time;
                                                                __u16 type;   // EV_KEY
                                                                __u16 code;   // KEY_A
                                                                __s32 value;  // 1=press, 0=release
                                                              }

What you learn building it:

  • open(), read() syscalls — reading raw device files

  • struct packing — C structs mapping kernel data structures

  • /dev/input/ — the Linux input subsystem

  • ioctl() — device control (grab exclusive access)

  • File permissions — /dev/input/event* requires root or input group

  • select()/poll() — multiplexing I/O from multiple devices

  • strace to see your own program’s syscalls

  • Security implications — why input devices need permission controls

This is week 23–28 material (C + syscalls) but you’re already drawn to it. That’s fine — motivation beats curriculum order.

What Fascinates You — The Common Thread

You listed: APIs, system calls, arrays, bash scripting, mathematics, Python, Go, Lua, looping, pipelines.

The common thread is transformation. Every one of those is about taking data in one form and producing it in another:

Concept Transformation

Pipelines

stream → filter → transform → output

APIs

request → endpoint → process → response

Syscalls

userspace call → kernel → hardware → result

Arrays

collection → index/iterate → element

Loops

condition → body → side effect → repeat

SQL

tables → join/filter/aggregate → result set

Regex

text → pattern → matches

C structs

raw bytes → typed fields → meaningful data

You’re not learning 8 different things. You’re learning one thing — how data flows through systems — expressed in 8 different syntaxes. The keylogger is keyboard events flowing through the kernel. The Monad pipeline is syslog data flowing through transforms. The grep \| awk \| sort pipeline is text flowing through filters. Same pattern. Different layer.

Ecosystem Statistics
Metric Count

Personal projects

21

Documentation repos

34

Education directories

55

Languages in active use

6 (Bash, Python, Lua, SQL, JS/TS, HCL)

Languages in curriculum

3 more (C, Go, Rust)

Total repos under ~/atelier

55+

Vendor integrations (netapi)

23

Neovim plugins managed

~90

Literary study directories

32

Natural languages studied

5+ (English, Spanish, Latin, Hebrew, Italian)

This Is Not Crazy

An engineer who:

  • writes ISE automation in Python

  • composes data pipelines in bash

  • configures their editor in Lua

  • queries infrastructure in SQL

  • studies C to understand the kernel beneath

  • plans Go for infrastructure tooling

  • builds a keylogger to understand input events

  • reads Cervantes in the original Spanish

  • studies Latin rhetoric

  • practices violin

…​is not scattered. Every item on that list feeds the others. The Spanish feeds the Latin. The Latin feeds the rhetoric. The rhetoric feeds the documentation. The C feeds the strace. The strace feeds the debugging. The debugging feeds the incident response. The incident response feeds the automation. The automation feeds the Python. The Python feeds the APIs. The APIs feed the pipelines.

It’s one system. You just see more of it than most people.

Unix Tool Panorama + CLI CTF

The Unix Text Processing Lineage

Every tool below exists because someone needed to solve a problem that the previous tools couldn’t. The progression isn’t arbitrary — it’s an argument about where complexity should live.

The Lineage
ERA         TOOL/CONCEPT   BORN  CREATOR           WHAT IT SETTLED
─────────── ────────────── ───── ───────────────── ──────────────────────────────────────

                        === LANGUAGES (live inside the tools) ===

Substrate   regex (BRE)    1956  Stephen Kleene    Pattern matching — the grammar of search
            regex (ERE)    1968  POSIX             Extended: +, ?, |, () without escaping
            regex (PCRE)   1997  Philip Hazel      Lookahead, lookbehind, \K, non-greedy, Unicode
            glob patterns  1971  Bell Labs         Shell filename expansion: *, ?, [a-z], **
            brace expan.   1978  csh / Bill Joy    {a,b,c} and {1..10} — generate sequences
            param expan.   1977  Bourne Shell      ${var:-default}, ${var##*/}, ${#var} — string ops in-shell
            exit codes     1971  Unix v1           0=success, 1-255=failure — the boolean of pipes
            signals        1971  Unix v1           SIGTERM, SIGKILL, SIGHUP, trap — process communication
            file descr.    1971  Unix v1           0=stdin, 1=stdout, 2=stderr, 2>&1, /dev/fd/

                        === TOOLS (operate on streams) ===

Primordial  ed             1969  Ken Thompson      Line-addressable editing (no screen)
            grep           1973  Ken Thompson      Pattern search — born from ed's g/re/p
            egrep          1979  Alfred Aho        ERE support (now grep -E)
            sort           1970  Bell Labs         Lexicographic & numeric ordering
            uniq           1970  Bell Labs         Adjacent duplicate elimination
            wc             1971  Bell Labs         Counting: lines, words, bytes
            cat            1971  Bell Labs         Concatenate streams
            head/tail      1971  Bell Labs         First/last N lines
            tee            1974  Bell Labs         Split a stream: file + stdout
            tr             1973  Bell Labs         Character-level transliteration

Classical   sed            1974  Lee McMahon       Stream editing — ed without interaction
            awk            1977  Aho/Weinberger/   Column processing, pattern-action,
                                 Kernighan         BEGIN/END blocks, associative arrays
            find           1974  Bell Labs         Filesystem traversal with predicates
            cut/paste      1975  Bell Labs         Column extraction / horizontal merge
            diff           1974  Hunt/McIlroy      Line-by-line comparison
            comm           1973  Bell Labs         Set operations on sorted files
            xargs          1978  Bell Labs         Stdin → arguments (batching, parallel)
            file           1973  Bell Labs         Content-type detection (not extension)
            strings        1978  Bell Labs         Extract printable from binary
            test / [       1971  Bell Labs         Conditional evaluation in shell
            expr           1971  Bell Labs         Arithmetic and string ops (before $(()))

Shell       sh (Bourne)    1977  Stephen Bourne    Scripting: variables, loops, functions, here-docs
            csh            1978  Bill Joy          Brace expansion, history, aliases
            ksh            1983  David Korn        Arrays, arithmetic, coprocess
            bash           1989  Brian Fox         GNU Bourne-again — interactive + scripting
            zsh            1990  Paul Falstad      Programmable completion, glob qualifiers, zmv

Imperial    vi/vim         1976  Bill Joy / Bram   Modal editing — the survivor
            less           1983  Mark Nudelman     Paginated viewing (more, but less)
            perl           1987  Larry Wall        "Swiss Army chainsaw" — regex + I/O
            GNU coreutils  1983+ FSF               Long flags, -i for in-place, --color
            strace         1991  Paul Kranenburg   Syscall tracing — see what the kernel sees
            ltrace         1997  Juan Cespedes     Library call tracing
            lsof           1994  Vic Abell         List open files — process ↔ file map

System      /proc          1984  Tom Killian       Kernel data as files — process, memory, network
            /sys           2003  Patrick Mochel    Device/driver data as files
            /dev/fd        1977  Bell Labs         File descriptors as paths
            ss             2001  Alexey Kuznetsov  Socket statistics (replaced netstat)
            ip             1999  Alexey Kuznetsov  Network config (replaced ifconfig)
            journalctl     2011  Lennart Poettering Structured log querying
            systemctl      2010  Lennart Poettering Service management

Modern      jq             2012  Stephen Dolan     JSON query language (like awk for JSON)
            yq             2017  Mike Farah        YAML/XML query (jq syntax for YAML)
            ripgrep (rg)   2016  Andrew Gallant    grep + .gitignore + speed + Unicode
            fd             2017  David Peter       find + .gitignore + speed + color
            fzf            2013  Junegunn Choi     Fuzzy finder — stdin → interactive pick
            bat            2018  David Peter       cat + syntax highlighting + git diff
            eza            2023  Community         ls + git status + color + tree
            delta          2019  Dan Davison       diff + syntax highlighting + side-by-side
            sd             2018  Gregory Neverov   sed + modern regex (no backslash hell)
            hyperfine      2018  David Peter       Benchmarking — statistical CLI timing
            tree           1996  Steve Baker       Directory structure visualization
            stat           1986  BSD               Inode metadata: times, size, permissions
            numfmt         2013  GNU               Byte → human-readable conversion
            column         1989  BSD               Tabular alignment

Binary      xxd            1990  Juergen Weigert   Hex dump and reverse — binary ↔ hex
            od             1971  Bell Labs         Octal dump (the original hex viewer)
            hexdump        1986  BSD               Flexible format hex/ascii dump
            binwalk        2010  Craig Heffner     Firmware extraction, embedded file detection
            objdump        1990  GNU               Disassemble binary → assembly

Network     tcpdump        1988  Van Jacobson      Packet capture at the wire
            tshark         1998  Gerald Combs      Wireshark CLI — deep protocol dissection
            ngrep          2001  Jordan Ritter     grep for network packets
            curl           1997  Daniel Stenberg   HTTP/FTP/SFTP client — the universal fetcher
            nc (netcat)    1996  Hobbit            Raw TCP/UDP — the "Swiss Army knife of networking"
            ss             2001  Alexey Kuznetsov  Socket statistics — what's connected, what's listening
            dig            1997  ISC               DNS query tool — authoritative lookups
            nmap           1997  Gordon Lyon       Port scanning, service detection, scripting
The Design Argument

Each era makes the same argument differently:

Era Philosophy

Primordial

One tool, one job. Compose with pipes. grep finds, sort orders, uniq deduplicates — the pipeline is the program.

Classical

Pattern-action processing. sed applies transforms per line. awk adds columns, state, and computation. find adds filesystem predicates. The tool knows more about structure.

Imperial

The general-purpose escape hatch. When pipelines get unwieldy, perl and vim absorb the complexity into one process. Power at the cost of readability.

Modern

Speed + ergonomics + awareness. ripgrep respects .gitignore. fd has sane defaults. jq understands JSON structure. fzf makes interactive selection composable. The tool knows about your workflow, not just your data.

The lesson: don’t reach for perl when awk suffices. Don’t reach for awk when grep suffices. But do reach for jq over grep when the data is JSON — the tool should match the structure.


Capture the Flag — Your Codebase, Your Tools

All challenges use your actual domus-captures repository. No synthetic data. Answers use the tools from the lineage above — escalating from primordial through modern.

Rules:

  • Every answer must be a single pipeline (no scripts, no temp files)

  • Multiple correct answers exist — find the one that fits the tool’s affordance

  • Difficulty marked: 🟢 warm-up, 🟡 working, 🔴 senior, ⚫ architect


Round 1 — Reconnaissance (grep, find, wc)
🟢 FLAG 1: How many .adoc partials exist in today’s daily notes?
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f | wc -l
🟢 FLAG 2: Which of today’s partials mentions "CVSS"?
grep -rl 'CVSS' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/
🟢 FLAG 3: How many total lines did you write today across all 19 partials?
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f \
  -exec cat {} + | wc -l
🟡 FLAG 4: Which partial is the largest by line count? Show filename and count, sorted.
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f \
  -exec wc -l {} + | sort -rn | head -20

Round 2 — File Intelligence (stat, find -printf, file)
🟡 FLAG 5: Which partial was modified most recently? Show ISO timestamp + filename.
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f \
  -printf '%T+ %p\n' | sort -r | head -1
🟡 FLAG 6: Show the birth time (creation time) of the CVE summary partial.
stat --format='Birth: %w' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/cve-2026-55200-summary.adoc
🟡 FLAG 7: List all of today’s partials sorted by size in bytes, with human-readable sizes.
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f \
  -printf '%s\t%p\n' | sort -rn | numfmt --to=iec --field=1
🔴 FLAG 8: Which partials were created AFTER 12:00 today? (birth time filter)
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f \
  -newerBt '2026-06-22 12:00' -printf '%T+ %p\n' | sort
-newerBt compares birth time (B) against a timestamp (t). Not all filesystems support birth time — ext4 does, tmpfs does not.

Round 3 — Content Extraction (awk, sed, cut)
🟡 FLAG 9: Extract every AsciiDoc section heading (lines starting with ====) from today’s partials. Show filename:heading.
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f \
  -exec awk '/^====/{print FILENAME": "$0}' {} \;
🟡 FLAG 10: Count how many [source,bash] code blocks exist across today’s partials.
grep -rc '\[source,bash\]' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/ \
  | awk -F: '{sum+=$2} END{print sum, "code blocks"}'
🔴 FLAG 11: Extract every URL from the CVE teams-response partial. URLs only, one per line, deduplicated.
grep -oP 'https?://[^\[, \]]+' \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/cve-2026-55200-teams-response.adoc \
  | sort -u
🔴 FLAG 12: Build a table showing: partial name | section count | code block count | line count. Aligned columns.
printf "%-45s %8s %8s %8s\n" "PARTIAL" "SECTIONS" "BLOCKS" "LINES"
printf "%-45s %8s %8s %8s\n" "-------" "--------" "------" "-----"
for f in docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/*.adoc; do
  name=$(basename "$f")
  sections=$(grep -c '^====' "$f" 2>/dev/null)
  blocks=$(grep -c '\[source,' "$f" 2>/dev/null)
  lines=$(wc -l < "$f")
  printf "%-45s %8d %8d %8d\n" "$name" "$sections" "$blocks" "$lines"
done

Round 4 — Cross-File Analysis (awk state machines, comm, diff)
🔴 FLAG 13: Which d001 projects are mentioned in today’s partials but NOT in project-master.yaml? (Set difference.)
comm -23 \
  <(grep -rohP '(?<=projects/)[a-z0-9-]+' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/ | sort -u) \
  <(grep -oP '^\s+slug:\s*\K\S+' data/project-master.yaml | sort -u)

comm -23 shows lines only in file 1. Process substitution <(…​) avoids temp files. (?⇐projects/) is a PCRE lookbehind — match projects/ but don’t include it.

🔴 FLAG 14: Extract every complete [source,bash] block from the triage-script partial. Print with line numbers.
awk '/^\[source,bash\]/{start=NR} start && /^----$/{if(fence++) {fence=0; start=0; print "---"} else {next}} start{printf "%4d  %s\n", NR, $0}' \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/cve-2026-55200-triage-script.adoc
⚫ FLAG 15: Produce a dependency graph: which partials include:: other partials? Show as parent → child pairs.
grep -rn 'include::partial\$worklog/daily-notes/2026-06/2026-06-22/' \
  docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/ \
  | awk -F: '{
      parent=$1; sub(/.*\//, "", parent)
      match($0, /2026-06-22\/([^[]+)/, a)
      if(a[1]) printf "  %s → %s\n", parent, a[1]
    }'

Round 5 — Modern Tools (ripgrep, fd, fzf, jq, yq)
🟡 FLAG 16: Use yq to count how many projects in project-master.yaml have status "active".
yq '[.projects[] | select(.status == "active")] | length' data/project-master.yaml
🔴 FLAG 17: Use fzf to interactively pick a partial from today, then open it in $EDITOR.
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' \
  | fzf --preview 'head -40 {}' | xargs "$EDITOR"
🔴 FLAG 18: Use process substitution + diff to compare today’s section headings vs yesterday’s.
diff \
  <(grep -rh '^====' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-21/ | sort) \
  <(grep -rh '^====' docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22/ | sort)
⚫ FLAG 19: One pipeline — find today’s largest partial, extract its section headings, show them numbered with awk.
find docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-22 -name '*.adoc' -type f \
  -printf '%s %p\n' | sort -rn | awk 'NR==1{print $2}' \
  | xargs grep '^====' | awk -F: '{gsub(/^=+ /, "", $2); printf "%2d. %s\n", NR, $2}'

Three stages: find -printf gets sizes, sort -rn | awk 'NR==1' picks the largest, xargs grep extracts headings, final awk numbers them. No temp files, no variables — pure pipeline.


Scoring
Flags Captured Rank Next Step

1–5

Recruit

Master grep -r flags, find -name -type, glob patterns

6–10

Operator

Regex: BRE → ERE → PCRE. awk field extraction. stat metadata.

11–14

Specialist

awk state machines, comm set operations, PCRE \K extraction

15–17

Engineer

yq/jq, fzf, pipeline composition, shell functions, process substitution

18–19

Architect

Multi-stage pipelines from memory. Teach others what you know.

The Full Landscape

The lineage table, the CTF flags, and the curriculum arc together form a single reference. This is the map of the territory:

SUBSTRATE (languages that live inside the tools)
  │
  ├── regex ──────── BRE → ERE → PCRE (the grammar of search)
  ├── glob ──────── *, ?, [a-z], ** (filename expansion)
  ├── brace exp ─── {a,b}, {1..10} (sequence generation)
  ├── param exp ─── ${var##*/}, ${var:-x} (string ops without tools)
  ├── exit codes ── $?, &&, ||, set -e (the boolean of pipes)
  ├── signals ───── trap, SIGTERM, SIGHUP (process communication)
  └── file desc ─── 0/1/2, 2>&1, >()/< () (stream routing)

TOOLS (operate on streams)
  │
  ├── Search ────── grep → egrep → ripgrep
  ├── Traverse ──── find → fd → tree
  ├── Transform ─── sed → awk → perl → jq/yq
  ├── Compare ───── diff → comm → delta
  ├── Measure ───── wc → stat → du → hyperfine
  ├── Route ─────── tee → xargs → parallel
  └── Inspect ───── file → strings → xxd → binwalk → objdump

SYSTEMS (the kernel beneath)
  │
  ├── /proc ─────── process state as files
  ├── /sys ──────── device state as files
  ├── strace ────── syscall visibility
  ├── lsof ──────── process ↔ file map
  └── C ─────────── write the tools yourself

NETWORK (packets and protocols)
  │
  ├── tcpdump ───── capture at the wire
  ├── tshark ────── deep protocol dissection
  ├── ss ─────────── socket state
  ├── dig ─────────── DNS query
  ├── nmap ────────── service discovery
  └── nc ──────────── raw TCP/UDP

Self-Teaching with --help and man — The Recursive Method

The tools teach themselves. man pages are plain text — which means every tool in the lineage works on them. You learn grep by grepping the grep man page.

The Basics — How to Read
Quick flag lookup — what does -o do?
# --help is the cheat sheet (fits in a terminal)
grep --help 2>&1 | grep '\-o'

# man is the textbook (complete, examples, edge cases)
man grep | grep -A5 '^\s*-o,'
Browse man pages with search (inside less)
man awk
# Type /pattern to search forward
# Type ?pattern to search backward
# Type n for next match, N for previous
# Type q to quit
Which section? man has 9 sections
# Section 1: user commands (what you want 90% of the time)
# Section 5: file formats (config files)
# Section 7: conventions (regex, ascii, signal)
# Section 8: admin commands (root-level)

man 1 find    # the command
man 7 regex   # POSIX regex syntax reference
man 5 ssh_config  # SSH config file format
man ascii     # the ASCII table — indispensable for hex/CTF work
Mining Man Pages with the Tools Themselves
grep the grep man page — find every flag that mentions "count"
man grep | grep -i 'count'
awk the awk man page — extract all built-in variable names
man awk | grep -oP '\b(NR|NF|FS|RS|OFS|ORS|FILENAME|FNR|ARGV|ARGC)\b' | sort -u
find every example in a man page (indented lines after EXAMPLES)
man find | awk '/^EXAMPLES/,/^[A-Z]/{print}'
extract all flags from --help into a sorted list
grep --help 2>&1 | grep -oP '^\s*-\S+' | sort
compare flags between two tools — what does find have that grep doesn’t?
diff <(find --help 2>&1 | grep -oP '^\s*-\S+' | sort) \
     <(grep --help 2>&1 | grep -oP '^\s*-\S+' | sort)
search ALL man pages for a concept you don’t know the tool for
# "I need to compare two files" — which tools mention "compare"?
apropos compare

# "I need to manipulate timestamps"
apropos timestamp

# "I need to process columns"
apropos column
read a specific section of a man page without scrolling
# Extract just the OPTIONS section from the find man page
man find | awk '/^OPTIONS$/,/^[A-Z]/' | head -80

# Extract just EXAMPLES from awk
man awk | awk '/^EXAMPLES/,/^[A-Z]/' | head -40
build a personal cheat sheet from man pages
# Extract every flag + its description from grep
man grep | awk '/^       -/{flag=$0; getline; print flag, $0}'

The recursive beauty: you’re using awk to parse the awk man page. You’re using grep -oP to extract patterns from the grep documentation. The act of learning is the practice.


Daily Practice Standard — Palaestra Digitalis

Palaestra: the ancient training ground. Five minutes. Every day. No exceptions.

The Format

Each day’s daily notes includes one palaestra-YYYY-MM-DD.adoc partial:

partials/worklog/daily-notes/YYYY-MM-DD/palaestra.adoc
Structure
==== Palaestra — YYYY-MM-DD

===== Challenge

[description of the challenge — what to find, extract, transform, compare]

===== Constraints

* Tools allowed: [specific tools for this challenge]
* Time limit: 5 minutes
* Bonus: [harder variant using a more advanced tool]

===== My Answer

[source,bash]
\----
# Your solution here — filled in during practice
\----

===== Instructor Answer

[%collapsible]
=====
[source,bash]
\----
# Reference solution with explanation
\----
=====

===== What I Learned

* [one line — the affordance, the flag, the pattern that stuck]
Curriculum Arc

The challenges follow a deliberate progression tied to real work:

Phase Tools + Substrate Challenge Source

Weeks 1–2

grep, find, wc, sort, uniq, head/tail, tee
Substrate: glob patterns (, ?, [a-z], *), exit codes ($?, &&, ||)

Search your own worklogs, count patterns, find files. Learn to read --help output.

Weeks 3–4

Regex deep dive: BRE (grep), ERE (grep -E), character classes, quantifiers, anchors, alternation
Substrate: brace expansion ({1..10}, {a,b,c}), man 7 regex

Regex drills against your own AsciiDoc. Extract IPs, dates, URLs, headings. Build patterns from man pages.

Weeks 5–6

PCRE: grep -P, lookahead (?=), lookbehind (?⇐), \K, non-greedy ?, named groups
*Substrate:
parameter expansion (${var##*/}, ${var:-default}, ${#var})

ISE log parsing with \K. Extract key=value pairs. Parameter expansion for path manipulation without basename.

Weeks 7–10

awk (fields, printf, arrays, BEGIN/END, state machines), sed (address ranges, hold buffer, in-place), cut/paste/tr
Substrate: file descriptors (2>&1, >/dev/null, 3<>file), here-docs, here-strings

Extract structured data from AsciiDoc. Transform configs with sed. awk state machines for code block extraction. Redirect patterns.

Weeks 11–14

stat, file, strings, xxd, diff, comm, process substitution <()
Substrate: signals (trap, SIGTERM, SIGHUP), /proc filesystem

File forensics: birth time, inode analysis. Set operations with comm. Binary inspection with xxd and strings. Process substitution for temp-file-free comparison.

Weeks 15–18

jq, yq, xargs -P, find -exec, pipeline composition, fzf, curl
Substrate: shell scripting (functions, loops, arrays, getopts), test/[[ ]] conditionals

API response parsing. Parallel operations. Multi-stage pipelines. Write shell functions that compose tools. project-master.yaml as training data for yq.

Weeks 19–22

strace, ltrace, lsof, /proc, /sys
Substrate: syscalls (open, read, write, mmap, execve), file descriptor inheritance

Trace what tools actually do. strace grep to see the syscalls. lsof to map processes to files. Read /proc/self/maps.

Weeks 23–28

C (stdio, syscalls, sockets, /proc parsing)
Substrate: compilation, linking, ELF format, objdump, nm

Write the tools — implement grep, wc, find in C. Parse /proc in C. Understand what the kernel does beneath every pipeline.

Weeks 29–34

ss, ip, tcpdump, tshark, ngrep, dig, nmap, nc
Substrate: TCP/IP stack, DNS resolution, packet structure, socket states

Network forensics. Capture and parse traffic. DNS enumeration. Port scanning. Raw socket communication with nc.

Weeks 35–40

binwalk, hexdump, objdump, xxd (advanced), file signatures, encoding detection
Substrate: hex/binary, base64, rot13, ELF headers, magic bytes

Binary analysis. Extract embedded files. Identify encodings. Reverse-engineer file formats.

Weeks 41+

External CTF challenge sets, DFIR scenarios, incident simulations
Substrate: everything above, composed under pressure

Real flags. Real time limits. The palaestra becomes the arena. Teach others.

How It Gets Generated

Each session, the challenge is drawn from one of three sources:

  1. Today’s work — something you actually did, reframed as a CLI puzzle

  2. Man page exploration — "find a flag in man find that you’ve never used, then use it"

  3. Curriculum progression — the next step in the arc above

The challenge appears at the start of the session. You solve it. You document what you learned. Five minutes. Then you do your real work — which is itself more practice.

The Commitment

This is not a suggestion. This is a standard. It joins STD-014 (Worklog System) as part of the daily notes architecture:

  • make new-day scaffolds the worklog shell

  • The palaestra partial is added to the daily notes index

  • The challenge is generated at session start

  • The answer is filled in during practice

  • The learning is captured for the permanent record

Every day. Even if it’s one grep flag you didn’t know existed. The compound effect of 365 five-minute sessions is not five minutes — it’s a different engineer.

Annual Review — Finalization

Annual Review — Finalization & Submission

All 24 cells populated. 3 Exemplary / 5 Effective. C239 intentionally blank. Follow the submission runbook below — steps 0 through 9 in order. Toolkit scripts for inspection and editing follow after the runbook.

Submission Runbook — Start to End
0. Push from home, pull at work
# HOME — push latest commits
git -C ~/atelier/_bibliotheca/domus-captures push origin main

# WORK — pull and open this file
git -C ~/atelier/_bibliotheca/domus-captures pull
v $(find docs -path '*/2026-06-22/annual-review.adoc')
1. Setup — venv (gone on reboot, recreate each session)
python3 -m venv /tmp/xlsx-env
/tmp/xlsx-env/bin/pip install openpyxl -q
2. Decrypt
decrypt-file data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx.age
3. Apply SIEM framing corrections (v2)

Softens ownership language — you built the pipeline and analysis, not driving the entire migration.

XLSX="data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx"
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/annual-review-v2-applied.txt
import openpyxl

wb = openpyxl.load_workbook('${XLSX}')
ws = wb['SelfEval']

changes = [
    ('C40',
     'SIEM Migration: QRadar to Microsoft Sentinel',
     'SIEM Migration Planning: QRadar to Microsoft Sentinel'),
    ('I49',
     'Architecting QRadar-to-Sentinel SIEM migration',
     'Building the collection and analysis tier for QRadar-to-Sentinel SIEM migration'),
    ('C66',
     'Drive the SIEM migration to Microsoft Sentinel to operational capability with validated log coverage across ISE, ASA, Windows, and firewall sources.',
     'Advance the SIEM collection and ETL pipeline toward operational capability, supporting validated log coverage across ISE, ASA, Windows, and firewall sources.'),
]

for cell, old, new in changes:
    val = ws[cell].value
    if old in val:
        ws[cell] = val.replace(old, new)
        print(f"[{cell}] ✅ replaced:")
        print(f"  v1: {old}")
        print(f"  v2: {new}")
    else:
        print(f"[{cell}] ⚠ pattern not found — already applied or changed")

wb.save('${XLSX}')
print("\nSaved.")
PYEOF
4. Verify all cells
XLSX="data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx"
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/annual-review-final-check.txt
import openpyxl

wb = openpyxl.load_workbook('${XLSX}')
ws = wb['SelfEval']

cells = [
    ('D32',  'Name',            'value'),
    ('D36',  'Title',           'value'),
    ('J91',  'Job Skills',      'value'),
    ('J109', 'Outcomes',        'value'),
    ('J127', 'Service',         'value'),
    ('J162', 'Collaboration',   'value'),
    ('J170', 'Empathy',         'value'),
    ('J178', 'Accountability',  'value'),
    ('J187', 'Growth',          'value'),
    ('J196', 'Service Care',    'value'),
    ('C40',  'Contributions',   'len'),
    ('G40',  'Meaningful',      'len'),
    ('I49',  'Indiv Goal',      'len'),
    ('C66',  'New Indiv Goal',  'len'),
    ('C71',  'New Team Goal',   'len'),
    ('C102', 'Skills Comments', 'len'),
    ('C120', 'Outcomes Cmt',    'len'),
    ('C207', 'Values Comments', 'len'),
    ('C232', 'Leader Support',  'len'),
    ('C239', 'Additional',      'len'),
]

optional = {'C239'}
empty = []

for cell, label, mode in cells:
    raw = ws[cell].value
    if mode == 'len':
        chars = len(str(raw or ''))
        print(f"{cell:5s} {label:20s} {chars} chars")
        if chars == 0 and cell not in optional:
            empty.append(cell)
    else:
        print(f"{cell:5s} {label:20s} {raw}")
        if raw is None and cell not in optional:
            empty.append(cell)

if empty:
    print(f"\n⚠ EMPTY: {', '.join(empty)}")
else:
    print("\n✅ All required cells populated (C239 blank by design)")
PYEOF
5. Confirm v2 framing took effect
XLSX="data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx"
PATTERN="siem|sentinel|monad|architect|drive the"
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/annual-review-siem-check.txt
import openpyxl, re

wb = openpyxl.load_workbook('${XLSX}')
ws = wb['SelfEval']

pattern = re.compile(r'${PATTERN}', re.IGNORECASE)
for cell in ['C40', 'I49', 'C66']:
    val = str(ws[cell].value or '')
    hits = [line.strip() for line in val.split('\n') if pattern.search(line)]
    if hits:
        print(f"\n[{cell}]")
        for h in hits:
            print(f"  {h}")
PYEOF
6. Open and spot-check
xdg-open data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx

Verify I49 and I57 — confirm [Yes] / [In Progress] render correctly (strip brackets if literal text).

8. Re-encrypt
rm data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx.age
encrypt-file data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx
rm data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx
9. Commit and push updated .age
git -C ~/atelier/_bibliotheca/domus-captures add data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx.age
git -C ~/atelier/_bibliotheca/domus-captures commit -m "docs(d000): update annual review xlsx with v2 SIEM framing (encrypted)"
git -C ~/atelier/_bibliotheca/domus-captures push origin main
Table 3. SIEM framing changes applied in step 3
Cell v1 (before) v2 (after)

C40

"SIEM Migration: QRadar to Microsoft Sentinel"

"SIEM Migration Planning: QRadar to Microsoft Sentinel"

I49

"Architecting QRadar-to-Sentinel SIEM migration"

"Building the collection and analysis tier for QRadar-to-Sentinel SIEM migration"

C66

"Drive the SIEM migration to Microsoft Sentinel…​"

"Advance the SIEM collection and ETL pipeline toward operational capability…​"


Toolkit — Inspection & Editing Scripts

The scripts below are for ad-hoc inspection and editing. Not part of the submission flow — use as needed.

Discover — Dump All Cells (any xlsx)
XLSX="data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx"
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/xlsx-dump-$(date +%F).txt
import openpyxl

wb = openpyxl.load_workbook('${XLSX}')

for sheet_name in wb.sheetnames:
    ws = wb[sheet_name]
    print(f"\n=== {sheet_name} ({ws.max_row} rows x {ws.max_column} cols) ===")
    for row in ws.iter_rows(max_row=ws.max_row, max_col=ws.max_column, values_only=False):
        for cell in row:
            if cell.value is not None:
                raw = cell.value
                if hasattr(raw, 'text'):
                    val = f"={raw.text}"
                else:
                    val = str(raw)
                chars = f"({len(val)} chars)" if len(val) > 70 else ""
                preview = val[:70] + "..." if len(val) > 70 else val
                print(f"  [{cell.coordinate}] {preview} {chars}")
PYEOF

Then search the dump:

PATTERN="ISE|TEAP|CVE|Exemplary|Effective"
grep -Pin "$PATTERN" /tmp/xlsx-dump-$(date +%F).txt
Read All Cells — Full Text (any xlsx)
XLSX="data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx"
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/xlsx-full-$(date +%F).txt
import openpyxl

wb = openpyxl.load_workbook('${XLSX}')

for sheet_name in wb.sheetnames:
    ws = wb[sheet_name]
    print(f"\n{'=' * 70}")
    print(f"=== {sheet_name} ({ws.max_row} rows x {ws.max_column} cols) ===")
    print(f"{'=' * 70}")
    for row in ws.iter_rows(max_row=ws.max_row, max_col=ws.max_column, values_only=False):
        for cell in row:
            if cell.value is not None:
                raw = cell.value
                if hasattr(raw, 'text'):
                    val = f"={raw.text}"
                else:
                    val = str(raw)
                if len(val) <= 70:
                    print(f"  [{cell.coordinate}] {val}")
                else:
                    print(f"\n[{cell.coordinate}] ({len(val)} chars)")
                    print(val)
PYEOF
Read Single Cell
CELL=C40
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/annual-review-${CELL}.txt
import openpyxl

wb = openpyxl.load_workbook('data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx')
ws = wb['SelfEval']

cell = '${CELL}'
print(f"[{cell}] ({len(str(ws[cell].value or ''))} chars)")
print(ws[cell].value)
PYEOF
Search Cells by Keyword
PATTERN="siem|sentinel|monad|cost|pipeline|qradar|etl|dcr|cribl|log source"
XLSX="data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx"
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/annual-review-search.txt
import openpyxl, re

wb = openpyxl.load_workbook('${XLSX}')
ws = wb['SelfEval']

pattern = re.compile(r'${PATTERN}', re.IGNORECASE)
cells = ['C40', 'G40', 'I49', 'I57', 'C66', 'C71', 'C102', 'C120', 'C207', 'C232']

for cell in cells:
    val = str(ws[cell].value or '')
    hits = [line.strip() for line in val.split('\n') if pattern.search(line)]
    if hits:
        print(f"\n{'=' * 70}")
        print(f"[{cell}]")
        print(f"{'=' * 70}")
        for h in hits:
            print(f"  {h}")
PYEOF
Edit a Cell
CELL=C66
XLSX="data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx"
TMP="/tmp/annual-review-${CELL}.txt"

# 1. Extract current value to temp file
/tmp/xlsx-env/bin/python3 << PYEOF
import openpyxl
wb = openpyxl.load_workbook('${XLSX}')
val = wb['SelfEval']['${CELL}'].value or ''
with open('${TMP}', 'w') as f:
    f.write(val)
print(f"BEFORE [${CELL}] ({len(val)} chars)")
PYEOF

# 2. Edit in your editor
${EDITOR:-nvim} "$TMP"

# 3. Write back and verify
/tmp/xlsx-env/bin/python3 << PYEOF | tee /tmp/annual-review-edit-${CELL}.txt
import openpyxl

with open('${TMP}') as f:
    new_val = f.read()

wb = openpyxl.load_workbook('${XLSX}')
ws = wb['SelfEval']
old_len = len(str(ws['${CELL}'].value or ''))
ws['${CELL}'] = new_val
wb.save('${XLSX}')

wb2 = openpyxl.load_workbook('${XLSX}')
final = wb2['SelfEval']['${CELL}'].value or ''
print(f"[${CELL}] {old_len} -> {len(final)} chars")
print(final)
PYEOF
Cell Map Reference
Cell Content Status

D32

Name

PASS

D36

Title

PASS

C40

Professional Contributions

✅ 5,158 chars

G40

Most Meaningful

✅ 879 chars

I49

Individual Goal Assessment

✅ 3,436 chars

G50

Individual Goal Y/N

✅ Yes

I57

Team Goal Assessment

✅ 850 chars

G58

Team Goal Y/N

✅ In Progress

C66

New Individual Goal

✅ 390 chars

C71

New Team Goal

✅ 231 chars

J91

Job Skills

✅ Exemplary

C102

Job Skills Comments

✅ 1,581 chars

J109

Outcomes

✅ Exemplary

C120

Outcomes Comments

✅ 1,656 chars

J127

Service

✅ Effective

C139

Service Comments

✅ 1,126 chars

J162

Collaboration

✅ Effective

J170

Empathy

✅ Effective

J178

Accountability

✅ Effective

J187

Growth

✅ Exemplary

J196

Responsibility

✅ Effective

C207

Values Comments

✅ 1,936 chars

C232

Leader Support

✅ 567 chars

C239

Additional Comments

— Blank (intentional)

Tested — WSL Arch to Windows Excel Workflow

Verified 2026-06-22. Steps 0-7 confirmed working from WSL Arch on work machine.

  • Pull, decrypt, v2 apply, verify, SIEM check — all passed

  • cp to /mnt/c/ fails if Excel holds the file lock — close first, then copy

  • invoke-item in PowerShell opens xlsx in Excel

WSL to Windows handoff (step 6 alternative on WSL)

Close the file in Excel before copying — cp fails with "Permission denied" if Excel holds the lock.

# Copy from WSL to Windows Downloads
cp data/d000/cursus/annual-review/chla/2026/my-annual-review-2026.xlsx /mnt/c/Users/erosado/Downloads/excel/
# Open in Excel from PowerShell
invoke-item .\Downloads\excel\my-annual-review-2026.xlsx

PowerShell — Work Computer Commands

PowerShell — Work Computer Commands

Open a file from Downloads (Invoke-Item = double-click equivalent)
Invoke-Item C:\Users\erosado\Downloads\excel\my-annual-review-2026.xlsx
Force-kill processes by name pattern
Get-Process | Where-Object { $_.ProcessName -like "*teams*" } | Stop-Process -Force
Get-Process | Where-Object { $_.ProcessName -like "*excel*" } | Stop-Process -Force
Get-Process | Where-Object { $_.ProcessName -like "*zoom*" } | Stop-Process -Force
Same thing, shorter — PowerShell aliases and parameter shortcuts
# gps = Get-Process, ? = Where-Object, spps = Stop-Process
gps | ? { $_.ProcessName -like "*teams*" } | spps -Force

# Even shorter — Get-Process accepts -Name with wildcards directly
Stop-Process -Name "*teams*" -Force
Stop-Process -Name "*excel*" -Force
Stop-Process -Name "*zoom*" -Force
The Unix equivalent (for comparison)
# Invoke-Item ≈ xdg-open
xdg-open ~/Downloads/excel/my-annual-review-2026.xlsx

# Get-Process | Where-Object | Stop-Process ≈ pkill
pkill -fi 'teams'
pkill -fi 'excel'
pkill -fi 'zoom'

# Or the pipeline version (closer to the PowerShell pattern)
ps aux | awk '/[t]eams/{print $2}' | xargs kill -9
Opening Files from Terminal — Invoke-Item / ii

Invoke-Item (alias ii) is PowerShell’s "double-click" — opens a file with its default Windows application. This is how you stay in the terminal and still use native Windows apps.

Open a file with its default handler
# Full cmdlet
Invoke-Item C:\Users\erosado\Downloads\excel\my-annual-review-2026.xlsx

# Alias — same thing, 2 characters
ii C:\Users\erosado\Downloads\excel\my-annual-review-2026.xlsx
Open WSL files in native Windows browser (not WSLg/Wayland)
# Access WSL filesystem via \\wsl$ UNC path — opens in Windows Chrome/Edge, not wslg
ii "\\wsl$\archlinux04\home\evanusmodestus\atelier\_bibliotheca\domus-captures\data\d001\projects\monad-pipeline\output\monad-pipeline.html"

This is the key pattern: \\wsl$\<distro>\ exposes the entire WSL filesystem to Windows. ii hands the path to Windows, which opens it with the native browser — full GPU rendering, no Wayland compositor overhead. The WSLg browser works but this is faster and uses your actual browser profile (bookmarks, extensions, sessions).

Open the current directory in Windows Explorer
ii .
Open a URL in default browser
ii "https://docs.domusdigitalis.dev"
From inside WSL — open files in Windows apps (without switching to PowerShell)
# wslview is the WSL equivalent of xdg-open — routes to Windows default handler
wslview output/monad-pipeline.html

# Or use explorer.exe directly from WSL bash
explorer.exe "$(wslpath -w output/monad-pipeline.html)"

# Open current directory in Windows Explorer from WSL
explorer.exe .
Productivity Patterns — Stay in the Terminal
Navigate fast — Tab completion + pushd/popd stack
# pushd remembers where you were, popd goes back
pushd C:\Users\erosado\Downloads\excel
ii my-annual-review-2026.xlsx
popd
# You're back where you started
Find and open — pipeline into ii
# Find the most recent .xlsx in Downloads and open it
Get-ChildItem ~\Downloads -Filter *.xlsx -Recurse |
  Sort-Object LastWriteTime -Descending |
  Select-Object -First 1 |
  ForEach-Object { ii $_.FullName }
Kill stubborn apps — pattern-based process termination
# Direct — no pipeline needed when you know the name
Stop-Process -Name "*teams*" -Force
Stop-Process -Name "*excel*" -Force
Stop-Process -Name "*zoom*" -Force

# Reusable function — add to $PROFILE
function Kill-App { param([string]$Name) Stop-Process -Name "*$Name*" -Force -ErrorAction SilentlyContinue }
Kill-App teams
Kill-App excel
Kill-App zoom
Check what’s eating RAM — sorted by working set
Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 10 Name, @{N='MB';E={[math]::Round($_.WorkingSet64/1MB)}} |
  Format-Table -AutoSize
Clipboard integration — pipe output to/from clipboard
# Copy command output to clipboard (paste into Teams/email)
Get-Process | Where-Object { $_.CPU -gt 100 } | Set-Clipboard

# Paste clipboard content into a variable
$data = Get-Clipboard
Quick file operations without Explorer
# Copy file
Copy-Item .\report.pdf ~\Desktop\

# Move and rename
Move-Item .\draft.docx ~\Documents\final-report.docx

# Recursive copy — entire directory
Copy-Item .\output\ ~\Desktop\monad-output\ -Recurse
Unix ↔ PowerShell Rosetta Stone
Task Unix (bash) PowerShell

Open file in default app

xdg-open file

ii file

Kill process by name

pkill -fi teams

Stop-Process -Name "teams" -Force

Find files by name

find . -name "*.xlsx"

Get-ChildItem -Recurse -Filter *.xlsx

Find files by content

grep -rl 'pattern' .

Select-String -Path *.adoc -Pattern 'pattern' | Select-Object Path -Unique

Sort by size

find . -type f -printf '%s %p\n' | sort -rn

Get-ChildItem -Recurse | Sort-Object Length -Descending

Disk usage

du -sh */

Get-ChildItem -Directory | ForEach-Object { $.FullName + ": " + ((Get-ChildItem $ -Recurse | Measure-Object Length -Sum).Sum / 1MB).ToString("F1") + " MB" }

Copy to clipboard

cmd | xclip -sel c

cmd | Set-Clipboard

View file with line numbers

cat -n file

Get-Content file | Select-Object @{N='Line';E={$.ReadCount}}, @{N='Text';E={$}}

Watch a log

tail -f /var/log/syslog

Get-Content log.txt -Wait -Tail 20

Count lines

wc -l file

(Get-Content file).Count

PowerShell pipes objects ($_.ProcessName). Unix pipes text (awk '{print $2}'). Same pattern — filter then act — but PowerShell knows the structure, Unix requires you to parse it. Trade-off: PowerShell is verbose but self-documenting. Unix is terse but requires you to know the column layout.

Ad-Hoc

Ad-Hoc Requests

Capture walk-ups, Teams pings, and unplanned work here.

*

Work (CHLA)

CHARGE TIME IN PEOPLESOFT - CRITICAL. Do this NOW before anything else.

Critical (P0)

Project Description Owner Status Due Blocker

Linux Research (Xianming Ding)

EAP-TLS for Linux workstations, dACL, UFW

Evan

BEHIND (72 days overdue)

02-24

Certificate "password required" - nmcli fix documented

iPSK Manager

Pre-shared key automation

Ben Castillo

BEHIND

 — 

DB replication issues

MSCHAPv2 Migration

Legacy auth deprecation — 6,227 devices, 5 waves. 6 batch SQL queries + 3-API endpoint profile script added (05-11). Report due.

Evan

25% — Report due, batch queries ready

05-30

Report to turn in

Research Segmentation

All endpoints to Untrusted VLAN

Evan

BLOCKED

 — 

CISO decision pending

Disaster Recovery

ISE DR scoping — dot1x closed mode = total blackout

Evan

Scoping

 — 

 — 

Mandiant Remediation

Copy 4/16 findings, Guest ACL lab, Q2 assessment

Evan

Active

 — 

 — 

SIEM QRadar → Sentinel

Full SIEM platform transition. Monad console error resolved 05-12. Secrets configured. Blocked on DCR creation (Rule ID + Stream Name). Azure private network policy unresolved.

Evan

Active — blocked on DCR

Q2 2026

Victor/Mauricio: create DCR, resolve Azure network policy

Abnormal Security

AI email platform — ESA cutover. CR assigned, CAB May 12 15:00. Implementation May 14 10:00.

Evan

Active — CAB today 15:00

05-14

Pre-CAB checklist: confirm Tyler, Jason, Sarah

High Priority (P1)

Project Description Owner Status Target

ISE 3.4 Migration

Upgrade from 3.2p9

Evan

Blocked — maintenance window needed

Q2 2026

Switch Upgrades

IOS-XE fleet update (C9300, 3560CX)

Evan

Pending

Q2 2026

Spikewell BYOD VPN

dACL SQL, AD group integration

Evan

Active

 — 

Strongline Gateway

MAC capture, Identity Group setup — 37 days aging

Evan

Active — David Rukiza assigned

 — 

Abnormal Security

AI email security platform research, ESA cutover timeline

Evan

Newly assigned

 — 

DMZ Migration

External services audit behind NetScaler

Evan

Audit phase

 — 

Firewall Audit (murus-portae)

EtherChannel query, prefilter, policy assignments

Evan

Scoping — ASA API creds needed

 — 

iPSK Manager HA

Server 2 config, TLS, SQL security audit

Evan

In progress

 — 

Sentinel KQL

Build proficiency, distinguish from team

Evan

Onboarding

 — 

VNC Blocking

Block and eliminate VNC protocol enterprise-wide

Evan

Active — Phase 0 (Discovery)

Mid-June 2026

Strategic (P2)

Project Description Owner Status

HHS Regulatory Compliance

New HHS security policies implementation

TBD

NOT STARTED

InfoSec Reporting Dashboard

PowerBI metrics for executives

TBD

NOT STARTED

EDR Migration (AMP → Defender)

Endpoint protection consolidation

TBD

NOT STARTED

Azure Legacy Migration

Modern landing zone

Team

In Progress

ChromeOS EAP-TLS

SCEP + Victor, Paul testing

Victor

In Progress

P0 — Critical / Blocking

Security & Compliance

  • ISE 3.2 Patch 10 upgrade — CVE-2026-20147 CVSS 9.9 / CVE-2026-20148. Propose maintenance window once patch confirmed on software.cisco.com.

  • ISE Advisory sa-ise-rce-traversal-8bYndVrZ — check Patch 10 availability

  • Mandiant Remediation — findings status tracked. Working session prep + defensive posture documented (comms-2026-04-24). Copy 4/16 updates into Excel at work. Guest ACL lockdown (WIR-M-01) pending lab validation. appendix-todos updated with MSCHAPv2 milestones.

  • Guest ACL update — guest redirect ACL work needed. Lab validate GUEST_CWA_REDIRECT_MAX_SECURITY in d000, then joint CR with NE. On today’s task list.

  • Disaster Recovery & Downtime Procedures — ISE top priority (dot1x closed mode = SPOF for network access)

    • ISE DR: Document failover sequence — PAN, MnT, PSN priority order

    • ISE DR: RADIUS dead-server detection on WLCs/switches — critical-auth VLAN fallback

    • ISE DR: Backup/restore procedures — scheduled config backups, tested restores

    • FTD/FMC DR: FMC loss = no policy management

    • Network DR: Core/distribution switch failure, STP reconvergence, HSRP failover

    • Document RTO/RPO per system

SIEM Migration (QRadar → Sentinel)

  • SIEM QRadar → Sentinel Migration — LEAD ROLE. 4 collection iterations (Apr 16, 17, 17-streamlined, 20-streamlined). Python chart pipeline built (qradar-charts.py). Migration XLSX generated. Verification pending. Comms sent Apr 23.

    • d001 artifacts: 8 JSON exports, 2 CSV inventories, migration XLSX, top5 source SVG/PNG, verification doc

    • Dependency: Monad pipeline for log source transition

    • Dependency: Sentinel KQL proficiency for query migration

  • Monad Pipeline Evaluation (origin: 2026-03-11) — lead role. Console error RESOLVED 05-12. 06-09: Architecture decision — rsyslog (CHLXSYSLOG01) as collection tier → Monad → Sentinel. ISE lab → rsyslog → Monad 6-step execution guide created with 10 API calls. ASA lab logs already flowing through rsyslog. DCR still needed — Victor + Mauricio.

  • Sentinel KQL — build proficiency, distinguish from team. Azure portal access acquired.

  • QRadar log source report — run AQL queries, fetch JSON, generate Python Excel

Active Deployments & Migrations

  • MSCHAPv2 Migration — 6-sheet Standard Report ready. Migration window 05-04 to 05-30 CLOSED. Confirm final report status and next steps with team. 6,227 MSCHAPv2 devices, 14,249 EAP-TLS/TEAP (70% migrated).

  • MSCHAPv2 weekly cadence — recurring Wednesday call established (first 04-22). Completed 2026-04-22.

  • MSCHAPv2 ownership matrix — sent in scoping email 4/24 with manager callouts (@Albert, @John). Completed 2026-04-24.

  • TCP Clocks deployment — Batch 1: 7 clocks validated (OUI 40:AC:8D). Batch 2 (06-09): 9 new MACs (OUI 40:AC:BD) added via ERS. 1 one-off reassigned. New switch deployed without RADIUS/AAA — clocks can’t authenticate. Switch onboarding template + 3 validation queries documented. ERS queries self-contained with ers function.

  • SRT Research VLAN — confirm roles with Tony Sun: Tony implementor, Evan tester. CAB approved 04-21.

  • Downtime Computers enforcement — draft ISE AuthZ rule: medigate_724 + Wireless = DenyAccess. Separate CR. d001: DC queries, audit CSVs (v1-v3), wireless violations report delivered 04-21.

  • Enterprise Linux 802.1X — standardize Shahab/Ding deployment (CISO priority). Overdue since 02-24. Blocked by nmcli cert fix. 06-15: Assembler build fixed (26 errors → 0). Segmentation document extension to 06-16. d001 open linux-research to work on it.

  • Abnormal Security — CR-2026-05-07. Implemented 05-13. 06-09 update: Full policy review — 20-section EOP validation commands rebuilt, Hoxhunt SCL-1 investigation (intentional bypass confirmed), sclizer junk folder triage (~800 emails), Outlook reactions audit added, Connect-ExchangeOnline msalruntime fix documented. ESA migration expansion in progress — priority to move off ESA to full environment.

    • Team: Cox/William, Landeros/Jason, Rosado/Evan, Naranjo/Mauricio, Sandoval/Carlos

  • ASA VPN: Okta RADIUS → Entra SAML — (NEW 06-09) 5-phase migration plan built. ASA baseline captured (2 tunnel groups: CHLA_CORPORATE_USERS, CHLA_BYOD_USERS). 6 ISE policy screenshots. Tony Sun (ASA), Justin Halbmann (Entra/Okta), Evan (ISE). VPN cert expires 07-28. PDF deliverable ready. Share with team this week.

Tube System Upgrade (NEW — 06-01)

  • Tube System Upgrade — iTrack 3528165. 15x 10" TS stations need MAC addresses added to ISE identity group IoT_Onboard. MACs received from vendor (C8:1A:FE:20:xx:xx series). Station list spans ICU (CTICU, PICU, BMT, NICU, NICCU), ED, Surgery, Trauma, Pharmacy. Vendor contact: John Genest. Rationale: manufacturer no longer supports current system; failure risks delayed/missed patient care.

BMS Controller Segmentation (MIGRATED — 06-09)

  • BMS Controller Segmentation — Full migration from Principia LaTeX to d001. 12 partials, 5 Mermaid diagrams, 4 legacy PDFs, ISE screenshots. d001 open bms-controller. Completed 2026-06-09.

BMS Device Inventory (NEW — 04-24)

  • BMS Device Inventory — 72 devices discovered across 37 switches (04-24). Profile-driven architecture (Claroty/Medigate). 16 queries built. Phase 0 complete. Next: cross-reference with Visio diagrams, classify by function, begin D2 diagrams. Cleanup: delete 4 orphaned test groups, migrate 4 retire-dACL devices, investigate 3 null-profile devices.

VNC Blocking (NEW — 05-11)

  • VNC Blocking — block and eliminate VNC enterprise-wide. Due mid-June 2026. Phase 0: discovery. January AQL query baseline to incorporate. Cross-reference BMS inventory for VNC-capable devices.

Investigations & Audits

  • Murus Portae (WAF) — Phase 0 discovery in progress. FMC cert expired. d001: DMZ NetScaler WAF investigation, zone map, architecture D2 diagrams (v1+v2 SVGs), FMC REST API reference guide, ops script. FMC API returning zero ACP rules — under investigation.

  • Firewall audit — FMC discovery inventory done (d001: fmc-discovery-2026-04-16). EtherChannel query, prefilter, policy assignments pending.

  • IoT Dr. Kim devices — RECURRING. All 4 MACs validated in IoT_iPSK_VLAN1620_Misc (04-24). v2 validation queries built with 7 deep analysis queries (group flapping, credential leakage, profile drift, NAS tracking, remediation timeline, deny audit, OUI scan). Revalidate — confirm no flapping since 04-24.

  • IoT device validation queries — v2 created with partials architecture, 16 queries across ERS/MnT/DataConnect/FMC. Completed 2026-04-24.

Stale Blockers (carried via carryover tracker)

  • k3s NAT verification — rule 170, 10.42.0.0/16 pod network (origin: 2026-03-09). 92 days. Blocks Wazuh indexer recovery → blocks SIEM visibility. Decide: test or defer to Q3.

  • Strongline Gateway VLAN fix — 8 devices wrong identity group (origin: 2026-03-16). 85 days. David Rukiza assigned — follow up on status.

Administrative

  • PeopleSoft — track time for current week

  • iTrack tickets — close open tickets

  • KQL library — build initial queries in codex + d001

  • Linux Research project — finalize and review

  • Tax filing 2025 (MFJ) — see encrypted case file in data/d000/personal/ for details and action items

P1 — Important

  • MSCHAPv2 action-item tracker — owner/status/next-steps per workstream

  • ISE admin MFA enforcement — recommendation tied to advisory (interim control pending Patch 10)

  • DMZ Migration — external services audit behind NetScaler. Linked to Murus Portae investigation.

  • Vocera/Wyse iTrack RCA — complete root cause report

  • GCC ISE Support — 3/4 nodes restored, PSN-04 deferred

  • Wazuh indexer recovery — blocked by k3s NAT (origin: 2026-03-09)

  • Vocera EAP-TLS Supplicant Fix (origin: 2026-03-12)

  • iPSK Manager HA — blocked by DB replication (Ben Castillo)

  • ISE 3.4 Migration — depends on Patch 10 completion first

  • Git history scrub — murus-portae-output.md + ise-analytics CSVs

  • Encrypt prep-cmds-2026-04-15.adoc — plaintext committed to git

  • ISE MnT Messaging Service — enable UDP syslog delivery (maintenance window needed)

Infrastructure (Personal)

  • Borg backups — test and validate on ALL systems (Razer, P16g, vault-01, bind-01, kvm-01, kvm-02)

  • Borg — verify backup script paths updated from dotfiles-optimus to dots-quantum

  • Borg — create initial archive for ThinkPad P16g if none exists

  • Libvirt VLAN hook debug on both KVMs

  • Te1/0/2 cable replacement and re-test

  • Vault Raft cluster — verify vault-01 rejoined

  • Fix EAP-TLS keyring/secrets issue on Razer workstation

Completed (confirmed — do not delete, archive only)

  • CR-2026-04-15 SRT Research VLAN — submitted to iTrack. Completed 2026-04-15.

  • CAB presentation 4/21 — SRT Research VLAN 233 → CHLA-Research. APPROVED. Completed 2026-04-21.

  • Downtime Computers wireless audit — 45 computers, 16 violating, v3 report delivered. Completed 2026-04-21.

  • Git identity fix — dots-quantum/git/.gitconfig email corrected. Completed 2026-04-21.

  • MSCHAPv2 10:30 meeting — next steps + ACL coordination. Completed 2026-04-17.

Service Requests (SR)

SR# Request Requestor Opened Status

3508542

Zoll cards connection issue

STALE — verify in iTrack

3508524

Disable dot1x on (2) network ports - 5th floor 3250 Wilshire (PXE-boot imaging issues)

STALE — verify in iTrack (issues persisted after disable)

3528165

Tube System Upgrade — 15 stations, MAC addresses for ISE IoT_Onboard identity group

Genest, John (vendor contact)

2026-06-01

NEW — MACs received, need ISE onboarding

Incidents (INC)

INC# Priority Description Opened SLA Status

1911859

Strongline Gateways in Miscellaneous Subnet

STALE — verify in iTrack (related to carryover P0)

Change Requests - Emergency (ECAB)

CR# Description Opened Scheduled Status

No emergency changes

Change Requests - Normal

CR# Description Opened Scheduled Status

No normal changes

Change Requests - Scheduled/Standard

CR# Description Opened Window Status

No scheduled changes

Change Requests - Root Cause / Post-Incident

CR# Description Related INC Opened Status

100451

Vocera Phones and Wyse devices went off network

STALE — verify in iTrack


Session Accomplishments (Claude Code)

Day-specific accomplishments here.


Personal

In Progress

Project Description Status Notes

k3s Platform

Production k3s cluster on kvm-01

Active

Prometheus, Grafana, Wazuh deployed

Wazuh Archives

Enable archives indexing in Filebeat

Active

PVC fix pending

kvm-02 Hardware

Supermicro B deployment

Active

Hardware ready, RAM upgrade done

Planned

Project Description Target Blocked By

Vault HA (3-node)

vault-02, vault-03 on kvm-02

Q2 2026 (slipped from Q1)

kvm-02 deployment

k3s HA (3-node)

Control plane HA

Q2 2026 (slipped from Q1)

kvm-02 deployment

ArgoCD GitOps

k3s GitOps deployment

After k3s stable

 — 

MinIO S3

Object storage for k3s

After ArgoCD

 — 

Domus Inventory

Personal asset management (YAML + CLI + AsciiDoc)

Q2 2026

Schema approved

Active — Infrastructure

Task Details Priority Status Due

Wazuh agent deployment

Deploy agents to all infrastructure hosts

P2

Pending

After archives fix

k3s Platform

Production k3s cluster on kvm-01

P1

In Progress

 — 

Wazuh Archives

Enable archives indexing in Filebeat, PVC fix

P1

In Progress

 — 

kvm-02 Hardware

Supermicro B deployment, RAM upgrade done

P1

In Progress

 — 


Active — Security & Encryption

Task Details Priority Status Due

Configure 4th YubiKey

SSH FIDO2 keys

P1

TODO

 — 

Cold storage M-DISC backup

age-encrypted archives

P1

TODO

After YubiKey setup


Active — Development & Tools

Task Details Priority Status Due

netapi Commercialization

Go CLI rewrite with Cobra-style argument discovery, package for distribution

P0

Active

 — 

Ollama API Service

FastAPI (17 endpoints), productize — config audit, doc tools, runbook gen

P0

Active

 — 

Shell functions (fe, fec, fef)

File hunting helpers

P3

TODO

 — 


Active — Documentation

Task Details Priority Status Due

D2 Catppuccin Mocha styling

domus-* spoke repos (177 files total)

P3

In Progress

 — 


Active — Financial

Task Details Priority Status Due

Amazon order history import

Download CSV from Privacy Central → parse with awk → populate subscriptions tracker

P1

Waiting

Pending Amazon data export (requested 2026-04-04)


Active — Education

Task Details Priority Status Due

No active education tasks — see education trackers


Active — Personal & Life Admin

Task Details Priority Status Due

ThinkPad T16g Setup

Arch install, stow dotfiles, Ollama stack, netapi dev env

P0

Pending

 — 

P50 Arch to Ubuntu migration

CR-2026-03-12

P2

In Progress

 — 

X1 Carbon Ubuntu installs

2 laptops, LUKS encryption

P2

In Progress

 — 

P50 Steam Test

Test Flatpak Steam + apt cleanup of broken i386 packages

P3

Pending

 — 

Documentation Sites

Notes

Day-specific personal notes here.


Education

Claude Code Mastery

Resource Details Progress Status

Claude Code Full Course (4 hrs)

Nick Saraev - YouTube comprehensive course

26:49 / 4:00:00

IN PROGRESS

Claude Code Certification

Anthropic official certification (newly released)

Not started

GOAL

Skills Mastery (Critical)

Certification Deadlines

  • CISSP - July 12, 2026 (10-week plan active — Week 1)

  • RHCSA 9 - Q3 2026 (after CISSP)

  • LPIC-1 - Renewal required (blocks LPIC-2)

Spanish C1 Certification Goals

Certification Provider Target Status Strategy

SIELE C1

Instituto Cervantes / UNAM / Salamanca

Q2 2026

ACTIVE

Computer-based, faster results - take FIRST

DELE C1

Instituto Cervantes

Q3/Q4 2026

PLANNED

After SIELE success, harder exam

DELE C2

Instituto Cervantes

2027

FUTURE

Mastery level - requires extensive immersion

SIELE is computer-adaptive, results in 3 weeks. DELE is paper-based, results in 3-4 months. Do SIELE first to validate readiness.

Don Quijote Writing Practice - DELE C1/C2 Initiative

Method:

  1. Read chapter in original Spanish

  2. Write personal analysis/understanding en espanol

  3. AI review for grammar, vocabulary, register

  4. Build comprehensive understanding of literary elements

Today’s Study

  • Focus: CISSP (41 days to July 12 exam — schedule exam today 06-01), MSCHAPv2 migration wrap-up

  • Secondary: RHCSA curriculum, Spanish SIELE C1

  • CISSP — Security & Risk Management (continuing). Schedule exam this afternoon.

  • RHCSA — continue curriculum phase

  • Spanish — Don Quijote reading + analysis (DTLA study day)

  • MSCHAPv2 — migration window closed 05-30, review final report

Regex Training (CRITICAL)

  • Status: 52 days carried over (since 2026-03-16)

  • Priority: After PeopleSoft, before Quijote

  • Session: Character classes, word boundaries


Infrastructure

Documentation Sites

Site URL Status Actions Needed

Domus Digitalis

docs.domusdigitalis.dev

Active

Validate, harden, improve

Architectus

docs.architectus.dev

Active

Public portfolio site - maintain

HA Deployment Status

System Description Status Notes

VyOS HA

vyos-01 (kvm-01) + vyos-02 (kvm-02) with VRRP VIP

✅ COMPLETE

2026-03-07 - pfSense decommissioned

BIND DNS HA

bind-01 (kvm-01) + bind-02 (kvm-02) with AXFR

✅ COMPLETE

Zone transfer operational

Vault HA

Raft cluster (vault-01/02/03)

✅ COMPLETE

Integrated with PKI

Keycloak Rebuild

keycloak-01 corrupted, rebuild from scratch

🔄 NEXT

Priority P3 - SSO broken

FreeIPA HA

ipa-02 replica planned

📋 PLANNED

Linux auth redundancy

AD DC HA

home-dc02 replication

📋 PLANNED

Windows auth redundancy

iPSK Manager HA

ipsk-mgr-02 with MySQL replication

📋 PLANNED

PSK portal redundancy

ISE HA

PAN HA (ise-01 reconfigure)

⏳ DEFERRED

Wait until ise-02 stable

ISE 3.5 Migration

Upgrade path: 3.2p9 → 3.4 (P1) → 3.5 (target)

📋 PLANNED

After 3.4 Migration completes (Q2 2026)

Single Points of Failure (CRITICAL)

These systems have NO redundancy - outage impacts production.
System Impact if Down Mitigation

ISE (ise-02)

All 802.1X stops - wired and wireless auth fails

ise-01 reconfiguration deferred until ise-02 stable

Keycloak (keycloak-01)

SAML/OIDC SSO broken (ISE admin, Grafana, etc.)

NEXT PRIORITY - Rebuild runbook

FreeIPA (ipa-01)

Linux auth, sudo rules, HBAC fails

ipa-02 replica planned

AD DC (home-dc01)

Windows auth, Kerberos, GPO fails

home-dc02 replica planned

iPSK Manager

Self-service PSK portal unavailable

ipsk-mgr-02 with MySQL replication planned

Validation Tasks

Task Details Status

docs.domusdigitalis.dev validation

Test all cross-references, search, rendering

TODO

docs.domusdigitalis.dev hardening

HTTPS, CSP headers, security review

TODO

docs.architectus.dev validation

Public site content review

TODO

Hub-spoke sync verification

All components building correctly

Ongoing


Quick Commands

Git & GitHub CLI

create GitHub repo from existing local repo
gh repo create <name> --private --source . --remote origin --push
clone a forked repo into a specific directory
gh repo clone EvanusModestus/PowerShell ~/atelier/_projects/work/PowerShell
gh repo clone defaults to SSH. If key is passphrase-protected, load agent first: eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519_github
cross-repo commit search — all domus repos on a specific date
for repo in ~/atelier/_bibliotheca/domus-*/ ~/atelier/_projects/personal/domus-*/; do
  [ -d "$repo/.git" ] || continue
  name=$(basename "$repo")
  git -C "$repo" log --since="2026-04-06" --until="2026-04-07" --format="%h %aI %s" 2>/dev/null |
    awk -v r="$name" '{print r, $0}'
done
commit history touching only today’s modified files
git log --oneline -- $(find . -name "*.adoc" -type f -newermt "$(date +%F)")
unstage a file without losing changes
git restore --staged data/d001/api/ise-dataconnect/output/output-2026-04-24

Safe — removes from staging area only. Working tree is untouched. Use when you accidentally git add a plaintext or output file.

gh CLI — repo discovery and filtering

list repos by name pattern (domus/antora ecosystem)
gh repo list --limit 100 --json name,description \
  | jq -r '.[] | select(.name | test("domus|antora|asciidoc"; "i")) | "\(.name)\t\(.description)"'
top 20 most recently updated repos
gh repo list --limit 100 --json name,description,updatedAt \
  | jq -r 'sort_by(.updatedAt) | reverse | .[:20] | .[] | "\(.updatedAt[:10])\t\(.name)\t\(.description)"'
top 10 repos by disk usage
gh repo list --limit 100 --json name,diskUsage \
  | jq -r '.[] | "\(.diskUsage)\t\(.name)"' | sort -rn | head -10
clone a repo that’s not local yet
gh repo clone EvanusModestus/<repo-name> ~/atelier/_bibliotheca/<repo-name>

find & grep

files modified since midnight today (precise — not "last 24 hours")
find . -name "*.adoc" -type f -newermt "$(date +%F)" | sort
-mtime 0 means "last 24 hours", not "today". -newermt "$(date +%F)" compares against midnight — exact.
case-insensitive file search
find . -iname "*mschap*" -type f | sort
multiple name patterns with -o
find . -type f \( -iname "*ise*" -o -iname "*mschap*" \) | sort
same thing, single regex — fewer parens, extensible
find . -type f -iregex '.*\(ise\|mschap\).*'
exclude directories
find . -type f -iname "*meeting*" \
  -not -path "*/node_modules/*" \
  -not -path "*/.git/*" \
  -not -path "*/build/*"
recent drafts by modification time (newest first)
find .drafts -type f -printf '%T@ %Tc %p\n' | sort -rn | awk '{$1="";print}' | head -3
grep — know what you’re counting
grep -rl "pattern" . --include="*.adoc"         # file count (which files)
grep -rn "pattern" . --include="*.adoc"         # line matches (every occurrence)
grep -rc "pattern" . --include="*.adoc" | grep -v ':0$'  # match count per file
search with context — avoid opening the file
grep -rn -E 'git init|gh repo create' docs/ --include='*.adoc' -B2 -A2

Search codex by content — which files contain a command?

find all PowerShell files that use a specific cmdlet
find docs/modules/ROOT/examples/codex/powershell -type f -name "*.adoc" \
  -exec grep -l 'Get-Process\|Start-Process\|pipeline\|Where-Object' {} \;

Pattern: find -exec grep -l returns only filenames with matches — like grep -rl but with find’s `-type f -name filtering. Use \| for OR in grep basic regex. Swap the pattern for any cmdlet or keyword to locate coverage across the codex.

inventory a codex tool directory — count files per tier
find docs/modules/ROOT -name "powershell" -type d \
  -exec sh -c 'echo "$1: $(find "$1" -type f | wc -l) files"' _ {} \;
find orphaned examples (not included by any page)
for f in $(find docs/modules/ROOT/examples/codex/powershell -name "*.adoc" -type f); do
  base=$(basename "$f")
  dir_parent=$(basename $(dirname "$f"))
  grep -rq "$dir_parent/$base" docs/modules/ROOT/pages/codex/powershell/ \
    docs/modules/ROOT/examples/codex/powershell/*.adoc 2>/dev/null \
    || echo "ORPHAN: $f"
done

find → grep → open in nvim

find by path + content, open result in nvim
nvim $(find -path '*oauth*' -name '*.adoc' -type f \
  -exec grep -l 'timeout\|expire\|reconfig\|token' {} \;)

Command substitution $(…​) feeds all matches as arguments to nvim — opens every hit as a buffer. :bn/:bp to cycle, :ls to list. One file? Opens directly. Five files? All loaded, ready to navigate.

find by content across entire tree, open in nvim
nvim $(find docs/modules/ROOT -name '*.adoc' -type f \
  -exec grep -l 'token.*expire\|oauth.*refresh' {} \;)
open one at a time (sequential — -exec nvim per match)
find -path '*oauth*' -name '*.adoc' -type f \
  -exec grep -l 'timeout\|expire' {} \; \
  -exec nvim {} \;
Trailing \| in grep patterns matches empty string — every file matches. Always end with a term, not a pipe: 'timeout\|expire\|token' not 'timeout\|expire\|token\|'.

Trace Antora partial inclusion chains

who includes this partial? (one level up)
grep -rl 'commands/shell' docs/modules/ROOT/partials/
count all pages that include a partial
grep -rl 'quick-commands' docs/modules/ROOT | wc -l
full chain: partial → assembler → every page that uses it
file="commands/shell"
grep -rl "$file" docs/modules/ROOT/partials/ | while read f; do
  parent=$(basename "$f" .adoc)
  echo "$file -> $parent"
  grep -rl "$parent" docs/modules/ROOT/pages/ | while read p; do
    echo "  -> $(basename "$p")"
  done
done

Pattern: grep -rl finds which files contain the string. Chain two passes — first finds the assembler partial, second finds every page that includes it. Works for any partial in the Antora include hierarchy.

Multi-pattern file search — worklog partial discovery

brute force — one find per partial name
find docs/modules/ROOT -name "*urgent.adoc*" -type f
find docs/modules/ROOT -name "*morning.adoc*" -type f
consolidated — single find with regex (production approach)
find docs/modules/ROOT -type f -regextype posix-extended \
  -regex '.*(urgent|morning|work-chla|personal|education|infrastructure|quick-commands|related)\.adoc' \
  | sort

Pattern: -regextype posix-extended enables | alternation without escaping. One process, one sort — versus 8 separate finds. The sort deduplicates visually and groups by path.

pipeline alternative — find piped to grep
find docs/modules/ROOT -type f -name "*.adoc" \
  | grep -E 'urgent|morning|work-chla|personal|education|infrastructure|quick-commands|related'

Trade-off: the pipeline version is more readable but spawns two processes. The regex version is a single find — faster on large trees, same result.

Cross-repo literary term search — bibliotheca-wide discovery

When searching for a term across the entire _bibliotheca (multiple repos, mixed file types), these patterns escalate from narrow to broad.

1. Single repo — count matches per file
grep -rn --include='*.adoc' -c 'sanchuelo' . | grep -v ':0$'
2. Cross-repo — filenames only (all bibliotheca)
grep -rl --include='*.adoc' -i 'sanchuelo' ~/atelier/_bibliotheca/ | sort
3. Cross-repo with context — see the line in situ
grep -rn --include='*.adoc' -i -B1 -A1 'sanchuelo' ~/atelier/_bibliotheca/domus-captures/
4. Multi-filetype — .adoc + .txt (catches source texts)
grep -rl -i 'sanchuelo' ~/atelier/_bibliotheca/ --include='*.txt' --include='*.adoc' | sort
5. Null-safe find + xargs — handles spaces in paths
find ~/atelier/_bibliotheca/ -type f \( -name '*.adoc' -o -name '*.txt' \) -print0 \
  | xargs -0 grep -li 'sanchuelo' | sort
6. Open all hits directly in nvim
grep -rl -i 'sanchuelo' ~/atelier/_bibliotheca/ --include='*.adoc' --include='*.txt' | xargs nvim

Pattern escalation: #1 confirms the term exists and where. #2 expands to all repos. #3 shows context without opening files. #4 adds plain text sources (Quijote .txt originals). #5 is the safe version for automation. #6 opens everything for editing.

Trade-off: grep -r --include is faster for known file types. find | xargs grep is safer for paths with spaces and more extensible (add -name '*.md' etc.). For literary searches across the bibliotheca, #4 or #5 is usually the right starting point — the source texts are .txt, not .adoc.

Search daily notes — find commands, builds, and patterns across worklogs

Source: 2026-06-22 — searching for build-antora-page invocations and theme usage across partials

which daily note partials mention a command?
# Find files — filenames only
grep -rl 'build-antora-page' docs/modules/ROOT/partials/worklog/
see the match in context without opening the file
# 3 lines of context around each hit
grep -rn -C3 'build-antora-page' docs/modules/ROOT/partials/worklog/
search for any theme flag across all partials (PCRE alternation)
grep -rPn 'theme\s+(light-cyan|catppuccin|mocha)' docs/modules/ROOT/partials/
find + grep — search all daily note partials for build/export patterns
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec grep -l 'build-antora-page\|build-adoc\|--theme\|pdf' {} \;
find + awk — show filename:matching_line for every hit (no -r needed)
find docs/modules/ROOT/partials/worklog/daily-notes -name '*.adoc' \
  -exec awk '/build-antora-page|build-adoc|--theme/{print FILENAME": "$0}' {} \;
awk state machine — extract full [source,bash] blocks containing a command
find docs/modules/ROOT/partials -name '*.adoc' -exec \
  awk '/^\[source,bash\]/{block=1; buf=""} \
       block{buf=buf"\n"$0} \
       /^----$/ && block>1{if(buf~/build-antora-page/) print FILENAME":"buf; block=0} \
       block{block++}' {} \;

The awk state machine: [source,bash] sets block=1 and starts buffering. Each line appends to buf. When the closing ---- arrives (and block>1 to skip the opening fence), check if the buffer contains the target command. If yes, print filename + entire block. Reset. One pass, arbitrary block size, no temp files.

sed address range — print only lines between code fences matching a pattern
find docs/modules/ROOT/partials/worklog -name '*.adoc' -exec \
  sed -n '/\[source,bash\]/,/^----$/{/build-antora-page/p}' {} +
search data/ too — standalone adoc outside Antora
grep -rn 'build-antora-page\|build-adoc' data/ --include='*.adoc'
every invocation sorted by path (chronological by date directory)
grep -rn 'build-antora-page' docs/modules/ROOT/partials/worklog/daily-notes/ \
  | awk -F: '{print $1, $3}' | sort

Pattern escalation: grep -rl → "where is it". grep -rn -C3 → "what’s around it". find -exec awk → "extract the structured block". sed address ranges → "print between delimiters". Each tool has a different affordance — grep finds, awk extracts structure, sed filters ranges, find -exec scales across trees.

Email thread analysis — extract people, dates, commitments, silence

who’s in the thread (@ mentions + From headers)
grep -P '(@\w+|^From:.*<)' comms.adoc
timeline — every date with context
grep -nP '\d{1,2}/\d{1,2}/\d{2,4}|20\d{2}-\d{2}-\d{2}' comms.adoc
commitments — who promised what
grep -niP '(I can |I will |I.ll |we will |we.ll )' comms.adoc
open questions and unknowns
grep -niP '(\?|need to confirm|need to validate|TBD|pending)' comms.adoc

comm — set difference (who hasn’t replied)

# All recipients
grep -oP '<\K[^>]+' comms.adoc | sort -u > /tmp/all-recipients

# All senders
grep -P '^From:' comms.adoc | grep -oP '<\K[^>]+' | sort -u > /tmp/replied

# Who's silent — follow-up targets
comm -23 /tmp/all-recipients /tmp/replied

comm -23 outputs lines only in file 1 (recipients not in senders). Requires sorted input. grep -oP '<\K[^>]+' uses PCRE lookbehind — match < but don’t include it, capture until >.

Sort find results by modification time (newest first)

find discovers files but has no sort. Chain -printf with sort to order by mtime.

awk '{print $2}' truncates filenames with spaces — Familia Romana_ Lingva…​ becomes Familia. Always use the null-safe or sub() variants below for real data.
epoch sort — space-safe (production version)
# Sort by mtime, strip epoch prefix — handles spaces in filenames
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T@ %p\n' | sort -rn | awk '{sub(/^[^ ]+ /,""); print}'

sub(/[ ]+ /,"") removes everything up to and including the first space (the epoch). {print $2} would split on every space — fatal for Familia Romana_ Lingva Latina.

human-readable timestamps alongside
# ISO 8601 timestamps — readable and lexicographically sortable
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T+ %p\n' | sort -r | head -20

%T+ renders YYYY-MM-DD+HH:MM:SS — no epoch math needed, still sorts correctly as text.

null-safe — the bulletproof version
# Null-delimited: survives any filename (newlines, quotes, unicode)
find ~/Downloads -maxdepth 1 -name '*.pdf' -printf '%T@\t%p\0' | sort -zrn | awk -v RS='\0' -F'\t' '{print $2}'

-printf '%T@\t%p\0' — tab separates epoch from path, null terminates. sort -z sorts null-delimited records. awk -v RS='\0' -F'\t' reads null-terminated, splits on tab — $2 is now the full path regardless of spaces.

stat fallback — portable (BSD/macOS)
# GNU stat equivalent — works where -printf is unavailable
find ~/Downloads -maxdepth 1 -name '*latin*' -exec stat --format='%Y %n' {} + | sort -rn | awk '{sub(/^[^ ]+ /,""); print}'

-exec …​ {} + batches all files into one stat call (faster than \;). On macOS, use stat -f '%m %N' instead of --format='%Y %n'.

File intelligence — size, type, duplicates, age

Beyond finding files — interrogating them.

top 10 largest files in a directory tree
# Size in bytes (-printf %s), human-readable via numfmt
find ~/Downloads -type f -printf '%s\t%p\n' | sort -rn | head -10 | numfmt --to=iec --field=1

numfmt --to=iec --field=1 converts the first field from bytes to K/M/G. sort -rn on raw bytes is exact — ls -lhS rounds and sometimes mis-sorts.

find duplicates by size (fast pre-filter before checksumming)
# Files sharing a byte count — likely duplicates (confirm with md5sum)
find ~/Downloads -type f -printf '%s %p\n' | awk '{seen[$1]++; files[$1]=files[$1] "\n  " $0} END {for (s in seen) if (seen[s]>1) print files[s]}'
find duplicates by content — definitive
# md5sum only files with duplicate sizes (two-pass: fast then precise)
find ~/Downloads -type f -printf '%s\n' | sort | uniq -d | while read -r size; do
  find ~/Downloads -type f -size "${size}c" -exec md5sum {} +
done | sort | uniq -w32 -D

Two-pass: first find duplicate sizes (cheap), then md5sum only those (expensive). uniq -w32 -D compares first 32 chars (the hash) and prints all duplicates.

file type census — what’s actually in this directory?
# Count files by MIME type (not extension — extensions lie)
find ~/Downloads -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn

file --mime-type -b reports actual content type. -b suppresses filename. A .pdf that’s really text/html is a failed download.

stale files — untouched for 30+ days
# Files not accessed in 30 days — candidates for cleanup
find ~/Downloads -maxdepth 1 -type f -atime +30 -printf '%A+ %s\t%p\n' | sort | numfmt --to=iec --field=2

-atime 30` = access time older than 30 days. `-printf '%A' shows last access. Useful for Downloads cleanup without deleting something you just renamed.

disk usage by subdirectory — sorted
# Which subdirectories consume the most space?
find . -maxdepth 1 -type d -exec du -sh {} + 2>/dev/null | sort -rh | head -20

Batch operations — rename, move, transform

rename all files — strip spaces, lowercase, normalize unicode
# Dry run — show what would change (remove echo to execute)
find ~/Downloads -maxdepth 1 -type f -name '* *' -print0 | while IFS= read -r -d '' f; do
  dir=$(dirname "$f")
  base=$(basename "$f" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
  echo mv "$f" "$dir/$base"
done

IFS= read -r -d '' — the holy trinity for null-safe filename reading. IFS= prevents whitespace trimming. -r prevents backslash interpretation. -d '' reads until null.

move files by extension into categorized subdirectories
# Sort Downloads chaos into folders by type
find ~/Downloads -maxdepth 1 -type f -print0 | while IFS= read -r -d '' f; do
  ext="${f##*.}"
  case "$ext" in
    pdf|epub)    dest="books" ;;
    jpg|png|svg) dest="images" ;;
    sh|py|rb)    dest="scripts" ;;
    *)           dest="other" ;;
  esac
  mkdir -p ~/Downloads/"$dest"
  echo mv "$f" ~/Downloads/"$dest"/
done

${f##.} — parameter expansion: strip longest match of . from front, leaving only the extension. No basename or awk needed.

batch convert epub → asciidoc (like your Cicero fetch script)
# Convert all epubs in a directory to asciidoc via pandoc
find . -name '*.epub' -type f -exec sh -c '
  for epub; do
    adoc="${epub%.epub}.adoc"
    pandoc -f epub -t asciidoc "$epub" -o "$adoc" \
      && printf "  → %s (%s lines)\n" "$adoc" "$(wc -l < "$adoc")" \
      || printf "  ✗ failed: %s\n" "$epub"
  done
' _ {} +

-exec sh -c '…​' _ {} + — batch mode. _ fills $0 (script name, discarded). All matched files become $1, $2, …​ iterated by for epub. One sh invocation, not one per file.

xargs power patterns

parallel processing — 4 cores
# Checksum all PDFs in parallel (4 processes)
find ~/Downloads -name '*.pdf' -print0 | xargs -0 -P4 md5sum

-P4 runs 4 md5sum processes simultaneously. -print0 | xargs -0 is the null-safe pipeline — no filename can break it.

batched execution — two arguments at a time
# Compare files pairwise with diff
find . -name '*.adoc' -print0 | xargs -0 -n2 diff --brief

-n2 feeds two arguments per invocation. Useful for pairwise comparisons, copy operations (-n2 with cp), or any command taking exactly two args.

placeholder — insert filename at specific position
# Backup every config file: cp <file> <file>.bak
find /etc -maxdepth 1 -name '*.conf' -print0 | xargs -0 -I{} cp {} {}.bak

-I{} replaces {} with each filename. Slower than + batching (one cp per file) but necessary when the filename must appear in a specific position.

Process substitution — diff without temp files

compare two directory listings
# What files exist in study-A but not study-B?
diff <(find data/d000/education/ciceron-study -type f -name '*.adoc' | sort) \
     <(find data/d000/education/latin-study -type f -name '*.adoc' | sort)

<(cmd) creates a file descriptor from command output. diff sees two "files" — no temp files created, no cleanup needed.

compare file counts across directories
# Side-by-side: file type census of two directories
paste <(find dir1 -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn) \
      <(find dir2 -type f -exec file --mime-type -b {} + | sort | uniq -c | sort -rn)

awk, sed, jq

awk — field extraction

print second field (whitespace-delimited)
awk '{print $2}' file.txt
custom delimiter — colon-separated (like /etc/passwd)
awk -F: '{print $1, $3}' /etc/passwd
extract JSON code blocks from AsciiDoc
awk '/\[source,json\]/{getline; if ($0 ~ /^----/) {p=1; next}} p && /^----/{p=0; next} p' file.adoc
field extraction with printf formatting
awk '{printf "%-30s %s\n", $1, $2}' file.txt

sed — stream editing

in-place replacement with verify-before/after
# Before
awk 'NR==73' /etc/ssh/sshd_config
# Change
sed -i '73s/#GSSAPIAuthentication no/GSSAPIAuthentication yes/' /etc/ssh/sshd_config
# After
awk 'NR==73' /etc/ssh/sshd_config
extract line range
sed -n '10,20p' file.txt

sed — line-targeted replacement (verify-before / change / verify-after)

the full pattern: locate → validate → change → verify
# 1. LOCATE: find the line number
grep -n 'adoc-pdf' zsh/.zshrc

# 2. VALIDATE: read the exact line before changing
awk 'NR==1760' zsh/.zshrc

# 3. CHANGE: target by line number — only hits that line
sed -i '1760s/alias adoc-pdf=/alias build-adoc=/' zsh/.zshrc

# 4. VERIFY: confirm change AND check for collateral
grep -n 'build-adoc\|adoc-pdf' zsh/.zshrc

Without the line number prefix (1760s/), sed replaces every match in the file — a shotgun. With it, surgical. The line number comes from grep -n.

multi-line verify — check two specific lines at once
awk 'NR==1218 || NR==1760' zsh/.zshrc
range extraction — NR for surgical reads from large files
# grep found the error at line 44164 — read 50 lines of context
awk 'NR>=44160 && NR<=44210' session-dump.adoc

No head | tail chains. No sed -n '44160,44210p'. One awk, two numbers.

grep -oP with \K — value extraction from key-value logs

extract just the value after a key (Perl regex)
# ISE syslog — extract failure reasons
grep -oP 'FailureReason=\K[^,;]+' /var/log/syslog | sort | uniq -c | sort -rn

# ISE — extract MAC addresses
grep -oP 'Calling-Station-ID=\K[0-9A-Fa-f:.-]+' /var/log/syslog | sort -u

# ISE — extract NAS IPs
grep -oP 'NAS-IP-Address=\K[0-9.]+' /var/log/syslog | sort -u

# ISE — extract device names
grep -oP 'NetworkDeviceName=\K[^,;]+' /var/log/syslog | sort -u

\K resets the match start — everything before \K is required context but excluded from output. [^,;]+ captures until the next delimiter. Pipe to sort -u for unique, sort | uniq -c | sort -rn for counted frequency.

pattern: grep -oP 'KEY=\KVALUE_REGEX' | sort pipeline
# Generic form — works for any key=value log format
grep -oP 'FIELD_NAME=\K[^,;]+' logfile | sort | uniq -c | sort -rn | head -20

jq — JSON processing

extract nested fields
curl -s localhost:8080/stats | jq '.stats.total_files'
filter array by property
jq '.results[] | select(.category == "standards")' response.json
transform to TSV for spreadsheets
jq -r '.[] | [.title, .path] | @tsv' response.json | column -t -s $'\t'
GitHub API + jq — commit history by path
gh api "repos/EvanusModestus/domus-captures/commits?path=docs/&per_page=10" |
  jq -r '.[] | "\(.commit.author.date[:10]) \(.sha[:7]) \(.commit.message | split("\n")[0])"'

Shell Patterns

xargs — when the next command reads arguments, not stdin

Next command reads…​ Use

stdin (awk, grep, wc, sort)

pipe directly

arguments (stat, rm, cp, nvim, git add)

xargs

copy today’s files to backup — -I{} placeholder
mkdir -p /tmp/adoc-backup-$(date +%F) && \
  find . -name "*.adoc" -type f -newermt "$(date +%F)" | \
  xargs -I{} cp {} /tmp/adoc-backup-$(date +%F)/
parallel validation — -P4 runs 4 at a time
find .drafts -name "*.adoc" -type f | xargs -P4 -I{} asciidoctor -o /dev/null {}
null-delimited pipeline — safe for filenames with spaces
find . -name "*.adoc" -type f -print0 | xargs -0 wc -l

Process substitution — <(cmd) treats output as a file

compare tracker state: yesterday vs today
diff <(grep '|' partials/trackers/work/adhoc/carryover.adoc | head -20) \
     <(git show HEAD~1:partials/trackers/work/adhoc/carryover.adoc | grep '|' | head -20)
files on disk vs files in nav — drift detection
diff <(find docs/modules/ROOT/pages/projects/chla/mschapv2-migration -name "*.adoc" -type f | sort) \
     <(grep -oP 'mschapv2-migration/[^[]+\.adoc' docs/modules/ROOT/nav.adoc | sort)

Command substitution — embed output as arguments

open most recently modified file in nvim
nvim "$(find data/ -name '*.adoc' -type f -printf '%T@ %p\n' | sort -rn | awk 'NR==1{print $2}')"
line count across a project
wc -l $(find docs/modules/ROOT -path '*mschapv2*' -name '*.adoc' -type f)

Conditional execution — capture, test, act

open matching files only if they exist
files=$(find .drafts -name 'in*' -type f) && [ -n "$files" ] && nvim $files
open files that contain unchecked items
files=$(grep -rl '\[ \]' .drafts/*.adoc) && [ -n "$files" ] && nvim $files
guard with grep -q — only act if pattern matches
grep -q 'TODO\|FIXME\|\[ \]' "$file" && nvim "$file"

Pattern: $(capture)[ -n ] tests non-empty → && only proceeds if true. grep -q is the idempotent guard — run repeatedly, only opens when there’s work.

Decrypt and open — find .age, decrypt, nvim in one shot

files=$(find . -name "*tcp-clock*.age" -type f) && \
  [ -n "$files" ] && echo "$files" | xargs -I{} decrypt-file {} && \
  nvim $(echo "$files" | sed 's/\.age$//')

Pattern: find .age only (never tries plaintext), sed derives the decrypted path, guard prevents empty nvim. Change the glob to match any project.

tee_clean — color on screen, clean text in file

tee_clean() {
  tee >(sed 's/\x1b\[[0-9;]*m//g' > "$1")
}

# Color output on terminal, stripped in file
jq -C '.' data.json | tee_clean output.json
xq -C '.' data.xml | tee_clean output.json

# Wrap a whole block
{
  echo "=== Summary ==="
  jq -C '.[] | .name' data.json
} | tee_clean summary.txt

The >(cmd) is process substitution — tee writes to stdout AND to the subshell pipe. sed strips ANSI escape sequences (\x1b\[[0-9;]*m) before they hit the file.

Dependency check — verify toolchain in one shot

for cmd in asciidoctor asciidoctor-pdf pandoc rouge d2 mmdc age; do
  printf "%-20s %s\n" "$cmd" "$(command -v $cmd >/dev/null 2>&1 && echo 'OK' || echo 'MISSING')"
done

Pattern: command -v checks if binary exists on PATH. >/dev/null 2>&1 suppresses output — we only care about exit code. Swap the tool list for any project’s dependencies.

printf safety — dashes as data, not options

wrong — printf treats --- as invalid option
printf '---\n\n'
right — %s format string treats --- as data
printf '%s\n\n' '---'

Kill stuck SSH sessions

Find established SSH connections
lsof -i TCP -n -P | awk '/ssh.*ESTABLISHED/ {print $2, $9}'
Kill all stuck SSH sessions to a specific host
lsof -i TCP -n -P | awk '/ssh.*kvm-01.*ESTABLISHED/ {print $2}' | sort -u | xargs kill
Kill ALL stuck SSH sessions
lsof -i TCP -n -P | awk '/ssh.*ESTABLISHED/ {print $2}' | sort -u | xargs kill

lsof -i TCP -n -P lists all TCP connections. awk filters for SSH + ESTABLISHED, prints only the PID ($2). sort -u deduplicates (multiple file descriptors per process). xargs kill sends SIGTERM to each.

File Descriptors & Redirection

The three file descriptors

FD Name Purpose

0

stdin

input to the command

1

stdout

normal output (valid results)

2

stderr

error messages

Split stdout and stderr into separate files

find / -name "*.conf" 1>results.txt 2>errors.txt

Suppress errors — 2>/dev/null

find / -name "*.conf" 2>/dev/null

Merge stderr into stdout — 2>&1

command 2>&1 | grep "pattern"

This sends both stdout and stderr through the pipe. Without 2>&1, only stdout reaches grep — errors print to the terminal and bypass the pipeline.

Heredoc patterns

multi-line input to a command
cat <<'EOF'
Line 1
Line 2
EOF
heredoc commit messages (quotes prevent variable expansion)
git commit -m "$(cat <<'EOF'
feat: add new feature

Multi-line description here.
EOF
)"

API & curl/jq

domus-api — Documentation System REST API

start the API server
cd ~/atelier/_projects/personal/domus-api && uv run uvicorn domus_api.main:app --host 0.0.0.0 --port 8080
health check
curl -s localhost:8080/ | jq
full-text search
curl -s 'localhost:8080/search?q=mandiant' | jq
search — extract path, title, match count
curl -s 'localhost:8080/search?q=mandiant' | jq '.results[] | {path, title, match_count}'
list pages by category
curl -s 'localhost:8080/pages?category=standards' | jq
all antora.yml attributes
curl -s localhost:8080/attributes | jq

GitHub API

cross-repo search via GitHub API
gh search code "vault seal" --owner EvanusModestus --json repository,path,textMatches |
  jq '.[] | {repo: .repository.full_name, file: .path, match: .textMatches[].fragment}'
count .adoc files in a repo via API
gh api 'repos/EvanusModestus/domus-captures/git/trees/main?recursive=1' |
  jq '[.tree[] | select(.path | endswith(".adoc"))] | length'

Domus Workflows

Read content from terminal (meeting-ready)

today’s worklog
bat docs/modules/ROOT/pages/2026/04/WRKLOG-$(date +%Y-%m-%d).adoc
current priorities
bat docs/modules/ROOT/partials/trackers/work/priorities/current.adoc
carryover backlog
bat docs/modules/ROOT/partials/trackers/work/adhoc/carryover.adoc
any project summary
bat docs/modules/ROOT/partials/projects/mandiant-remediation/summary.adoc

Search and discovery

find all files related to a topic
grep -rl "MSCHAPv2" docs/modules/ROOT/ --include="*.adoc" | sort
search codex entries
grep -rn "pattern" docs/modules/ROOT/partials/codex/ --include="*.adoc" -B1 -A3
list all worklogs for a month
ls -1 docs/modules/ROOT/pages/2026/04/WRKLOG-*.adoc

Tracker aging — calculate days from origin

how many days since a carryover item started
echo $(( ($(date +%s) - $(date -d "2026-03-09" +%s)) / 86400 ))

Encrypted data access (d001)

view encrypted file without disk write
age --decrypt -i ~/.secrets/.metadata/keys/master.age.key \
  data/d001/projects/mandiant-remediation/findings-status-2026-04-16.adoc.age \
  | bat --language asciidoc
project encryption dashboard
for d in data/d001/projects/*/; do
  total=$(find "$d" -type f | wc -l)
  plain=$(find "$d" -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | wc -l)
  printf "%-25s %s files  %s plaintext\n" "$(basename "$d")" "$total" "$plain"
done

d000 study builds

batch build all docs for a Quijote chapter range
for d in p1-cap-03{7,8,9}; do
  for f in data/d000/education/quijote-study/notas/$d/*.adoc; do
    d000 build "$d/$(basename "$f" .adoc)" html --variant light-cyan
  done
done
build a single study doc (use unique path fragment)
d000 build p1-cap-038/texto-anotado html --variant light-cyan
d000 build p1-cap-038/texto-anotado pdf --theme light-cyan
batch build PDFs for a chapter range
for d in p1-cap-03{7,8,9}; do
  for f in data/d000/education/quijote-study/notas/$d/*.adoc; do
    d000 build "$d/$(basename "$f" .adoc)" pdf --theme light-cyan
  done
done
open all rendered chapters at once
firefox data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.html &
firefox data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.pdf &
print chapter PDFs (requires CUPS configured)
lp data/d000/education/quijote-study/notas/p1-cap-03{7,8,9}/output/*.pdf
build LPL logic study (English / Spanish)
d000 build annotated-text pdf --theme light-cyan
d000 build lpl-study/notas/texto-anotado pdf --theme light-cyan
build Cicero study
d000 build de-oratore/libro-i/texto-anotado html --variant light-cyan

Available themes

PDF themes (--theme)
ls ~/atelier/_bibliotheca/domus-asciidoc-build/themes/pdf/ | sed 's/-theme\.yml//'
# base blue burgundy catppuccin creative dark don-quijote green
# learning light-cyan navy operations orange purple reference royal
HTML variants (--variant)
~/atelier/_bibliotheca/domus-asciidoc-build/docinfo/compose.sh --list
# light dark catppuccin royal light-cyan

ISE & Network Ops

ISE ERS API — endpoint CRUD

set credentials (session)
export ISE_HOST="{ise-ip}" ISE_USER="admin" ISE_PASS="$(gopass show -o ise/admin)"
list identity groups
curl -sk "https://$ISE_HOST:{ise-ers-port}/ers/config/identitygroup" \
  -H "Accept: application/json" -u "$ISE_USER:$ISE_PASS" | jq '.SearchResult.resources[].name'
check if endpoint exists by MAC
curl -sk "https://$ISE_HOST:{ise-ers-port}/ers/config/endpoint?filter=mac.EQ.AA:BB:CC:DD:EE:FF" \
  -H "Accept: application/json" -u "$ISE_USER:$ISE_PASS" | jq '.SearchResult.total'

Certificate inspection

view EAP-TLS client cert from local store
openssl x509 -in {cert-dir}/client.pem -text -noout | head -30
check cert expiry
openssl x509 -in {cert-dir}/client.pem -enddate -noout

Network diagnostics

check listening ports
ss -tlnp | grep -E ':{port-https}|:{port-ssh}|:{port-ldaps}'
test ISE connectivity
nc -zv {ise-ip} {ise-ers-port}
DNS resolution
dig {ise-hostname} +short

ISE eval rotation — backup & restore

backup from ISE CLI (when admin UI is license-locked)
# SSH to ISE
ssh admin@ise-02.inside.domusdigitalis.dev

# Verify NAS repo
show repository nas-01

# Get encryption key (on workstation)
dsource d000 dev/storage
echo $ISE_BACKUP_KEY

# Run backup
backup pre-rotation-2026-06 repository nas-01 ise-config encryption-key plain <KEY>
list backups on NAS
ssh admin@ise-02.inside.domusdigitalis.dev
show repository nas-01
restore to fresh ISE node
configure terminal
repository nas-01
  url nfs://10.50.1.70:/volume1/ise_backups
exit

restore <backup-filename> repository nas-01 encryption-key plain <KEY>

VyOS — VRRP & VLAN inspection

VRRP status and VIPs
show vrrp
show configuration commands | grep vrrp | grep 'address'
firewall zone membership
show configuration commands | grep 'firewall zone' | grep 'member'
DHCP leases and ARP
show dhcp server leases
show arp
full interface/VLAN map
show interfaces

CUPS printing — validation & setup

software validation
command -v lpstat && echo "CUPS present" || echo "CUPS not installed"
lpstat -r                                # scheduler running?
lpstat -p -d                             # printers + default
daemon lifecycle
sudo systemctl enable --now cups         # start + persist
printer discovery
lpinfo -v                                # available backends/URIs
lpinfo -m | grep -i <brand>             # available drivers
add printer and set default
sudo lpadmin -p <name> -v <uri> -m everywhere -E
lpoptions -d <name>
print
lp file.pdf                              # default printer
lp -d <name> -o sides=two-sided-long-edge file.pdf

PowerShell (from zsh)

All PowerShell commands run inside pwsh -NoLogo -Command '…​' from zsh. Running them bare fails — zsh interprets $, |, () as shell syntax.

Process management

top 5 processes by memory
pwsh -NoLogo -Command 'Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 5 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} | Format-Table'
stop/start Teams
pwsh -NoLogo -Command 'Get-Process | Where-Object {$_.ProcessName -like "*teams*"} | Stop-Process'
pwsh -NoLogo -Command 'Start-Process "ms-teams"'

Export to JSON (pipe to jq)

always use -NoLogo when piping pwsh output to zsh tools
pwsh -NoLogo -Command 'Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 5 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} | ConvertTo-Json' | jq '.'
Never pipe Format-Table into ConvertTo-Json — it produces layout metadata, not data. Select-Object first, then ConvertTo-Json.

Wi-Fi management (netsh)

force fresh network scan
netsh wlan disconnect interface="Wi-Fi"
netsh wlan show networks mode=bssid
netsh wlan connect name="CHLA-Remote" interface="Wi-Fi"

SSH from PowerShell

connect to homelab from Windows terminal
ssh evan@modestus-razer.inside.domusdigitalis.dev

WSL ↔ Windows — Cross-Environment Commands

From zsh (WSL) — control Windows
run any PowerShell command from zsh
pwsh -NoLogo -Command 'Get-Date'
run multi-line PowerShell from zsh (heredoc)
pwsh -NoLogo -Command "$(cat <<'PS'
$procs = Get-Process | Where-Object { $_.WorkingSet64 -gt 100MB }
$procs | Sort-Object WorkingSet64 -Descending |
  Select-Object ProcessName, Id, @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} |
  Format-Table -AutoSize
PS
)"
open a file in Windows from WSL
# Open in default Windows app
wslview /mnt/c/Users/erosado/Documents/report.pdf

# Open Explorer to current WSL directory
explorer.exe .

# Open specific Windows path
explorer.exe 'C:\Users\erosado\Downloads'
copy WSL output to Windows clipboard
# Pipe anything to Windows clipboard
cat file.txt | clip.exe

# Copy a command's output
pwsh -NoLogo -Command 'Get-TransportRule | Format-List Name, State' | clip.exe
access Windows files from WSL
# Windows C: drive is at /mnt/c
ls /mnt/c/Users/erosado/Downloads/

# Copy from Windows to WSL
cp /mnt/c/Users/erosado/Downloads/report.pdf ~/atelier/

# Watch a Windows directory for new files
find /mnt/c/Users/erosado/Downloads -maxdepth 1 -mmin -5 -type f -printf '%T+ %p\n' | sort -r
From PowerShell — control WSL
run a bash command from PowerShell
wsl -e bash -c 'grep -rn "Ghost-Sender" ~/atelier/_bibliotheca/domus-captures/docs/'
run a specific WSL command and capture output
$result = wsl -e bash -c 'git -C ~/atelier/_bibliotheca/domus-captures log --oneline -5'
$result
Process Management — Windows Side
top processes by memory — formatted table
pwsh -NoLogo -Command '
Get-Process | Sort-Object WorkingSet64 -Descending |
  Select-Object -First 20 ProcessName, Id,
    @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}},
    @{N="CPU(s)";E={[math]::Round($_.CPU,1)}},
    @{N="Handles";E={$_.HandleCount}} |
  Format-Table -AutoSize'
find a specific process
pwsh -NoLogo -Command 'Get-Process | Where-Object { $_.ProcessName -like "*teams*" } |
  Select-Object ProcessName, Id, @{N="MB";E={[math]::Round($_.WorkingSet64/1MB)}} |
  Format-Table -AutoSize'
kill by name
pwsh -NoLogo -Command 'Stop-Process -Name "Teams" -Force -ErrorAction SilentlyContinue'
kill by PID
pwsh -NoLogo -Command 'Stop-Process -Id 12345 -Force'
what’s listening on a port (Windows equivalent of ss -tulnp)
pwsh -NoLogo -Command 'Get-NetTCPConnection -State Listen |
  Select-Object LocalAddress, LocalPort, OwningProcess,
    @{N="Process";E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} |
  Sort-Object LocalPort | Format-Table -AutoSize'
specific port check
pwsh -NoLogo -Command 'Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue |
  Select-Object LocalAddress, LocalPort, RemoteAddress, State,
    @{N="Process";E={(Get-Process -Id $_.OwningProcess).ProcessName}}'
Services — Windows Side
list running services
pwsh -NoLogo -Command 'Get-Service | Where-Object { $_.Status -eq "Running" } |
  Sort-Object DisplayName | Format-Table Name, DisplayName, Status -AutoSize'
check a specific service
pwsh -NoLogo -Command 'Get-Service -Name "WinRM" | Format-List Name, DisplayName, Status, StartType'
restart a service
Restart-Service -Name "WinRM" -Force
System Info — Quick Health from zsh
one-shot Windows system summary
pwsh -NoLogo -Command '
Write-Host "=== Windows System ===" -ForegroundColor Cyan
Write-Host "Hostname: $env:COMPUTERNAME"
Write-Host "User:     $env:USERNAME"
Write-Host "OS:       $((Get-CimInstance Win32_OperatingSystem).Caption)"
Write-Host "Uptime:   $((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime)"
Write-Host "RAM:      $([math]::Round((Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize/1MB))GB total, $([math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory/1MB))GB free"
Write-Host "CPU:      $((Get-CimInstance Win32_Processor).Name)"
Write-Host "Disk C:   $([math]::Round((Get-PSDrive C).Free/1GB))GB free of $([math]::Round(((Get-PSDrive C).Used + (Get-PSDrive C).Free)/1GB))GB"'
disk usage — all drives
pwsh -NoLogo -Command 'Get-PSDrive -PSProvider FileSystem |
  Select-Object Name, @{N="Used(GB)";E={[math]::Round($_.Used/1GB,1)}},
    @{N="Free(GB)";E={[math]::Round($_.Free/1GB,1)}},
    @{N="Total(GB)";E={[math]::Round(($_.Used+$_.Free)/1GB,1)}} |
  Format-Table -AutoSize'
Exchange Online — Connect from zsh
connect to Exchange Online (launches MFA prompt in Windows)
pwsh -NoLogo -Command 'Connect-ExchangeOnline -UserPrincipalName erosado@chla.usc.edu'
MFA prompt opens in the Windows browser. After auth, the session persists in the pwsh process. For multi-command sessions, start pwsh interactively instead of one-shot commands.
interactive PowerShell session from zsh (for Exchange, etc.)
pwsh -NoLogo
# Then inside pwsh:
# Connect-ExchangeOnline
# Get-TransportRule | Format-List Name, State
# exit
File Transfer Patterns
move files between WSL and Windows
# WSL → Windows Downloads
cp ~/atelier/_bibliotheca/domus-captures/output/report.pdf /mnt/c/Users/erosado/Downloads/

# Windows → WSL (glob)
cp /mnt/c/Users/erosado/Downloads/*.{png,pdf,jpg} ~/atelier/_staging/

# Bulk move with null safety
find /mnt/c/Users/erosado/Downloads -maxdepth 1 -name '*.pdf' -mmin -60 -print0 |
  xargs -0 -I{} cp {} ~/atelier/_staging/
watch Windows Downloads for new files (live)
inotifywait -m /mnt/c/Users/erosado/Downloads -e create -e moved_to |
  awk '{printf "%s  %s\n", strftime("%H:%M:%S"), $3}'
inotifywait requires inotify-tools. Install with sudo pacman -S inotify-tools if not present.

Security & Encryption

View encrypted files without writing to disk

pipe age decrypt to bat — nothing touches the filesystem
age --decrypt -i ~/.secrets/.metadata/keys/master.age.key \
  data/d001/projects/mandiant-remediation/findings-status-2026-04-16.adoc.age \
  | bat --language asciidoc --file-name "findings-status-2026-04-16.adoc"

Batch re-encrypt — brace expansion + loop

re-encrypt multiple project files
for f in data/d001/projects/mandiant-remediation/{findings-status,guest-acl-update,siem-report}-2026-04-16.adoc; do
  rm -f "${f}.age" && echo y | encrypt-file "$f"
done
Always rm -f the .age first. If you skip it, encrypt-file prompts about overwrite and may only delete the plaintext without re-encrypting.

Detect stale plaintext — files needing re-encryption

find plaintext newer than its .age counterpart
for f in data/d001/projects/*/*.adoc; do
  age="${f}.age"
  if [ -f "$f" ] && [ -f "$age" ]; then
    pt_mod=$(/usr/bin/stat -c'%Y' "$f")
    age_mod=$(/usr/bin/stat -c'%Y' "$age")
    [ "$pt_mod" -gt "$age_mod" ] && echo "STALE: $f"
  fi
done

Secure delete — shred for sensitive plaintext

shred -u data/d001/projects/mandiant-remediation/man-report.txt
On SSD/NVMe, shred is less effective (wear leveling), but better than rm which only removes the directory entry.

Pre-push audit — find all unencrypted project files

find data/d001/projects -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | sort

System & Infrastructure

PipeWire audio validation

wpctl status                                    # PipeWire status
pactl list sinks short                          # list audio sinks
pw-play /usr/share/sounds/freedesktop/stereo/bell.oga  # test default sink
journalctl -b --grep='sof|cs35l56' --no-pager | tail -20  # kernel audio firmware
cat /proc/asound/cards                          # ALSA sound cards

gopass — personal document management

gopass-personal-docs    # interactive entry creation
gopass-query bills      # list recurring bills with totals
gopass-query storage    # list storage units with gate codes
gopass-query export bills  # export category to JSON

Makefile — daily workflow

make new-day      # create today's worklog + update attributes
make serve        # build + local server (port 8000)
make              # build only
make sync-nav     # sync worklog nav entries
make update-index # rebuild monthly index

KVM — VM & ISO management

list VMs on a KVM host
ssh kvm-01 "sudo virsh list --all"
ssh kvm-02 "sudo virsh list --all"
find ISE ISOs across KVM hosts (case-insensitive glob)
ssh kvm-01 "ls -lh /mnt/nas/isos/*[Ii][Ss][Ee]* /var/lib/libvirt/images/*[Ii][Ss][Ee]* /mnt/onboard-ssd/isos/*[Ii][Ss][Ee]* 2>/dev/null"
ssh kvm-02 "ls -lh /mnt/nas/isos/*[Ii][Ss][Ee]* /mnt/ssd/libvirt/images/*[Ii][Ss][Ee]* 2>/dev/null"
console into a VM
sudo virsh console <vm-name>             # Escape: Ctrl+]
check NAS mount on KVM host
ssh kvm-01 "mount | grep nas; ls /mnt/"

Per-project file dashboard

per-project summary — total files vs unencrypted plaintext
for d in data/d001/projects/*/; do
  total=$(find "$d" -type f | wc -l)
  plain=$(find "$d" -type f ! -name '*.age' ! -name 'README.adoc' ! -name '.gitkeep' ! -name '*.py' | wc -l)
  echo "$(basename "$d") | ${total} files | ${plain} plaintext"
done

USB-C / Thunderbolt Charging Diagnostics

Full evidence capture to file (one block, timestamped)
{
  echo "=== Power Supply ==="
  cat /sys/class/power_supply/*/status
  echo ""
  cat /sys/class/power_supply/*/type
  echo ""
  echo "=== UPower ==="
  upower -d | grep -E 'state|percentage|energy-rate|voltage'
  echo ""
  echo "=== dmesg (typec/thunderbolt/PD) ==="
  sudo dmesg | grep -iE 'typec|thunderbolt|ucsi|PD|power.delivery|charging' | tail -20
  echo ""
  echo "=== Pacman log (kernel/typec) ==="
  grep -iE 'thunderbolt|typec|ucsi|^.*upgraded linux ' /var/log/pacman.log | tail -20
} | tee /tmp/INC-$(date +%F)-usbc-charging.txt

Pattern: { } groups commands into a single stdout stream. tee writes to file AND displays on screen. Reusable for any multi-command evidence capture — change the commands inside, keep the structure.

pacman — package inspection

Source: 2026-06-25 — curl error 77 investigation, checking for package upgrades

# Check package version and install date (awk field filter)
pacman -Qi curl | awk '/^Version|^Install Date/'