WRKLOG-2026-06-23

Summary

Tuesday. 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

Triage Status

Item Status Destination

PeopleSoft timesheet — 3 weeks outstanding

■■■■■□□□□□ 5/10

trackers/work/peoplesoft-codes/timesheet-2026-06-{08,15}.adoc

TCP Clocks — code 000016014 approved (60 hrs)

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

trackers/work/peoplesoft-codes/codes.adoc

Secrets backup cleanup — d000 tier

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

Script: scripts/secrets-backup-cleanup.sh

alias-viewer.sh rewrite

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

scripts/alias-viewer.sh

MSCHAPv2 d001 project restructure

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

data/d001/projects/mschapv2-migration/

.zshrc cleanup — dots-quantum

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

dots-quantum/zsh/.zshrc — restow needed

Linux Research — d000/d001 split

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

data/d00{0,1}/projects/linux-research/

Abnormal Security — d000/d001 restructure

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

data/d00{0,1}/projects/abnormal-security/

Essential tools inventory — triad audit

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

worklog/daily-notes/2026-06/2026-06-23/essential-tools-inventory.adoc

find time patterns — mtime vs newermt

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

worklog/daily-notes/2026-06/2026-06-23/find-time-patterns.adoc

tmux-quantum switchover runbook

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

worklog/daily-notes/2026-06/2026-06-23/tmux-quantum-switchover.adoc

WLC 9800 RADIUS accounting audit

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

worklog/daily-notes/2026-06/2026-06-23/wlc-accounting-audit.adoc

STD-026 Network Device Interaction

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

standards/operations/network-device-interaction.adoc

wlc-push.sh — automated CLI push tool

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

data/d000/infra/wlc-api-reference/scripts/cli/wlc-push.sh

WLC accounting fix — ISE-ACCT on all profiles

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

/tmp/wlc-push-2026-06-23-204101.txt

PeopleSoft Timesheet — Catch-Up Submission

PeopleSoft Timesheet — Catch-Up Submission

Three weeks outstanding since last submitted timesheet (2026-06-01 through 2026-06-05).

Table 1. Outstanding Periods
Pay Period Status Timesheet Partial

2026-06-08 through 2026-06-12

FAILt submitted

partials/trackers/work/peoplesoft-codes/timesheet-2026-06-08.adoc

2026-06-15 through 2026-06-19

FAILt submitted

partials/trackers/work/peoplesoft-codes/timesheet-2026-06-15.adoc

2026-06-22 through 2026-06-26

🟡 Current week (partial)

partials/trackers/work/peoplesoft-codes/timesheet-2026-06-22.adoc

Submission Checklist
  • Fill in hours for week of 06-08 — reference daily notes + session logs

  • Fill in hours for week of 06-15 — reference daily notes + session logs

  • Verify PeopleSoft codes against partials/trackers/work/peoplesoft-codes/codes.adoc

  • Cross-check: projects marked NEEDS CODE in codes tracker

  • Submit all outstanding periods in PeopleSoft

  • Confirm approval routing to manager

Reconstruction Tip

Use daily notes to reconstruct hours:

ls docs/modules/ROOT/partials/worklog/daily-notes/2026-06-{08,09,10,11,12}/*.adoc
ls docs/modules/ROOT/partials/worklog/daily-notes/2026-06-{15,16,17,18,19}/*.adoc

TCP Clocks — PeopleSoft Code Approved

TCP Clocks — PeopleSoft Code Approved

Chris Muburi (PM) approved 60 hours to cover entirety of project work.

Table 2. Code Assignment
Field Value

Project

TCP Clocks Deployment

Code

000016014

Hours Approved

60

Scope

Full project — ISE onboarding, switch config, validation, go-live

PM

Chris Muburi

Date Approved

2026-06-23

Action Items
  • Update partials/trackers/work/peoplesoft-codes/codes.adoc — add code 000016014 to TCP Clocks row

  • Backfill prior timesheets where TCP Clocks hours were unattributed

  • Track burn rate against 60-hour budget

Secrets Backup Cleanup — d000 Tier

Secrets — Backup Cleanup & Inventory Rebuild

Phase 1: Backup Pruning (✅ Complete)

~70 stale .backup-* files pruned to 26 (retention: 3 per file).

bash scripts/secrets-backup-cleanup.sh --dry-run
bash scripts/secrets-backup-cleanup.sh --keep 3
Phase 2: Tier Consolidation — Two-Tier Model

Decision: drop lab and staging. Dev absorbs lab (experiment + test). Prod stays prod.

Table 3. Before (4 tiers)
env file dev lab staging production

app

PASS

PASS

PASS

identifiers

PASS

PASS

PASS

identity

PASS

PASS

PASS

network

PASS

PASS

PASS

storage

PASS

PASS

PASS

http

PASS

FAIL

FAIL

pki

PASS

FAIL

PASS

PASS

vault

PASS

FAIL

FAIL

observability

PASS

FAIL

FAIL

cicd

PASS

FAIL

FAIL

identity/ad-*

PASS

FAIL

FAIL

network/ise

PASS

FAIL

FAIL

network/wlc

PASS

FAIL

FAIL

pki/byod

PASS

FAIL

PASS

PASS

Table 4. After (2 tiers)
env file dev prod

app

PASS

PASS

identifiers

PASS

PASS

identity

✅ (+ scoped)

PASS

network

✅ (+ scoped)

PASS

storage

PASS

PASS

http

PASS

pki

✅ (+ scoped)

✅ (+ scoped)

vault

PASS

observability

PASS

cicd

PASS

Phase 3: DOMAIN_INVENTORY Rebuild

Generator script creates YAML skeleton from filesystem:

bash scripts/secrets-inventory-gen.sh --domain d000
bash scripts/secrets-inventory-gen.sh --domain d000 --output /tmp/generated.yaml

Hand-crafted inventory with descriptions and migration roadmap at /tmp/DOMAIN_INVENTORY.yaml.

Phase 4: Validate with yq
List tiers in hand-crafted inventory
yq '.tiers | keys' /tmp/DOMAIN_INVENTORY.yaml
List all env files per tier
yq '.tiers.dev | keys' /tmp/DOMAIN_INVENTORY.yaml
yq '.tiers.prod | keys' /tmp/DOMAIN_INVENTORY.yaml
Show deprecated files awaiting migration
yq '.tiers.dev | to_entries | map(select(.value.status == "DEPRECATED")) | from_entries | keys' /tmp/DOMAIN_INVENTORY.yaml
Show scoped files under a deprecated parent
yq '.tiers.dev.network.scoped | keys' /tmp/DOMAIN_INVENTORY.yaml
yq '.tiers.dev.identity.scoped | keys' /tmp/DOMAIN_INVENTORY.yaml
yq '.tiers.dev.pki.scoped | keys' /tmp/DOMAIN_INVENTORY.yaml
Compare structure: hand-crafted vs generated
diff <(yq '.tiers | keys' /tmp/DOMAIN_INVENTORY.yaml) \
     <(yq '.tiers | keys' /tmp/generated.yaml)
Show retired tiers and removal commands
yq '.retired | to_entries[] | .key + ": " + .value.reason' /tmp/DOMAIN_INVENTORY.yaml
Full validation: round-trip encrypt → decrypt → compare
yq '.' /tmp/DOMAIN_INVENTORY.yaml > /tmp/before.yaml
age -e -R ~/.age/recipients/self.txt -o /tmp/test.yaml.age /tmp/DOMAIN_INVENTORY.yaml
age -d -i ~/.age/identities /tmp/test.yaml.age | yq '.' > /tmp/after.yaml
diff /tmp/before.yaml /tmp/after.yaml && echo "Round-trip clean" || echo "MISMATCH"
rm /tmp/before.yaml /tmp/after.yaml /tmp/test.yaml.age
Phase 5: Deploy
rm ~/.secrets/environments/domains/DOMAIN_INVENTORY.yaml.age
age -e -R ~/.age/recipients/self.txt \
  -o ~/.secrets/environments/domains/DOMAIN_INVENTORY.yaml.age \
  /tmp/DOMAIN_INVENTORY.yaml
age -d -i ~/.age/identities ~/.secrets/environments/domains/DOMAIN_INVENTORY.yaml.age \
  | yq '.tiers | keys'
rm /tmp/DOMAIN_INVENTORY.yaml /tmp/generated.yaml
git -C ~/.secrets add environments/domains/DOMAIN_INVENTORY.yaml.age
git -C ~/.secrets commit -m "docs(d000): two-tier model — retire lab/staging, blast radius roadmap"
Phase 6: Remove Retired Tiers
Only after verifying lab/staging credentials are redundant.
diff <(age -d -i ~/.age/identities ~/.secrets/environments/domains/d000/lab/network.env.age \
       | yq 'keys') \
     <(age -d -i ~/.age/identities ~/.secrets/environments/domains/d000/dev/network.env.age \
       | yq 'keys')
rm -rf ~/.secrets/environments/domains/d000/lab
rm -rf ~/.secrets/environments/domains/d000/staging
rm -rf ~/.secrets/environments/domains/d000/notes
git -C ~/.secrets add -A
git -C ~/.secrets commit -m "chore(d000): remove retired tiers (lab, staging, notes)"
Decisions Made
  • Two-tier model: dev + prod (lab and staging retired)

  • Blast radius: scoped subdirectories over flat files

  • Backup retention: 3 most recent per file

  • notes/ — remove (empty, no purpose)

  • Lab/staging credential diff — verify before deletion

  • Phase 2 scoped decomposition — network, identity, pki

alias-viewer.sh — Shell Alias Inspector

alias-viewer.sh — Shell Alias Inspector

Rewrote scripts/alias-viewer.sh from scratch. Original was broken: alias returns nothing in non-interactive shells, sed|sed|awk|sort|column pipeline was fragile, sort -t '│' fails on multi-byte Unicode.

Table 5. What changed
Problem Fix

alias empty in bash script.sh

--stdin flag — pipe from live shell: alias | ./script --stdin

sed | sed | awk | sort | column

Single gawk program — parse, filter, categorize, sort in one pass

sort -t '│' multi-byte error

asorti() inside gawk — no external sort needed

-s git matches digital

-x flag for word-boundary search via regex

All aliases in one flat list

-c categorizes into 20 groups by prefix, sorted within each

Categories (20)

git, docker, kubernetes, jq-yq, python, terraform, vault, secrets, infra-session, hyprland, packages, systemd, domus-nav, navigation, history-vault, editor, files, safety, diff, grep, general

Usage
alias | ./scripts/alias-viewer.sh --stdin -c
alias | ./scripts/alias-viewer.sh --stdin -s git -x
alias | ./scripts/alias-viewer.sh --stdin -s cd -x -c
Teaching points
  • gawk asorti() — sort associative array keys without external sort

  • index() + substr() — parse name=value without fragile -F field splitting

  • 2D associative arrays — entries[category][name] for grouped storage

  • Word-boundary regex — (|[a-zA-Z0-9])term($|[^a-zA-Z0-9]) prevents substring false positives

  • \047 octal escape — single quote inside awk without shell quoting hell

MSCHAPv2 Migration — d001 Project Restructure

Brought data/d001/projects/mschapv2-migration/ into compliance with STD-001, STD-020, STD-025.

Changes

  • STD-025 (Conditional Rendering): Rewrote attributes.adoc with ifdef::env-d001[] + zeroed ifndef fallback — 60 naked attribute definitions wrapped

  • STD-001 (Project Structure): Created metadata.adoc and summary.adoc partials. Demoted 4 misplaced assemblers (mschapv2-migration-team, mschapv2-queries, mschapv2-standard-report, mschapv2-weekly-report) to content-only partials

  • STD-020 (Diagrams): Created diagrams/ with migration-architecture.dot (Graphviz) and migration-architecture.d2 (D2), both Catppuccin Mocha. Added README.adoc attribute map

  • STD-008 (Code Block Standard): Added subs=attributes+ to all [source,bash] blocks referencing {attributes}. Added Sheet 6 (policy-match-by-protocol) to weekly runbook

  • File renames: Removed date stamps from PowerShell partials (pwsh-cmds-2026-05-26-v2pwsh-rsat-discovery, pwsh-reports-2026-05-27pwsh-gpo-analytics). Cleaned API report names (dataconnect-auth-reportdataconnect-report, etc.)

  • Root assembler: Rewritten with logical section grouping — summary → metadata → team → waves → runbook → queries → API reports → PowerShell

Build & Verify

build-adoc $(find data/d001/projects/mschapv2-migration -maxdepth 1 -name 'mschapv2-migration.adoc') html --variant light-cyan -a env-d001 && firefox $(find data -path '*/mschapv2-migration/output/mschapv2-migration.html') &

(find data/d001/projects/mschapv2-migration -maxdepth 1 -name 'mschapv2-migration.adoc') data/d001/projects/mschapv2-migration/mschapv2-migration.adoc

grep -rn 'Investigation Steps '(find data/d001/projects/mschapv2-migration -maxdepth 1 -name 'mschapv2-migration.adoc')

Investigation Steps

Partials (18 → 17, clean slugs)

attributes.adoc          metadata.adoc           summary.adoc
team.adoc                queries.adoc            weekly-report.adoc
dataconnect-report.adoc  ers-report.adoc         mnt-report.adoc
endpoint-profile.adoc    wave1-chromebooks.adoc   wave2-wyse.adoc
wave3-windows.adoc       wave4-macos.adoc        wave5-ios.adoc
pwsh-rsat-discovery.adoc pwsh-gpo-analytics.adoc

Next

  • Run weekly report to refresh migration numbers (see runbook in rendered doc)

  • d001 close mschapv2-migration before commit

Linux Research — d000/d001 Split

Split linux-research into personal (d000, full API) and team-facing (d001, GUI + CLI only) versions. First onboarding case: Dr. Shahab’s workstation. Standard applies to all future Linux onboards including David Rukiza Ntashamaje.

Changes

  • d000 (personal): Full copy with netapi CLI, DataConnect SQL, playbook automation, ISE API helpers

  • d001 (team): Stripped all API references — netapi-reference.adoc, queries.adoc, playbook/, sql/, scripts/ removed. ISE verification replaced with GUI steps + screenshot placeholders

  • Assembler: Rewritten to include team/ (ISE GUI phases) + endpoint/ (Linux CLI) + switch/ — no API sections

  • Attributes: Added {person-david} (David Rukiza Ntashamaje, Security Analyst). Added 17 missing attribute definitions (ad-domain, ad-base-dn, key-path, machine-key, wired-interface, nm-connection-wired, etc.)

  • Scripts extracted: 5 inline scripts → scripts/ with monad pattern (pasteable run + [%collapsible] source): check-endpoint-connectivity.sh, full-endpoint-validation.sh, full-rollback.sh, verify-cert-key-match.sh, verify-luks-encryption.sh

  • Stripped: dsource credential lines from phase-0-prereqs.adoc and linux-research-playbook.adoc

  • Troubleshooting: Replaced netapi ise mnt session / dc auth-history with ISE Live Logs GUI instructions

Build & Verify

d000 — personal (full API, netapi, DataConnect)
build-adoc $(find data/d000/projects/linux-research -maxdepth 1 -name 'linux-research.adoc') html --variant light-cyan -a env-d001 && firefox $(find data/d000 -path '*/linux-research/output/linux-research.html') &
d001 — team (ISE GUI + Linux CLI, no API)
build-adoc $(find data/d001/projects/linux-research -maxdepth 1 -name 'linux-research.adoc') html --variant light-cyan -a env-d001 && firefox $(find data/d001 -path '*/linux-research/output/linux-research.html') &
d001 — zeroed fallback (safe to share, no operational data)
build-adoc $(find data/d001/projects/linux-research -maxdepth 1 -name 'linux-research.adoc') html --variant light-cyan && firefox $(find data/d001 -path '*/linux-research/output/linux-research.html') &

Conditional Attributes — ifdef in Practice

The partials/attributes.adoc file uses AsciiDoc’s ifdef preprocessor to switch between environments at build time. The same source files produce different output depending on which flag is passed.

With -a env-d001 (production values):

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

Every {attribute} in every partial resolves to the real CHLA value: real hostnames, real IPs, real policy names, real personnel. The rendered doc is operationally useful — David can follow it step by step.

Without -a env-d001 (zeroed fallback):

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

Every {attribute} resolves to UNDEFINED or 0.0.0.0. The document structure is intact but contains zero operational data. Safe to share externally, use in presentations, or commit to public repos.

How it works inside partials/attributes.adoc:

:switch-name: UNDEFINED
:switch-ip: 0.0.0.0
:ise-ppan: 0.0.0.0

The ifdef block only activates when the build flag matches. The ifndef block provides safe defaults when no flag is passed. This is defense-in-depth at the rendering layer — even if someone obtains the .adoc source, building without -a env-d001 produces nothing useful.

Four-layer security model:

Layer Protection

1. Encryption

d001 close.age files at rest

2. Gitignore

data/*/.adoc blocked from tracking

3. Conditional rendering

ifdef::env-d001[] — production values require explicit build flag

4. Scrub rules

Pre-push grep audit catches leaks

Verification

  • Zero netapi, dc-run-sql, dsource references in rendered HTML

  • Zero UNDEFINED values with -a env-d001

  • Zero unresolved {attribute} literals — all 17 missing definitions added

  • 1.6M HTML — ISE GUI + Linux CLI + switch + troubleshooting

Next

  • Capture ISE GUI screenshots for placeholder images

  • Onboard David’s workstation using this standard

  • Update data/d000/projects/linux-research/ attributes for env-d000 homelab values

.zshrc Cleanup — dots-quantum

.zshrc Cleanup — dots-quantum

Full rewrite of ~/atelier/_projects/personal/dots-quantum/zsh/.zshrc. 1,804 → ~1,250 lines (728 insertions, 1,241 deletions). Zero behavior change except bug fixes.

Bugs Fixed
Location Bug Fix

deltadiff

Alias with $1 $2 — aliases can’t take positional args; expanded to nothing at definition time

Converted to function: deltadiff() { diff -u "$1" "$2" | delta }

tlog

eval "$@" re-parses command string, breaks on special characters (quotes, pipes, globs)

Changed to "$@" — direct execution, no re-parsing

extract()

Unquoted $1 throughout — breaks on filenames with spaces

All $1"$1"

dsh()

Unquoted $1

"$1"

domus-check()

cd without subshell — Ctrl+C leaves you in a random repo directory

output=$(cd "$dir" && make 2>&1) — subshell per repo, working dir preserved

Dead Code Removed
  • Duplicate LANG/LC_ALL exports (lines 53-54, identical to 23-25)

  • Duplicate history config (lines 215-226, overridden by 1082-1095 with 1M setting)

  • Duplicate keybindings (lines 765-776, repeated at 1057-1075 as "FINAL")

  • 30 lines commented-out zsh-autocomplete config (disabled plugin)

  • WSL block (native Arch/Hyprland — not used)

  • Principia capture functions (capture-ssh, capture-cmd, capture-encrypt, capture-conv, plog) — migrated to domus-captures

Architecture Improvements
d000/d001 Consolidation

~270 lines of near-identical code → one _dx_ops() parameterized helper (~150 lines). d000() and d001() are now thin wrappers: d000() { _dx_ops d000 "$@" }.

Differences preserved:

  • d001 excludes output/ in find queries

  • d001 close creates /tmp backup before destructive operations

  • All subcommands (build, open, close, open-file, close-file, list) work identically

Source Block Consolidation

12 repetitive if [ -f …​ ]; then source …​; fi blocks → loop over array:

local -a _shell_modules=(aliases.sh atelier.shell ollama.sh ...)
for _mod in "${_shell_modules[@]}"; do
    [[ -f "$HOME/.config/shell/$_mod" ]] && source "$HOME/.config/shell/$_mod"
done
PATH Consolidation

5 scattered export PATH=…​ additions gathered into the main typeset -U path array at the top of the file. typeset -U ensures no duplicates from re-sourcing.

Clipboard Integration

vi-yank-to-clipboard now checks wl-copy first (Wayland native) before falling back to xclip/pbcopy. Previous version checked xclip first.

Restow Command
dq-stow -R zsh

Then open a new terminal to test. If broken, restore:

cd ~/atelier/_projects/personal/dots-quantum
git checkout -- zsh/.zshrc
dq-stow -R zsh
Shell Banner Redesign

64 tools across 15 categories in a box-drawn frame with dynamic padding. Visible-length tracking prevents ragged right edge from ANSI escape codes.

Key design decisions
  • $'\033[36m' not '\033[36m' — escape bytes at assignment, not interpretation time

  • _row() helper takes visible char count, pads with %${pad}s "" to exact width

  • _cat() computes 10 + 3 + cmd_vis_len for label + separator + content

  • Live secrets state: shows d000/dev when loaded, unloaded when not

  • domus-audio.sh and domus-media.sh removed from source list — files don’t exist

Test Checklist
  • New terminal opens without errors

  • Welcome banner displays correctly — right edge aligned

  • d000 list works

  • d001 list works

  • d001 close <project> creates backup and encrypts

  • tlog echo "test" works without eval issues

  • deltadiff file1 file2 works (now a function)

  • domus-check runs without changing your working directory

  • alias | ./scripts/alias-viewer.sh --stdin -c shows same 318 aliases

  • ds d000 dev/network loads secrets

  • dsu clears secrets

Stow Inventory Audit

Audit of all 49 stow packages in dots-quantum/. Only 10 are actively stowed on this machine.

Table 6. Stowed (10 packages — active symlinks)
Package Files

bash

4

chromium

1

fd

1

fzf

1

mimeapps

1

oh-my-posh

1

ripgrep

1

vim

4

w3m

3

zsh

3

Table 7. Partial (4 packages — some links, some missing)
Package Status Notes

aider

2/3 linked

1 file not stowed

claude

1/7598 linked

Only top-level config needed — rest is cache/data

git

1/2 linked

Investigate which file is missing

ssh

2/10 linked

Expected — .ssh/config.age tracked, plaintext stowed separately

Table 8. Not Stowed (35 packages)
Package Files Action

shell

19

Investigate~/.config/shell/*.sh loaded by .zshrc but not stowed. How are they deployed?

bin

78

Investigate~/.local/bin/ tools. Direct symlinks or separate stow?

systemd

22

Investigate — user units. Are they deployed via systemctl --user?

tmux

40

Investigate — tmux config. Direct copy or separate mechanism?

hyprland

17

Machine-specific — Hyprland not on WSL

kitty

18

Machine-specific — may not be on all machines

waybar

11

Machine-specific — Wayland compositor bar

hosts

5

Requires root — cannot stow to /etc/hosts

secrets

5

Likely deployed via dsec or manual placement

opencode

770

Likely bundled app — not traditional stow target

eww

2

Wayland widget — machine-specific

mako

2

Wayland notifications — machine-specific

wofi

3

Wayland launcher — machine-specific

fastfetch

12

Likely stowed on desktop, not WSL

cava

9

Audio visualizer — machine-specific

btop

2

Optional — may use default config

aerc

4

Email client — evaluate if in use

ghostty

1

Terminal — evaluate if in use

himalaya

1

Email client — evaluate if in use

jj

1

Jujutsu VCS — evaluate if in use

lazygit

1

Git TUI — evaluate if in use

glow

1

Markdown renderer — evaluate if in use

htop

1

Optional — may use btop instead

gpg

2

Likely deployed separately for security

libvirt

1

KVM config — machine-specific

thunar

2

File manager — desktop-specific

vscodium

3

Editor — evaluate if in use

wezterm

1

Terminal — evaluate if in use

zathura

1

PDF viewer — evaluate if in use

nvim-portable

1

Neovim portable config — evaluate

powershell

1

WSL-specific — may only be on work machine

fish

1

Fish shell — evaluate if in use

.claude

1

Claude config — separate from claude package

share

48

XDG share data — investigate contents

Action Items
  • Investigate shell + bin — 97 files not stowed but actively used. Are they deployed via a separate mechanism? If not, stow them: dq-stow shell bin

  • Investigate git partial — which file is missing its symlink?

  • Investigate tmux — 40 files not stowed. Is tmux config deployed differently?

  • Investigate systemd — 22 user units. Are they enabled via systemctl --user enable?

  • Review WSL needs — which packages should be stowed on work WSL vs home Arch?

  • Prune dead packages — fish, himalaya, aerc, ghostty, wezterm — still in use?

  • Create machine manifest — per-machine list of which packages to stow (e.g., machines/modestus-aw.txt, machines/wsl-chla.txt)

Implementation: Stow Missing Critical Packages
cd ~/atelier/_projects/personal/dots-quantum

# Dry run — see what would be linked
stow -n -v shell 2>&1 | head -20
stow -n -v bin 2>&1 | head -20
stow -n -v tmux 2>&1 | head -20

# If clean, stow them
dq-stow shell
dq-stow bin
dq-stow tmux
Backout: Unstow If Something Breaks
# Unstow a package (removes symlinks, leaves originals in dots-quantum)
dq-stow -D shell
dq-stow -D bin
dq-stow -D tmux

# Nuclear: restore .zshrc specifically
cd ~/atelier/_projects/personal/dots-quantum
git checkout -- zsh/.zshrc
dq-stow -R zsh
Backout: Full .zshrc Rollback

If the rewrite itself is broken (not just stow):

# 1. Revert the commit
git -C ~/atelier/_projects/personal/dots-quantum log --oneline -5
git -C ~/atelier/_projects/personal/dots-quantum revert HEAD

# 2. Reload
source ~/.zshrc

# 3. Or hard reset to previous commit (destructive — loses all changes since)
git -C ~/atelier/_projects/personal/dots-quantum reset --hard HEAD~1

find Time Patterns — File Search Across Linux Systems

The Problem

Two different questions, two different find invocations:

  • Last 24 hours — rolling window from now, crosses midnight

  • Today since midnight — calendar day, stops at 00:00:00

Confusing these is the most common find mistake. The -mtime flag counts in 24-hour periods; -newermt compares against a timestamp string.

Last 24 Hours — -mtime -1

find . \
  -path './.git' -prune -o \
  -type f -mtime -1 \
  -printf '%TY-%Tm-%Td %TH:%TM  %p\n' |
sort -r |
head -50

-mtime -1 means "modified less than 1 day (24h) ago."
-path './.git' -prune -o eliminates .git/ without descending — critical in repos with thousands of pack objects.

Today Since Midnight — -newermt

First, understand what date gives you:

echo "now:      $(date)"
echo "today:    $(date -d today)"
echo "tomorrow: $(date -d tomorrow)"

Then bracket the calendar day:

find . \
  -path './.git' -prune -o \
  -type f \
  -newermt "$(date +%F) 00:00:00" \
  ! -newermt "$(date -d tomorrow +%F) 00:00:00" \
  -printf '%TY-%Tm-%Td %TH:%TM  %p\n' |
sort -r

The pattern: -newermt START ! -newermt END creates a time window.
$(date +%F) outputs YYYY-MM-DD — works on RHEL, Arch, Debian, Alpine.

-newermt is a GNU extension. On macOS or BusyBox, use -newer with a reference file, or gfind from coreutils.

Everything Modified Recently (Epoch Sort)

find . \
  -path './.git' -prune -o \
  -type f \
  -printf '%T@ %TY-%Tm-%Td %TH:%TM  %p\n' |
sort -nr |
head -40 |
cut -d' ' -f2-

%T@ prints the epoch timestamp (seconds since 1970), which sort -nr orders numerically in reverse.
cut -d' ' -f2- strips the epoch column, leaving only the human-readable date and path.

This is the most portable "what changed recently?" command — no date math, no -newermt, just raw sort on modification time.

Git Alternatives — When You’re Inside a Repo

When the question is "what changed?" rather than "what’s new on disk?", Git answers faster:

# Working tree status (staged + unstaged + untracked)
git status --short

# Tracked files with content changes (unstaged)
git diff --name-only

# Files changed in the latest commit
git show --name-only --pretty='format:' HEAD

git status --short is almost always what you want inside a repo.
find matters when you’re outside a repo, across multiple repos, or on systems where files arrive outside Git (logs, uploads, cron output, RHEL package updates).

When to Use Which

Question Tool

What changed in the last 24 hours?

find -mtime -1

What changed today (calendar day)?

find -newermt "$(date +%F) 00:00:00"

What are the most recently modified files?

find -printf '%T@…​' | sort -nr

What did I change in this repo?

git status --short / git diff --name-only

What did the last commit touch?

git show --name-only --pretty='format:' HEAD

What changed across multiple repos?

find ~/projects -name '.git' -prune -o -type f -mtime -1 -print

Improved Patterns — Tested 2026-06-23

Every command below was run against domus-captures and verified.

Multi-Prune — Skip Noise Directories

The single -path './.git' -prune is a starting point. Real repos have node_modules, pycache, .claude — prune them all in one predicate group:

find . \
  \( -path './.git' -o -path '*/.git' -o -path '*/node_modules' \
     -o -path '*/__pycache__' -o -path '*/.claude' \) -prune \
  -o -type f -mtime -1 \
  -printf '%TY-%Tm-%Td %TH:%TM  %p\n' |
sort -r | head -20

The \( …​ \) groups multiple -path tests with -o (OR). The final -o after -prune means "otherwise, apply these tests."
Without the grouping parentheses, operator precedence makes -prune bind wrong and you get .git contents in output.

Extension Filtering — find + awk in One Pass

Instead of adding -name '.adoc' -o -name '.sh' -o -name '*.yaml' (verbose, error-prone with -o precedence), pipe to awk:

find . -path './.git' -prune -o \
  -type f -mtime -1 \
  -printf '%TY-%Tm-%Td %TH:%TM  %p\n' |
awk '/\.(adoc|md|yaml|yml|sh)$/' |
sort -r | head -20

Why awk over grep: awk’s regex is applied to the full line — no need for grep -E. The pattern /\.(adoc|md|yaml|yml|sh)$/ anchors to end-of-line, matching the filename at the end of -printf’s `%p.

Size + Time — See What’s Large and Recent
find . -path './.git' -prune -o \
  -type f -mtime -1 \
  -printf '%TY-%Tm-%Td %TH:%TM  %8s  %p\n' |
sort -r | head -20

%8s right-pads the file size to 8 columns — bytes aligned for scanning. This reveals when an encrypted .age file is unexpectedly large (re-encryption bloat) or when a build artifact leaked into the tree.

Minute Precision — -mmin

-mtime counts in 24-hour chunks. -mmin counts in minutes:

find . -path './.git' -prune -o \
  -type f -mmin -120 \
  -printf '%TY-%Tm-%Td %TH:%TM  %p\n' |
sort -r | head -20

-mmin -120 = modified in the last 2 hours.
Use case: "what did I touch during this Claude session?"

Epoch Sort Without cut — awk Replaces a Pipe Stage

The original uses cut -d' ' -f2- to strip the epoch. awk does it without spawning another process:

find . -path './.git' -prune -o \
  -type f \
  -printf '%T@ %TY-%Tm-%Td %TH:%TM  %p\n' |
sort -nr | head -20 |
awk '{$1=""; print substr($0,2)}'

$1="" blanks the epoch field; substr($0,2) trims the leading space. One fewer fork — the difference matters when scanning 100K+ files on a remote RHEL system over SSH.

Cross-Repo Scan — Modification Count per Repo
find ~/atelier/_bibliotheca -maxdepth 1 -type d |
while read -r repo; do
  count=$(find "$repo" -path '*/.git' -prune -o -type f -mtime -1 -print 2>/dev/null | wc -l)
  [ "$count" -gt 0 ] && echo "$count  $repo"
done | sort -nr

Shows which repos had activity in the last 24 hours. On RHEL at work, point this at /opt/ or /var/log/ to see which services are writing.

stat-Based — RHEL 7 Portability

If -printf format differs across findutils versions, stat is the fallback:

find . -path './.git' -prune -o \
  -type f -mtime -1 \
  -exec stat --format='%y  %n' {} \; 2>/dev/null |
awk '{print $1, $2, $NF}' |
sort -r | head -15

stat --format='%y' gives human-readable mtime. -exec stat …​ {} \; forks once per file — slower than -printf, but works everywhere GNU coreutils is installed (RHEL 7/8/9, Ubuntu, Debian).

fd — Modern Alternative (Already Installed)

fd respects .gitignore by default and has cleaner syntax:

fd --type f --changed-within 1d --exclude .git .

No -printf, no -prune, no sortfd handles all of it. Trade-off: fd is not on RHEL by default. Install via cargo install fd-find or EPEL. On your Arch workstation, it’s already at /usr/bin/fd. On work RHEL boxes, find is the universal tool.

RHEL/Enterprise Notes

  • RHEL 7+ ships GNU findutils — -newermt and -printf both work.

  • SELinux contexts don’t affect find output unless you add -ls or -printf '%Z'.

  • For audit trails on RHEL: ausearch -ts today -f /path gives kernel-level file access, not just modification.

  • On systems with locate installed: locate -S shows database age; updatedb refreshes. Faster than find for name searches across the entire filesystem, but doesn’t track modification time.

  • RHEL 9 ships findutils 4.8 — all features here work. RHEL 7 ships 4.5 — -newermt works, -printf works, -empty works. No version gating needed.

tmux-quantum Switchover — Replace tmux-config on Second Workstation

This runbook switches a workstation from tmux-config to tmux-quantum. Both machines share the same directory structure (~/atelier/_projects/personal/).

Current State (This Machine — Reference)

~/.config/tmux        → ~/atelier/_projects/personal/tmux-quantum    (symlink)
~/.config/tmuxinator  → ~/.config/tmux/sessions                      (symlink)
~/atelier/_projects/personal/tmux-config/                             (archived, not deployed)

Pre-Flight — Verify on the Other Machine

# What's currently deployed?
file ~/.config/tmux
ls -la ~/.config/tmux

# Is it a symlink to tmux-config, or a direct clone?
readlink -f ~/.config/tmux

# Confirm directory structure matches
ls ~/atelier/_projects/personal/tmux-config

If ~/.config/tmux points to tmux-config or IS the tmux-config repo, proceed. If it’s something else entirely, investigate before overwriting.

Step 1 — Back Up Running Config

# Save current tmux.conf in case you need to roll back
cp ~/.config/tmux/tmux.conf /tmp/tmux.conf.bak

# If you have a local.conf with machine-specific bindings, save it
cp ~/.config/tmux/local.conf /tmp/local.conf.bak 2>/dev/null

# Capture current tmux server state
tmux list-sessions 2>/dev/null
tmux show-options -g > /tmp/tmux-options-before.txt 2>/dev/null

Step 2 — Clone tmux-quantum

git clone git@github.com:EvanusModestus/tmux-quantum.git \
  ~/atelier/_projects/personal/tmux-quantum
# Remove old symlink (or unlink if it's a symlink)
rm ~/.config/tmux

# If it was a directory (not a symlink), move it instead:
# mv ~/.config/tmux ~/.config/tmux-old

# Create new symlink
ln -sf ~/atelier/_projects/personal/tmux-quantum ~/.config/tmux

# Link tmuxinator sessions
ln -sf ~/.config/tmux/sessions ~/.config/tmuxinator

Step 4 — Machine-Specific local.conf

tmux-quantum gitignores local.conf — this is where per-machine bindings go.

# Start from the example
cp ~/.config/tmux/local.conf.example ~/.config/tmux/local.conf

# Edit for this machine — paths, display size, etc.
nvim ~/.config/tmux/local.conf

If you backed up a local.conf from the old config, review it — don’t blindly copy. Keybinding tables may differ between tmux-config and tmux-quantum.

Step 5 — Install TPM Plugins

# Bootstrap TPM if not already present
test -d ~/.config/tmux/plugins/tpm || \
  git clone https://github.com/tmux-plugins/tpm ~/.config/tmux/plugins/tpm

# Reload config
tmux source-file ~/.config/tmux/tmux.conf

# Inside tmux: prefix + I  (capital I) to install plugins

Step 6 — Verify

# Confirm symlink
readlink -f ~/.config/tmux

# Confirm config loads without errors
tmux source-file ~/.config/tmux/tmux.conf && echo "clean"

# Confirm keybindings loaded (check for vim-style navigation)
tmux list-keys | grep -E 'select-pane.*-[UDLR]'

# Confirm prefix is C-a (not C-b)
tmux show-option -g prefix

# Confirm tmuxinator sessions are visible
tmuxinator list

# Run the conflict checker
bash ~/.config/tmux/scripts/check-conflicts.sh

Step 7 — Archive tmux-config

# Don't delete — archive in place
# The repo stays at ~/atelier/_projects/personal/tmux-config
# It's just no longer deployed anywhere

# Verify it's not symlinked
file ~/.config/tmux
# Should show: symbolic link to .../tmux-quantum

Rollback (If Something Breaks)

# Point back to the old config
rm ~/.config/tmux
ln -sf ~/atelier/_projects/personal/tmux-config ~/.config/tmux
tmux source-file ~/.config/tmux/tmux.conf

Quick Reference — Differences from tmux-config

Aspect tmux-config tmux-quantum

Architecture

Single monolithic tmux.conf

9 domain modules in lib/

Keybinding conflicts

Possible (no detection)

scripts/check-conflicts.sh validates

Themes

Hardcoded hex colors

Variable-based (#{@theme-*}), 3 switchable themes

Plugin config

Scattered across files

Consolidated in plugins/tpm.conf

Sessions

Separate tmuxinator install

Sessions bundled in sessions/ (23 templates)

Machine-specific

Edit main config

Gitignored local.conf — main config untouched

Essential Tools Inventory — The Development Triad

The three repos that constitute the core development environment — the shell/desktop layer, the multiplexer, and the editor. Each is independently deployable, deliberately modular, and designed to share muscle memory across contexts.

dots-quantum — Hyprland Dotfiles Manager

Location: ~/atelier/_projects/personal/dots-quantum/
Deployment: GNU Stow — 49 packages, symlink-based
Remotes: GitHub + GitLab + Gitea (triple mirror)
History: 93 commits since 2026-03-16

Table 9. What it owns
Layer Scope

Window manager

Hyprland compositor (791-line config, 9 utility scripts: wallpaper, dj-mode, shader-toggle, snap-corner, toggle-pip, eww-toggle)

Status bar

Waybar with 9 module scripts (cpu, memory, network, weather)

Shells

zsh (68 KB — 46 commands, ~43 functions, 104 aliases), bash (48 KB), fish (44 KB)

Shared shell modules

shell/ (184 KB) — vault-wrappers, atelier aliases, data-analysis, history-vault, cli-explorer (FZF-powered library browser), bash mastery functions

Terminals

kitty, ghostty, wezterm

Editors

nvim-portable (single-file zero-plugin init for servers), aider, vscodium, opencode

CLI tools

bin/ — 69+ executables: clir/cliq/def/rae (dictionary), awklib/sedx/grp (text processing), backup scripts (borg, seagate), repo management, vault tools, project utilities

Libraries

share/ (448 KB, 48 files) — 8 AWK libraries, 10 sed scripts, 10 grep pattern files, 8 bash modules, AsciiDoc themes

System services

systemd timers — 7-timer accountability: pacman-news, project-stale, dotfiles-sync, daily-audit, security-remediation, cert-expiry-monitor, weekly-review

Desktop

wofi (launcher), mako (notifications), eww (widgets)

Monitoring

btop (Catppuccin), fastfetch (12 presets), htop, cava

Secrets

ssh/ + gpg/ + hosts/ + secrets/ — gitignored, age-encrypted

Stow deployment model
  • Public packages (18 default): bash, git, hyprland, waybar, wofi, mako, kitty, ghostty, fish, oh-my-posh, btop, lazygit, fastfetch, fzf, ripgrep, fd, bin, opencode

  • Private packages (local-only, never committed): ssh, gpg, hosts, secrets

  • Local overrides: ~/.config/hypr/local.conf, ~/.bashrc.local

dots-quantum’s tmux/ package contains only tmuxinator sessions — the actual tmux config is owned by tmux-quantum.

tmux-quantum — Library-Style Multiplexer

Location: ~/atelier/_projects/personal/tmux-quantum/
Deployment: Direct symlink (ln -sf~/.config/tmux/)
Remote: GitHub
History: 8 commits, stable
Supersedes: tmux-config (archived, not deployed)

Table 10. Architecture — one domain, one file
Module Responsibility

lib/core.conf

Terminal, prefix (C-a), base settings — no keybindings

lib/vim.conf

Pane/window navigation (h/j/k/l), copy mode (Enter, v/V/C-v, y)

lib/mouse.conf

Click, scroll, resize

lib/clipboard.conf

Platform-detected: wl-copy (Wayland) / xclip (X11)

lib/panes.conf

Splits (|/-), resize (H/J/K/L), zoom (z), mark (m/M), swap (~), layouts (M-1..5), sync (S)

lib/sessions.conf

Session management (N=new, X=kill, w/W=choose tree), nested tmux (F10)

lib/popups.conf

Popup workflows: ` =terminal, g=lazygit, e=yazi, C-f=fzf picker, n=scratchpad, /=help, Space=thumbs

lib/status.conf

Status bar via #{@theme-*} variables

lib/modern.conf

tmux 3.2+ features

Themes (variable-based, no hardcoded hex)
  • aethelred — purple gradient (active)

  • solar-flare — warm orange

  • tokyo-night — cool blue

Plugins (6 via TPM)

tmux-resurrect, tmux-continuum, tmux-yank, tmux-open, tmux-prefix-highlight, tmux-thumbs, tmux-sessionist

Session templates (23 tmuxinator YAML)

Prefixed by domain: cfg- (6), proj- (7), lrn- (2), lab- (1), data- (1), daily- (1)

Integrity guarantees
  • scripts/check-conflicts.sh — verifies no same-table keybinding collisions across all lib/ modules

  • All plugin config consolidated in plugins/tpm.conf — no scatter

  • Claude Code integration: rules, validator agent, /tmux-qa skill

domus-nvim — Full IDE Configuration

Location: ~/atelier/_projects/personal/domus-nvim/
Deployment: Symlink or NVIM_APPNAME
Remote: GitHub
History: 72 commits since 2026-02-12
Requires: Neovim 0.11.0+

Table 11. Plugin inventory (88 total via lazy.nvim)
Category Key Plugins

Editor fundamentals (14)

treesitter + textobjects + context, autopairs, Comment.nvim, mini.ai + mini.surround, flash.nvim, rainbow-delimiters, todo-comments, undotree

UI/Visual (15)

Catppuccin Mocha (primary), lualine, indent-blankline, noice + nvim-notify, alpha-nvim (dashboard), which-key, zen-mode, screenkey (teaching)

Coding/LSP/Completion (17)

mason + mason-lspconfig, nvim-lspconfig, blink.cmp (completion), LuaSnip, conform (formatting), nvim-lint, glance (peek), refactoring, copilot + CopilotChat, cloak (secret hiding)

Git (4)

vim-fugitive, gitsigns, lazygit.nvim, diffview

Navigation/Tools (11)

telescope + fzf-native + extensions, harpoon, oil, toggleterm, auto-session

Language-specific (14)

iron.nvim (Python REPL), markdown-preview, cisco.vim, junos.vim, rainbow_csv, vim-dadbod (database)

Debugging (4)

nvim-dap + UI + virtual-text + mason-nvim-dap

LSP servers (13 via mason)

lua_ls, pyright, rust_analyzer, ts_ls, html, cssls, jsonls (SchemaStore), yamlls (kubernetes), bashls, dockerls, ansiblels, marksman, taplo, clangd

Formatters

stylua (Lua), ruff (Python), prettier (JS/TS/JSON/YAML/HTML/CSS), shfmt (shell), rustfmt, clang-format, sqlfluff, taplo

Linters

ruff, eslint_d, selene, markdownlint, yamllint, jsonlint, hadolint, golangci-lint, tflint

Keybinding philosophy
  • Leader: <Space>

  • Vim-standard LSP bindings (gd, gr, K)

  • Harpoon for rapid file switching (<Space>1-4)

  • Telescope for fuzzy everything (<Space>ff/fg)

  • Git via <Space>g* or LazyGit (<Space>lg)

  • DAP debugging via <F5-F12>

First-class language support
  • Python — iron.nvim REPL (ipython), ruff formatting + linting, pyright type checking

  • Rust — rust-analyzer with clippy on save, inlay hints

  • AsciiDoc — custom ftplugin, linting, 15 exotic syntax snippets

  • C/C++ — clangd LSP, clang-format, codelldb DAP

  • SQL — vim-dadbod with UI + completion

Relationship to dots-quantum
  • nvim-portable in dots-quantum = minimal single-file init for servers (zero plugins, same muscle memory)

  • domus-nvim = full IDE for workstations (88 plugins, 13 LSPs, DAP)

The Triad — How They Interlock

Aspect dots-quantum tmux-quantum domus-nvim

Deployment

GNU Stow (49 packages)

Direct symlink

Symlink / NVIM_APPNAME

Owns

Shell, desktop, CLI tools, system services

Multiplexer config, session templates

Editor plugins, LSP, completion, debugging

Theme

Catppuccin Mocha (btop, waybar, mako)

Variable-based (@theme-*), 3 schemes

Catppuccin Mocha (primary)

Secret model

age-encrypted + gitignored packages

local.conf gitignored

cloak.nvim hides inline secrets

Claude integration

Stow rules, settings

Rules, validator agent, /tmux-qa skill

.claude/settings.local.json

Overlap

tmuxinator sessions only (tmux/ package)

None — standalone

None — standalone

Key insight

The three repos share no configuration overlap. dots-quantum owns everything except ~/.config/tmux/ (tmux-quantum) and ~/.config/nvim/ (domus-nvim). Each can be updated, tested, or rebuilt independently without affecting the others.

CAB Rehearsal — Abnormal Security CR-2026-06-25

Dispositio — CAB Presentation Structure

Exordium (Opening — 30 seconds)

"Good morning. CR-2026-06-25 — Abnormal Security tester group expansion. We’re requesting approval to add server team and all IS staff to the pilot group that’s been running for six weeks with zero incidents. No policy changes, no mail flow changes, no downtime. We’re adding people to a group."

Narratio (Context — 60 seconds)

"We’re migrating from Cisco ESA to Abnormal Security for email threat detection. Abnormal is API-based — it connects to M365 via Graph API and analyzes email behavior without changing mail flow. It doesn’t sit inline like ESA did.

For the past six weeks, 21 InfoSec team members have operated under four Microsoft Defender policies scoped to a single Exchange security group — Abnormal-Pilot-Users. Anti-spam, safe links, safe attachments, and an external sender subject modification rule. All four policies are governed by group membership. Adding someone to the group turns on protection. Removing them turns it off."

Propositio (The Ask — 20 seconds)

"We’re asking to expand this group in two phases:

  • Wednesday 06-25, 09:00: Add server team members

  • Thursday 06-26, 09:00: Add all IS staff

Jason Landeros executes the PowerShell. Carlos Sandoval and Alejandro Mejia monitor for 24 hours after each phase. Tyler Horsford from Abnormal is on-call for both windows."

Confirmatio (Evidence — 90 seconds)

Use these proof points. Don’t rush — each one is a complete sentence:

  • "Six-week pilot. 21 users. Zero incidents. Zero false positive escalations."

  • "No policy changes — we’re adding members to an existing group. The four Defender policies are already live and scoped."

  • "No mail flow changes — ESA remains inline until MX cutover, which is a separate CR."

  • "No downtime — group membership propagation takes 15-30 minutes. Users don’t notice."

  • "Five-minute rollback — remove newly added members from the group. Policies revert immediately."

  • "During the pilot, we identified and mitigated a ghost-sender vulnerability through a transport rule — a Direct Send exploitation via our external MX that was allowing spoofed internal messages."

  • "We also identified DKIM is disabled across all domains — that’s a separate remediation item, not part of this CR."

Peroratio (Close — 15 seconds)

"Low-risk expansion of a proven pilot. Group membership change only. Rollback is instant. Requesting approval for Wednesday and Thursday implementation windows."

Anticipated Questions & Answers

Question Answer

"What happens if Abnormal flags legitimate email?"

"Abnormal is in read/write mode — it can auto-remediate. But we have 24-hour monitoring by Carlos and Alejandro after each phase. If we see false positives, we remove the affected users from the group within 5 minutes and they revert to ESA-only protection."

"What’s the rollback?"

"Remove the newly added members from Abnormal-Pilot-Users via PowerShell. Takes under 5 minutes. Policies stop applying to those users immediately. No configuration changes needed."

"Why two phases instead of all at once?"

"Risk containment. Server team is a smaller group — if we see any issues Wednesday, we don’t expand Thursday. If Wednesday is clean, Thursday is low risk."

"Does this change mail flow?"

"No. ESA remains inline — mail still flows through the Cisco MX. Abnormal connects via M365 Graph API and analyzes post-delivery. The MX cutover is a separate CR with a separate timeline."

"What about the Graph API permissions?"

"Already approved in the May 2026 CR. No permission changes in this CR — we’re only adding group members."

"How do you know 21 users is enough to validate?"

"We ran for six weeks across InfoSec — security analysts, engineers, and management. That’s the team most likely to receive targeted phishing and BEC. Zero incidents across that attack surface is strong evidence."

"What did you find during the pilot?"

"Three investigations: a ghost-sender vulnerability we mitigated with a transport rule, a spam bypass via user safe-sender lists (SCL override), and a Hoxhunt simulation delivery that we confirmed as intentional. All documented."

"Is Abnormal replacing ESA completely?"

"Yes, eventually. But ESA stays inline until MX cutover — that’s a separate CR after full org rollout. This CR only expands the pilot group."

"What if someone complains about missing email?"

"We run a message trace in Exchange Online to verify delivery status. If the message was quarantined by one of the four policies, we release it and add a policy exception if warranted. Carlos has the message trace commands ready."

"Who owns this after implementation?"

"InfoSec. Carlos Sandoval handles day-to-day Abnormal operations. I handle policy and integration. Sarah Clizer has executive oversight."

Your Strengths Walking In

You are not "new to this." In six weeks you:

  • Built 5 Defender policy templates from scratch (anti-spam, anti-phish, anti-malware, safe attachments, safe links)

  • Discovered DKIM disabled across all domains — nobody else caught it

  • Investigated and mitigated a ghost-sender exploit (InfoGuard Labs research → transport rule deployment)

  • Ran three email security investigations (ghost-sender, spam bypass, Hoxhunt)

  • Wrote 907 lines of CR documentation with full PowerShell implementation, pre/post validation, and rollback

  • Mapped the entire ESA → EOP/MDO policy migration path

Network engineers understand authentication, trust chains, and policy enforcement. Email security is the same pattern — different protocol, same logic. You belong in that room.

Workflow addendum 2026-06-23 15:59

The dispositio summary needed revision. The workflow below documents two methods for performing a multi-line block replacement in an AsciiDoc file: one using Neovim, the other using sed with a heredoc. Both follow the same lifecycle: identify → backup → implement → verify → rollback.

Locate the target

Use grep -rn to return both the filename and line number in a single pass.

grep -rn '=== Dispositio' data/d001/proj*/abnor*
Output
data/d001/projects/abnormal-security/partials/cr-tester-expansion-2026-06-18.adoc:156:=== Dispositio

Assign the path to a variable to avoid repetition in every subsequent command. The find filter excludes .bak files and constrains the match to partials/ to guarantee a single result.

TARGET=$(find data/d001/proj*/abnor* -name 'cr-tester-ex*.adoc' ! -name '*.bak' -path '*/partials/*')
echo "$TARGET"
Identify the line range

Print from the === Dispositio heading through the closing ==== delimiter to confirm the exact block boundaries. AsciiDoc admonition blocks use ==== for both open and close, so sed range addressing stops on the first match. awk with a delimiter counter handles this correctly.

awk 'NR==156{found=0} NR>=156{print; if(/^====$/) found++; if(found==2) exit}' "$TARGET"
Output
=== Dispositio

[TIP]
====
Abnormal Security has been running in read/write mode for six weeks alongside Microsoft Defender for Office 365. Twenty-one pilot users across InfoSec and two server team members — the people most likely to be targeted by BEC and credential phishing. The platform performed exactly as expected throughout the pilot.

All protection is governed by a single Exchange security group. Four Defender policies — anti-spam, safe links, safe attachments, and the external sender modification rule — are scoped to that group. This change adds members to the group. That's it. No policy modifications, no mail flow changes, no configuration drift, no downtime.

We expand in two phases: remaining server team on Wednesday, all of Information Services on Thursday. Each phase gets 24 hours of monitored validation before the next. If anything surfaces, we remove the newly added members — policies revert within minutes. The pre-change state is captured as a JSON export for audit traceability.

This is a group membership change with instant rollback. The architecture makes it low-risk by design.
====
Backup

Create a backup before any modification. This is the rollback point regardless of method.

cp "$TARGET"{,.bak}
Implement — Method A: Neovim

Open the file at the target line.

v +156 "$TARGET"

Inside Neovim, get the ending line number and delete the range:

:156                       " jump to line 156
/^====$/                   " search forward — lands on opening ====
n                          " advance to closing ====
:.                         " note this line number (e.g. 176)
:156,176d                  " delete the entire block
O                          " open line above cursor — paste revised content
Implement — Method B: sed with heredoc

Write the replacement block to a temp file.

cat > /tmp/dispositio.adoc << 'EOF'
=== Dispositio

[TIP]
====
We've successfully piloted Abnormal Security alongside Microsoft Defender for Office 365 for six weeks with the team most likely to face targeted attacks — our InfoSec staff and two server team members. The platform delivered exactly what we expected, and we're ready to expand.

The rollout is straightforward. All protection runs through a single Exchange security group with four scoped Defender policies (anti-spam, safe links, safe attachments, and external sender modification). We're simply adding members to this existing group — no policy changes, no mail flow adjustments, no configuration drift, and no downtime.

We'll move in two phases:

* *Wednesday* — remaining server team
* *Thursday* — all of Information Services

Each phase gets 24 hours of monitored validation before the next. If anything surfaces, we remove the new members and revert within minutes — the protection policies remain unchanged, so there's no ripple effect. We've captured the pre-change state as a JSON export for audit traceability.

This is a group membership change with instant rollback baked into the design. The architecture itself makes it low-risk.
====
EOF

Get the ending line number dynamically, then delete and insert in a single pass.

END=$(awk 'NR>=157 && /^====$/{ if(++n==2){print NR; exit} }' "$TARGET")
sed -i -e "156,${END}d" -e "155r /tmp/dispositio.adoc" "$TARGET"

The awk one-liner counts ==== delimiters starting after the heading (line 157) and returns the line number of the second match — the closing delimiter. The two sed -e expressions execute in a single pass: the first deletes the range, the second reads the replacement file after line 155.

Verify

Diff the dispositio block between the backup and the working copy using process substitution. This scopes the comparison to only the modified block without generating intermediate files.

diff <(awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}' "$TARGET".bak) \
     <(awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}' "$TARGET")

If the file is tracked in git and has been committed at least once, the backup can be replaced with the last committed version:

diff <(git show HEAD:"$TARGET" | awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}') \
     <(awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}' "$TARGET")

Process substitution (<(…​)) feeds each awk output as a virtual file descriptor to diff, eliminating temp files. The .bak method works anywhere; the git show variant is available once the file has at least one commit.

Confirm the replacement block visually:

awk 'NR==156{found=0} NR>=156{print; if(/^====$/) found++; if(found==2) exit}' "$TARGET"
Rollback

If the diff reveals unintended changes, restore from backup.

cp "$TARGET"{.bak,}

If the file is git-tracked:

git checkout -- "$TARGET"
Clean up

Once the change is confirmed, remove the backup and temp file.

rm "$TARGET".bak /tmp/dispositio.adoc
Include created using the below workflow
tee docs/modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-23/dispositio-revision.adoc << 'ADOC'
==== Workflow addendum 2026-06-23 15:59

The dispositio summary needed revision. The workflow below documents two methods for performing a multi-line block replacement in an AsciiDoc file: one using Neovim, the other using sed with a heredoc. Both follow the same lifecycle: *identify → backup → implement → verify → rollback*.

===== Locate the target

Use `grep -rn` to return both the filename and line number in a single pass.

[source,bash]

grep -rn '=== Dispositio' data/d001/proj*/abnor*

.Output
[source,console]

data/d001/projects/abnormal-security/partials/cr-tester-expansion-2026-06-18.adoc:156:=== Dispositio

Assign the path to a variable to avoid repetition in every subsequent command. The `find` filter excludes `.bak` files and constrains the match to `partials/` to guarantee a single result.

[source,bash]

TARGET=$(find data/d001/proj*/abnor* -name 'cr-tester-ex*.adoc' ! -name '.bak' -path '/partials/*') echo "$TARGET"

===== Identify the line range

Print from the `=== Dispositio` heading through the closing `====` delimiter to confirm the exact block boundaries. AsciiDoc admonition blocks use `====` for both open and close, so `sed` range addressing stops on the first match. `awk` with a delimiter counter handles this correctly.

[source,bash]

awk 'NR==156{found=0} NR>=156{print; if(/^====$/) found++; if(found==2) exit}' "$TARGET"

.Output
[source,asciidoc]

Dispositio

Abnormal Security has been running in read/write mode for six weeks alongside Microsoft Defender for Office 365. Twenty-one pilot users across InfoSec and two server team members — the people most likely to be targeted by BEC and credential phishing. The platform performed exactly as expected throughout the pilot.

All protection is governed by a single Exchange security group. Four Defender policies — anti-spam, safe links, safe attachments, and the external sender modification rule — are scoped to that group. This change adds members to the group. That’s it. No policy modifications, no mail flow changes, no configuration drift, no downtime.

We expand in two phases: remaining server team on Wednesday, all of Information Services on Thursday. Each phase gets 24 hours of monitored validation before the next. If anything surfaces, we remove the newly added members — policies revert within minutes. The pre-change state is captured as a JSON export for audit traceability.

This is a group membership change with instant rollback. The architecture makes it low-risk by design.

===== Backup

Create a backup before any modification. This is the rollback point regardless of method.

[source,bash]

cp "$TARGET"{,.bak}

===== Implement — Method A: Neovim

Open the file at the target line.

[source,bash]

v +156 "$TARGET"

Inside Neovim, get the ending line number and delete the range:

[source,vim]

:156 " jump to line 156 /^====$/ " search forward — lands on opening ==== n " advance to closing ==== :. " note this line number (e.g. 176) :156,176d " delete the entire block O " open line above cursor — paste revised content

===== Implement — Method B: sed with heredoc

Write the replacement block to a temp file.

[source,bash]

cat > /tmp/dispositio.adoc << 'EOF' === Dispositio

We’ve successfully piloted Abnormal Security alongside Microsoft Defender for Office 365 for six weeks with the team most likely to face targeted attacks — our InfoSec staff and two server team members. The platform delivered exactly what we expected, and we’re ready to expand.

The rollout is straightforward. All protection runs through a single Exchange security group with four scoped Defender policies (anti-spam, safe links, safe attachments, and external sender modification). We’re simply adding members to this existing group — no policy changes, no mail flow adjustments, no configuration drift, and no downtime.

We’ll move in two phases:

  • Wednesday — remaining server team

  • Thursday — all of Information Services

Each phase gets 24 hours of monitored validation before the next. If anything surfaces, we remove the new members and revert within minutes — the protection policies remain unchanged, so there’s no ripple effect. We’ve captured the pre-change state as a JSON export for audit traceability.

This is a group membership change with instant rollback baked into the design. The architecture itself makes it low-risk.

EOF

Get the ending line number dynamically, then delete and insert in a single pass.

END=$(awk 'NR>=157 && /^====$/{ if(++n==2){print NR; exit} }' "$TARGET")
sed -i -e "156,${END}d" -e "155r /tmp/dispositio.adoc" "$TARGET"

The awk one-liner counts ==== delimiters starting after the heading (line 157) and returns the line number of the second match — the closing delimiter. The two sed -e expressions execute in a single pass: the first deletes the range, the second reads the replacement file after line 155.

Verify

Diff the dispositio block between the backup and the working copy using process substitution. This scopes the comparison to only the modified block without generating intermediate files.

diff <(awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}' "$TARGET".bak) \
     <(awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}' "$TARGET")

If the file is tracked in git and has been committed at least once, the backup can be replaced with the last committed version:

diff <(git show HEAD:"$TARGET" | awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}') \
     <(awk 'NR==156{f=0} NR>=156{print; if(/^====$/) f++; if(f==2) exit}' "$TARGET")

Process substitution (<(…​)) feeds each awk output as a virtual file descriptor to diff, eliminating temp files. The .bak method works anywhere; the git show variant is available once the file has at least one commit.

Confirm the replacement block visually:

awk 'NR==156{found=0} NR>=156{print; if(/^====$/) found++; if(found==2) exit}' "$TARGET"
Rollback

If the diff reveals unintended changes, restore from backup.

cp "$TARGET"{.bak,}

If the file is git-tracked:

git checkout -- "$TARGET"
Clean up

Once the change is confirmed, remove the backup and temp file.

rm "$TARGET".bak /tmp/dispositio.adoc

ADOC

// ── Work — Abnormal Security ──────────────────────

=== Abnormal Security — d000/d001 Restructure

// Partial: Abnormal Security — d000/d001 Restructure

Restructured `abnormal-security` to gold standard (STD-001/020/025). Split into personal (d000, full API) and team-facing (d001, PowerShell Exchange + GUI only).

==== Changes

* **attributes.adoc:** Created from scratch — previous file was misnamed API queries. 50+ attributes with `ifdef::env-d001[]` + zeroed fallback: personnel (9), domains (6), portals (6), policy names (6), distribution groups, phase labels
* **metadata.adoc + summary.adoc:** Created per STD-001 — phase status table, blockers (DKIM disabled, AutoForwarding on), investigation summary
* **Assembler:** 8 sections with `leveloffset=+1` — Project Overview, Policy Migration, Policy Validation, Investigations, Change Requests, Audit Output, Operational
* **Demoted:** `abnormal-security-playbook.adoc` and `esa-policy-migration-review.adoc` — stripped `= Title` headers and nested includes
* **d001 stripped:** `api-queries.adoc`, `sentinel-integration.adoc`, `abnormal-security-queries.adoc` removed (Abnormal REST API + Sentinel KQL → d000 only)
* **d000:** Full copy with all API content, encrypted at rest

==== Open / Close

[source,bash]

d001 open abnormal-security d001 close abnormal-security

[source,bash]

d000 open abnormal-security d000 close abnormal-security

==== Build & Verify

.d001 — team (PowerShell Exchange + GUI, no Abnormal API)
[source,bash]

d001 open abnormal-security build-adoc $(find data/d001/projects/abnormal-security -maxdepth 1 -name 'abnormal-security.adoc') html --variant light-cyan -a env-d001 && firefox $(find data/d001 -path '*/abnormal-security/output/abnormal-security.html') &

.d001 — zeroed fallback (safe to share)
[source,bash]

build-adoc $(find data/d001/projects/abnormal-security -maxdepth 1 -name 'abnormal-security.adoc') html --variant light-cyan && firefox $(find data/d001 -path '*/abnormal-security/output/abnormal-security.html') &

.d000 — personal (full Abnormal REST API + Sentinel KQL)
[source,bash]

d000 open abnormal-security build-adoc $(find data/d000/projects/abnormal-security -maxdepth 1 -name 'abnormal-security.adoc') html --variant light-cyan -a env-d001 && firefox $(find data/d000 -path '*/abnormal-security/output/abnormal-security.html') &

==== Verification

* Zero build errors
* Zero `UNDEFINED` with `-a env-d001`
* Zero unresolved `\{attribute}` literals
* Zero Abnormal REST API / Sentinel KQL references in d001 rendered HTML
* 1.6M HTML with structured TOC (8 top-level sections)

==== Next

* CR-2026-06-25 — tester group expansion (this week)
* Present policy migration status at meeting
* DKIM remediation — all domains `Enabled: False`
* AutoForwardingMode policy change

==== Workflow addendum 2026-06-23 15:59

The dispositio summary needed revision. The workflow below documents two methods for performing a multi-line block replacement in an AsciiDoc file: one using Neovim, the other using sed with a heredoc. Both follow the same lifecycle: *identify → backup → implement → verify → rollback*.

===== Locate the target

Use `grep -rn` to return both the filename and line number in a single pass.

[source,bash]

grep -rn '=== Dispositio' data/d001/proj*/abnor*

.Output
[source,console]

data/d001/projects/abnormal-security/partials/cr-tester-expansion-2026-06-18.adoc:156:=== Dispositio

Assign the path to a variable to avoid repetition in every subsequent command. The `find` filter excludes `.bak` files and constrains the match to `partials/` to guarantee a single result.

[source,bash]

TARGET=$(find data/d001/proj*/abnor* -name 'cr-tester-ex*.adoc' ! -name '.bak' -path '/partials/*') echo "$TARGET"

===== Identify the line range

Print from the `=== Dispositio` heading through the closing `====` delimiter to confirm the exact block boundaries. AsciiDoc admonition blocks use `====` for both open and close, so `sed` range addressing stops on the first match. `awk` with a delimiter counter handles this correctly.

[source,bash]

awk 'NR==156{found=0} NR>=156{print; if(/^====$/) found++; if(found==2) exit}' "$TARGET"

.Output
[source,asciidoc]

Dispositio

Abnormal Security has been running in read/write mode for six weeks alongside Microsoft Defender for Office 365. Twenty-one pilot users across InfoSec and two server team members — the people most likely to be targeted by BEC and credential phishing. The platform performed exactly as expected throughout the pilot.

All protection is governed by a single Exchange security group. Four Defender policies — anti-spam, safe links, safe attachments, and the external sender modification rule — are scoped to that group. This change adds members to the group. That’s it. No policy modifications, no mail flow changes, no configuration drift, no downtime.

We expand in two phases: remaining server team on Wednesday, all of Information Services on Thursday. Each phase gets 24 hours of monitored validation before the next. If anything surfaces, we remove the newly added members — policies revert within minutes. The pre-change state is captured as a JSON export for audit traceability.

This is a group membership change with instant rollback. The architecture makes it low-risk by design.

===== Backup

Create a timestamped backup before any modification. This is the rollback point.

[source,bash]

cp "$TARGET"{,.bak-$(date +%Y%m%dT%H%M%S)}

===== Implement — Method A: Neovim

Open the file at the target line.

[source,bash]

v +156 "$TARGET"

Inside Neovim, get the ending line number and delete the range:

[source,vim]

:156 " jump to line 156 /^====$/ " search forward — lands on opening ==== n " advance to closing ==== :. " note this line number (e.g. 176) :156,176d " delete the entire block O " open line above cursor — paste revised content

===== Implement — Method B: sed with heredoc

Write the replacement block to a temp file.

[source,bash]

cat > /tmp/dispositio.adoc << 'EOF' === Dispositio

We’ve successfully piloted Abnormal Security alongside Microsoft Defender for Office 365 for six weeks with the team most likely to face targeted attacks — our InfoSec staff and two server team members. The platform delivered exactly what we expected, and we’re ready to expand.

The rollout is straightforward. All protection runs through a single Exchange security group with four scoped Defender policies (anti-spam, safe links, safe attachments, and external sender modification). We’re simply adding members to this existing group — no policy changes, no mail flow adjustments, no configuration drift, and no downtime.

We’ll move in two phases:

  • Wednesday — remaining server team

  • Thursday — all of Information Services

Each phase gets 24 hours of monitored validation before the next. If anything surfaces, we remove the new members and revert within minutes — the protection policies remain unchanged, so there’s no ripple effect. We’ve captured the pre-change state as a JSON export for audit traceability.

This is a group membership change with instant rollback baked into the design. The architecture itself makes it low-risk.

EOF

Get the ending line number dynamically, then delete and insert in a single pass.

END=$(awk 'NR>=157 && /^====$/{ if(++n==2){print NR; exit} }' "$TARGET")
sed -i -e "156,${END}d" -e "155r /tmp/dispositio.adoc" "$TARGET"

The awk one-liner counts ==== delimiters starting after the heading (line 157) and returns the line number of the second match — the closing delimiter. The two sed -e expressions execute in a single pass: the first deletes the range, the second reads the replacement file after line 155.

Verify

Diff the backup against the modified file to confirm only the intended block changed.

diff "$TARGET".bak-* "$TARGET"

Confirm the replacement block visually:

awk 'NR==156{found=0} NR>=156{print; if(/^====$/) found++; if(found==2) exit}' "$TARGET"
Rollback

If the diff reveals unintended changes, restore from backup.

cp "$TARGET".bak-* "$TARGET"

Once confirmed, clean up the backup and temp file.

rm "$TARGET".bak-* /tmp/dispositio.adoc

ISE API Scripts — Inventory, Endpoint Profile, DataConnect

ISE API Scripts Reference

Shell scripts for ISE endpoint discovery, switch location, and operational stats. All require dsource credentials and helper functions loaded.

Load Prerequisites
dsource d001 dev/network/ise

Paste the ERS, MnT, and DataConnect helpers from shared partials:

cat data/shared/partials/ise-ers/helpers.adoc
cat data/shared/partials/ise-mnt/helpers.adoc
cat data/shared/partials/ise-dataconnect/helpers.adoc
Full ISE Inventory — dACLs, Profiles, Policy Sets, Rules, Sessions
source data/shared/scripts/ise-inventory.sh | tee /tmp/ise-inventory-$(date +%F).txt
ise-inventory.sh (click to expand)
Unresolved include directive in modules/ROOT/partials/worklog/daily-notes/2026-06/2026-06-23/ise-api-scripts.adoc - include::example$ise/ise-inventory-summary.sh[]

Outputs:

  • dACLs — full list with count

  • Authorization profiles — full list

  • Guest-related dACLs and authz profiles (filtered by guest|cwa)

  • Policy sets (OpenAPI) — name, state, ID

  • Authentication rules per policy set — name, state, identity store

  • Authorization rules per policy set — name, state, profiles

  • MnT active session count + ISE version

Single Endpoint Deep-Dive — 3-API Combined (DataConnect + ERS + MnT)

Replicates ~95% of the ISE GUI "General Attributes" page for a single MAC.

MAC="14:F6:D8:7B:31:80" . $(find data -name 'full-endpoint-profile.sh')
full-endpoint-profile.sh — what it returns
  • DataConnect — 5-table JOIN: RADIUS auth history, accounting, NAD, endpoint identity, profiler

  • ERS — endpoint details: identity group, static assignment, custom attributes, profile

  • MnT — active session: auth method, NAS IP/port, session ID, posture, dACL applied

  • Auth history — last 10 authentications with timestamp, method, status, authz profile

Output saved to /tmp/ise-endpoint-profile/

DataConnect SQL Queries — Bulk Reporting

Run any .sql file against ISE DataConnect (Oracle, port 2484):

dc-run-sql $(find data -name 'migration-executive-summary.sql') | tee /tmp/exec-summary.json | jq '.'
Available SQL files
find data/d001/projects/mschapv2-migration/sql -name '*.sql' -exec basename {} \; | sort

Key queries:

  • migration-executive-summary.sql — device counts, migration %, auth totals (30d)

  • migration-trend-weekly.sql — week-over-week protocol distribution (90d)

  • migration-wave-summary.sql — per-wave device counts and failure rates

  • migration-device-detail.sql — one row per MAC with owner, location, switches

  • migration-stale-devices.sql — devices not seen in 30+ days

  • linux-inventory-summary.sql — Linux endpoint count by profile and identity group

  • linux-inventory-ise-profiler.sql — Linux endpoints via ISE profiler classification

  • linux-inventory-ise-radius.sql — Linux endpoints by RADIUS auth behavior

Quick Commands
List all endpoints in a group
netapi ise get-endpoints --group "Linux-Research-Workstations" --all -f json | jq -r '.[].mac'
Get endpoint details for a single MAC
netapi ise get-endpoint 14:F6:D8:7B:31:80 -f json | jq '{groupId, identityGroup, mac, staticGroupAssignment}'
Check auth history for a MAC
netapi ise dc auth-history 14:F6:D8:7B:31:80 -f json | jq '.[] | {time, status, authz_profile}'
Active session for a MAC
netapi ise mnt session 14:F6:D8:7B:31:80
CoA (Change of Authorization) — bounce a session
netapi ise mnt coa 14:F6:D8:7B:31:80
List all policy sets
netapi ise get-policy-sets -f json | jq -r '.[].name'

ISE Session Investigation — Home Lab

ISE Session Investigation — Home Lab

Investigated endpoint visibility on d000 ISE using the ise-api-reference scripts. These are the proven, battle-tested scripts at data/d000/infra/ise-api-reference/scripts/.

Prerequisites
dsource d000 dev/network/ise
Active Sessions
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'active-sessions.sh') table
Output — 4 active sessions through VyOS gateway (10.50.1.40)
MAC                IP                 USER        NAS
64:32:A8:C4:C7:19  64:32:A8:C4:C7:19  10.50.1.40
A0:59:50:C9:92:EE  A0:59:50:C9:92:EE  10.50.1.40
56:38:C0:55:C5:8F  56:38:C0:55:C5:8F  10.50.1.40
24:FD:FA:04:A8:33  24:FD:FA:04:A8:33  10.50.1.40
Session Detail — Razer Workstation
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-detail.sh') 64:32:A8:C4:C7:19
Output — Intel-Device profile, MAB via capwap, ISE-01
{
  "user_name": "64:32:A8:C4:C7:19",
  "nas_ip_address": "10.50.1.40",
  "calling_station_id": "64:32:A8:C4:C7:19",
  "cpmsession_id": "2801320A00000161DE36DEF9",
  "destination_ip_address": "10.50.1.20",
  "acs_server": "ise-01",
  "auth_acs_timestamp": "2026-06-18T21:49:59.690-07:00",
  "nas_port_id": "capwap_90000002",
  "endpoint_policy": "Intel-Device",
  "acct_status_type": "Start"
}
Finding: ThinkPad P16g Not in ISE Sessions

e0:d5:5d:6c:e1:66 (P16g wired MAC) — no active session in MnT, no auth history. ERS shows the endpoint exists (profiler classified it) but identityStore: ipsk-mgr-01 with group Profiled. The workstation connects upstream of the ISE-controlled switch — port likely has no 802.1X/MAB configured, or it’s on a trunk/management VLAN bypassing ISE.

Available Scripts
Script Usage

active-sessions.sh

bash $(find data/d000/infra -name 'active-sessions.sh') table

session-detail.sh

bash $(find data/d000/infra -name 'session-detail.sh') <MAC>

auth-history.sh

bash $(find data/d000/infra -name 'auth-history.sh') <MAC> table

coa-reauth.sh

bash $(find data/d000/infra -name 'coa-reauth.sh') <MAC>

posture-detail.sh

bash $(find data/d000/infra -name 'posture-detail.sh') <MAC>

All scripts auto-source lib/ise-common.sh — no manual function loading. Portable across d000/d001 via dsource.

WLC 9800 — RADIUS Accounting Audit

WLC 9800 — RADIUS Accounting Audit

ISE MnT returns framed_ip_address: null for all sessions on NAS 10.50.1.40 (9800-WLC-01). Full audit of AAA, RADIUS, accounting, credentials, wireless profiles, HA/SSO, and AP management.

IOS-XE Output Modifiers
Modifier Unix Analog Regex Use Case

include

grep

show run | include aaa|radius

Line-match — anchors, alternation, character classes

exclude

grep -v

show run | exclude ^!

Inverse match — strip comments from running-config

section

awk '/^pat/{p=1} p'

show run | section ^radius.server

Full config block from match to next top-level line

begin

awk '/pat/,0'

show run | begin ^interface

From first match to EOF

count

grep -c

show run | count dot1x

Count matching lines

tee

tee

show run | tee flash:baseline.txt

Display + save to flash simultaneously

redirect

>

show run | redirect flash:config.txt

Save to flash (suppresses display)

append

>>

show run | append flash:audit-log.txt

Append to existing file on flash

format

xmlstarlet

show run | format

XML-structured output

IOS-XE regex uses . for "anything" (not alone — that’s glob). The $ anchor fails on most show output due to trailing whitespace. Use unanchored suffix patterns instead.
Capturing Audit Output to Workstation

Three methods for capturing IOS-XE command output to local files — from simplest to most automated.

Method 1: SSH + Unix tee (interactive)

SSH with forced TTY, pipe through tee to capture everything to a local file while watching live:

ssh -tt 9800-wlc-01 2>&1 | tee /tmp/wlc-audit-2026-06-23.txt

Log in, paste audit commands, exit. The entire session — prompts, commands, output — lands in /tmp. Clean control characters after:

col -b < /tmp/wlc-audit-2026-06-23.txt | sed '/^ *$/d' > /tmp/wlc-audit-clean.txt

col -b strips backspaces and control sequences. sed '/^ *$/d' removes blank lines.

Redact credentials before storing:

sed -i 's/\(key 7\|password 0\|secret 0\|ascii 0\) .*/\1 <REDACTED>/g' /tmp/wlc-audit-clean.txt
Method 2: script (full terminal recording)

script captures every byte — including ANSI colors, cursor movement, timing. Raw but complete:

script /tmp/wlc-session-2026-06-23.txt
ssh 9800-wlc-01

Paste commands, exit the WLC, then exit the script session. Clean with col -b.

Method 3: IOS-XE tee to flash (on-device)

Save output directly to WLC flash — useful for pre-change baselines:

show running-config | tee flash:pre-audit-2026-06-23.txt

Diff after changes:

show archive config differences flash:pre-audit-2026-06-23.txt system:running-config

Copy from flash to workstation via SCP:

scp 9800-wlc-01:flash:pre-audit-2026-06-23.txt /tmp/
Key Differences
Method Captures Credentials Automation

SSH + tee

Full session including prompts

Visible in file — redact after

Paste commands manually

script

Raw terminal bytes (ANSI included)

Visible — redact after

Paste commands manually

IOS-XE tee

Single command output only

On-device flash — access-controlled

Per-command, not bulk

SSH + tee is the sweet spot — live visibility, local capture, paste the full audit script in one shot.

Full Audit Script

28-check audit script at data/d000/infra/wlc-api-reference/scripts/cli/wlc-audit.txt. View it, then paste into a WLC SSH session:

cat $(find data/d000/infra/wlc-api-reference/scripts/cli -name 'wlc-audit.txt')

Key regex patterns:

Pattern What it catches

( 0 .*[A-Za-z])

Any Type 0 plaintext credential — key 0, secret 0, password 0, ascii 0

(key|secret|password|set-key).*(0|7|8|9)

Full credential type inventory — every credential with its obfuscation type

^(device-tracking|ip.dhcp.snooping|ip.device.tracking)

Device tracking + DHCP snooping — root cause check for framed_ip_address: null

(interim|accounting.update)

Accounting update method — newinfo vs periodic

^(key.config-key|password.encryption)

AES master key status — prerequisite for Type 8 mgmtuser encryption

^(Gi|Te|Vl|Lo).*is up

Active interfaces — GigE, TenGig, VLAN, Loopback

(state|SSO|peer|Mode)

HA/SSO redundancy state

^vlan.[0-9]

VLAN database with names

^ap.(78|dc|[0-9a-f]{4})

Per-AP config — tag assignments by MAC prefix

Audit Output — 2026-06-23

Captured via ssh -tt 9800-wlc-01 2>&1 \| tee /tmp/wlc-audit-2026-06-23.txt.

01: Platform
Cisco IOS XE Software, Version 17.15.04d
C9800-CL (VXE) processor, 12260012K/3075K memory
9800-WLC-01 uptime: 10 weeks, 1 day, 11 hours, 48 minutes
Control processor uptime: 3 weeks, 2 days, 9 hours, 14 minutes
License: AIR Network Advantage + AIR DNA Advantage
Configuration register: 0x2102
02: AAA + RADIUS
aaa new-model
aaa group server radius RADIUS_SERVER_GROUP_DAY0
aaa group server radius ISE-GROUP
aaa authentication login authentication_login_day0 group RADIUS_SERVER_GROUP_DAY0
aaa authentication dot1x authentication_dot1x_day0 group RADIUS_SERVER_GROUP_DAY0
aaa authentication dot1x ISE-Auth group ISE-GROUP
aaa authorization exec default local
aaa authorization network default group radius
aaa authorization network ISE-AuthZ group ISE-GROUP
aaa accounting update newinfo
aaa accounting identity ISE-ACCT start-stop group ISE-GROUP
aaa accounting network ISE-Acct start-stop group ISE-GROUP
aaa server radius dynamic-author
aaa session-id common
radius server ISE-1
radius server ISE-2
03: RADIUS Servers
radius server ISE-1
 address ipv4 10.50.1.20 auth-port 1812 acct-port 1813
 timeout 5
 retransmit 3
 key 7 <REDACTED>
radius server ISE-2
 address ipv4 10.50.1.21 auth-port 1812 acct-port 1813
 timeout 5
 retransmit 3
 key 7 <REDACTED>
04: RADIUS Server Groups
aaa group server radius RADIUS_SERVER_GROUP_DAY0
 server name RADIUS_SERVER_DAY0_1
aaa group server radius ISE-GROUP
 server name ISE-1
 server name ISE-2
 ip radius source-interface Vlan100
 deadtime 5
 mac-delimiter hyphen
05: CoA / Dynamic Authorization
aaa server radius dynamic-author
 client 10.50.1.20 server-key 7 <REDACTED>
 client 10.50.1.21 server-key 7 <REDACTED>
06: Policy Profiles
wireless profile policy POLICY-DOMUS_IoT
 aaa-override
 vlan 40
 no shutdown
wireless profile policy POLICY-DOMUS_SECURE
 aaa-override
 aaa-policy Domus-AAA-Policy
 accounting-list ISE-ACCT
 nac
 vlan DATA_VLAN
 no shutdown
wireless profile policy default-policy-profile
 aaa-override
 autoqos mode voice
 description "default policy profile"
 service-policy input platinum-up
 service-policy output platinum
 vlan VLAN0100
 no shutdown
07: WLAN Profiles
wlan Domus-IoT 5 Domus-IoT
 mac-filtering ISE-AuthZ
 security wpa psk set-key ascii 0 <REDACTED>
 no security wpa akm dot1x
 security wpa akm psk
 no shutdown
wlan Domus-Secure 4 Domus-Secure
 security dot1x authentication-list ISE-Auth
 no shutdown
08: AP Profile
ap profile default-ap-profile
 description "default ap profile"
 mgmtuser username admin password 0 <REDACTED> secret 0 <REDACTED>
09: Device Tracking + DHCP Snooping

Empty — not configured.

10: Accounting Update
aaa accounting update newinfo

No periodic interval configured.

11: Plaintext Credential Scan
show running-config | include ( 0 .*[A-Za-z])
 security wpa psk set-key ascii 0 <REDACTED>
 mgmtuser username admin password 0 <REDACTED> secret 0 <REDACTED>

Two Type 0 credentials remain. Require AES master key for encryption.

12: Full Credential Inventory
show running-config | include (key|secret|password|set-key).*(0|7|8|9)
 client 10.50.1.20 server-key 7 <REDACTED>
 client 10.50.1.21 server-key 7 <REDACTED>
 database archive pkcs12 password 7 <REDACTED>
 password 7 <REDACTED>
username admin privilege 15 secret 9 $9$...
username apiuser privilege 15 secret 9 $9$...
 key 7 <REDACTED>
 key 7 <REDACTED>
 security wpa psk set-key ascii 0 <REDACTED>
 mgmtuser username admin password 0 <REDACTED> secret 0 <REDACTED>

Credential type distribution: Type 9 (scrypt) for local users ✅, Type 7 for RADIUS/CoA keys ✅, Type 0 for PSK + mgmtuser ❌.

13: Encryption Status
service password-encryption              ✅ Enabled
key config-key password-encrypt          ❌ Not configured
password encryption aes                  ❌ Not configured
14: Active Interfaces
GigabitEthernet1 is up, line protocol is up
GigabitEthernet3 is up, line protocol is up
Vlan100 is up, line protocol is up , Autostate Enabled
15: VLANs
vlan 10   DATA_VLAN
vlan 20   VOICE_VLAN
vlan 30   GUEST_VLAN
vlan 40   IOT_VLAN
vlan 100  MANAGEMENT_VLAN
vlan 110  SECURITY_VLAN
vlan 120  SERVICES_VLAN
vlan 666  NATIVE_VLAN_UNUSED
vlan 999  CRITICAL_AUTH_VLAN
16: HA/SSO
                 Hardware Mode = Duplex
    Configured Redundancy Mode = sso
     Operating Redundancy Mode = sso
              Maintenance Mode = Disabled
        Current Software state = ACTIVE
       Uptime in current state = 3 weeks, 2 days, 4 hours, 26 minutes
        Current Software state = STANDBY HOT
       Uptime in current state = 3 weeks, 2 days, 4 hours, 21 minutes
17: AP Summary
Number of APs: 1
C9130AX-01   CW9166I-B   8c88.812a.0000   10.50.10.117   Registered
18: AP Tag Assignments
ap 78bc.1a36.82c0
 policy-tag HomeRF-Tag
ap dc8c.3796.20a6
 policy-tag TAG-DOMUS
 rf-tag TAG-DOMUS-RF
 site-tag TAG-DOMUS-SITE
19: Wireless Tags
wireless tag site TAG-DOMUS-SITE
 description "Domus site settings"
wireless tag policy TAG-DOMUS
 wlan Domus-IoT policy POLICY-DOMUS_IoT
 wlan Domus-Secure policy POLICY-DOMUS_SECURE
wireless tag policy default-policy-tag
 wlan Domus-IoT policy POLICY-DOMUS_IoT
 wlan Domus-Secure policy POLICY-DOMUS_SECURE
wireless tag rf TAG-DOMUS-RF
 24ghz-rf-policy Low_Client_Density_rf_24gh
 5ghz-rf-policy Low_Client_Density_rf_5gh
20: NTP
ntp server 10.50.1.1
ntp server 129.6.15.28
ntp server 132.163.96.5
Clock is synchronized, stratum 3, reference is 10.50.1.1
21: Logging
logging console warnings
logging monitor errors
logging host 10.50.1.134
22: HTTP/HTTPS
ip http server
ip http authentication local
ip http secure-server
ip http secure-trustpoint WLC-MGMT-CERT
23: Certificates
Trustpoint CN / Purpose Status

TP-self-signed-1917173558

IOS-Self-Signed-Certificate

✅ Issued

SLA-TrustPoint

Cisco Licensing Root CA

✅ CA only

WLC_CA

CA-vWLC_WLC-LAB

✅ CA only

WLC-LAB_WLC_TP

WLC-LAB_WLC_TP (exportable)

✅ Issued

HOME-ROOT-CA

HOME-ROOT-CA

⚠️ CA not authenticated

WLC-MGMT-CERT

9800-wlc-01.inside.domusdigitalis.dev

✅ Issued — Vault CA, used for HTTPS

24: RESTCONF
restconf

Enabled. API scripts at data/d000/infra/wlc-api-reference/scripts/restconf/.

Findings
Check Status Finding

AAA new-model

PASS

Enabled

dot1x auth

PASS

ISE-AuthISE-GROUP

Network authz

PASS

ISE-AuthZISE-GROUP

RADIUS server group

PASS

ISE-GROUP: both servers, source Vlan100, deadtime 5, mac-delimiter hyphen

Accounting (POLICY-DOMUS_SECURE)

PASS

accounting-list ISE-ACCT bound

Accounting (POLICY-DOMUS_IoT)

FAIL

No accounting-list — ISE blind to IoT SSID

Accounting (default-policy-profile)

FAIL

No accounting-list — ISE blind to default SSID

Accounting update

PARTIAL

newinfo only — no periodic interim

Device tracking

FAIL

Not configured — WLC cannot learn client IPs

DHCP snooping

FAIL

Not configured — WLC cannot intercept DHCP assignments

NAC (POLICY-DOMUS_SECURE)

PASS

nac enabled — posture/URL redirect possible

NAC (POLICY-DOMUS_IoT)

FAIL

No nac — no posture capability on IoT SSID

Session timeout

PARTIAL

Not configured — no forced reauth interval

CoA

PASS

Both ISE nodes registered as dynamic-author clients

RADIUS servers

PASS

ISE-1 (10.50.1.20) + ISE-2 (10.50.1.21), ports 1812/1813

RADIUS shared secret

PASS

Converted to key 7 via service password-encryption

CoA server-key

PASS

Converted to key 7 via service password-encryption

WPA PSK (Domus-IoT)

FAIL

Plaintext ascii 0 — requires AES master key

AP mgmtuser

FAIL

Plaintext password 0 / secret 0 — requires AES master key

Local users

PASS

admin + apiuser — both Type 9 (scrypt)

NTP

PASS

Synchronized stratum 3, reference 10.50.1.1

Logging

PASS

Syslog to 10.50.1.134, console warnings, monitor errors

SNMP

PARTIAL

Not configured

HTTPS

PASS

Vault-issued cert WLC-MGMT-CERT on secure-server

HA/SSO

PASS

Active/Standby HOT, SSO operational, 3+ weeks uptime

APs

PASS

1 registered — C9130AX-01 (CW9166I-B) at 10.50.10.117

Root Cause — framed_ip_address: null

Device tracking is not configured. The 9800 never learns client IP addresses post-DHCP, so aaa accounting update newinfo has no IP to report in Interim-Update records. ISE only receives the Accounting-Start (pre-DHCP) and never gets framed_ip_address.

Lesson Learned: Credential Exposure

Four classes of credentials found during audit:

Credential Location Resolution

RADIUS shared secret

radius server ISE-{1,2}key 0

■■■■■■■■■■ 10/10 — service password-encryptionkey 7

CoA server-key

aaa server radius dynamic-authorserver-key 0

■■■■■■■■■■ 10/10 — service password-encryptionkey 7

AP mgmtuser

ap profile default-ap-profilepassword 0 / secret 0

❌ Requires key config-key password-encrypt + password encryption aes

WPA PSK

wlan Domus-IoTset-key ascii 0

❌ Requires AES master key

service password-encryption reaches RADIUS keys and line passwords (→ Type 7) but does not reach mgmtuser or set-key ascii. Those require the AES master key infrastructure.

Type 8 in mgmtuser context means AES encryption (master-key-dependent), not PBKDF2-SHA256 used by username secret 8.

Credential Encryption Reference
Type Algorithm Reversible Scope

0

Plaintext

N/A

Never acceptable

7

Vigenère

Yes — trivially

RADIUS keys, line passwords. Enabled by service password-encryption.

8 (global)

PBKDF2-SHA256

No

username secret 8. Generated with algorithm-type sha256.

8 (mgmtuser)

AES

With master key

AP credentials. Requires key config-key password-encrypt + password encryption aes.

9

scrypt

No

username secret 9. Strongest. Default on IOS-XE 17.x.

Master Key Configuration — Future CR

Enabling AES password encryption converts all Type 0 and Type 7 credentials to Type 6/8. Global change affecting RADIUS keys, PSK, mgmtuser, and all stored credentials.

Prerequisites:

  • Store master key in secrets manager — unrecoverable if lost

  • Baseline to flash: show running-config \| tee flash:pre-masterkey.txt

  • Test in maintenance window — AP reconnection behavior is version-dependent

  • Document as CR per STD-005

Reference:

Remediation
Priority Action

P0 ✅

RADIUS shared secret — converted from key 0 to key 7 via service password-encryption.

P0 ✅

CoA server-key — converted to key 7.

P1

Enable device-tracking on client VLANs — required for IP→MAC binding. Root cause of framed_ip_address: null.

P1

Add aaa accounting update periodic <interval> — ensures ISE gets IP even if newinfo misses it.

P2

Add accounting-list ISE-ACCT to POLICY-DOMUS_IoT and default-policy-profile.

P2

Configure key config-key password-encrypt + password encryption aes — converts mgmtuser and WPA PSK from Type 0 to AES. CR required.

P2

Add nac to POLICY-DOMUS_IoT if posture/redirect needed for IoT.

P3

Configure session-timeout for periodic reauth on all policy profiles.

P3

Configure SNMP for monitoring.

net-push — Multi-Device Network CLI Tool

net-push — Multi-Device Network CLI Tool

Built a hardened, multi-device CLI automation tool for pushing commands to network devices via SSH with full output capture. Evolved from a single-purpose wlc-push.sh into a generalized net-push.sh that works with any SSH-managed device.

Problem

Interacting with network devices (WLC, switches, firewalls) requires:

  • Typing commands from memory or pasting from notes — error-prone

  • No local capture of output — evidence lost

  • Credentials managed ad-hoc — password in clipboard, shell history risk

  • No safety net for destructive commands — write memory is one typo away

Solution

net-push.sh — a single tool that:

  • Reads device inventory from YAML

  • Pulls credentials from gopass at runtime via file descriptor (never in env/argv/disk)

  • Sends command files over SSH via pexpect

  • Captures full output to timestamped files with 600 permissions

  • Auto-redacts credentials in output

  • Blocks destructive commands unless operator types the device hostname

  • Verifies its own integrity via SHA-256 before touching credentials

Architecture
data/d000/infra/scripts/cli/
├── net-push.sh          # Multi-device push tool (chmod 700)
├── devices.yaml         # Device inventory — hosts, gopass paths, prompts
└── .net-push.sha256     # Integrity hash (auto-generated)

data/d000/infra/wlc-api-reference/scripts/cli/
├── wlc-audit.txt        # 28-check WLC audit
├── wlc-push.sh          # Original single-device tool (superseded)
└── patches/
    ├── accounting-fix.txt
    └── write-memory.txt
Device Inventory
devices.yaml
devices:
  9800-wlc-01:
    host: 9800-wlc-01
    gopass: v3/domains/d000/network/devices/9800-wlc-01/ssh/admin
    prompt: "#"
    type: ios-xe
    destructive: "write memory|write mem|wr mem|wr$|erase|reload|delete"

  vyos-01:
    host: vyos-01
    gopass: v3/domains/d000/network/devices/firewall/vyos-01/vyos-admin
    prompt: "\\$"
    type: vyos
    destructive: "commit|save|reboot|delete"

Adding a device: one YAML block. The prompt field is regex — # for IOS-XE, \\$ for VyOS/Linux.

Usage
# List all devices
bash $(find data/d000/infra/scripts/cli -name 'net-push.sh') --list

# Dry run — preview commands without sending
bash $(find data/d000/infra/scripts/cli -name 'net-push.sh') 9800-wlc-01 wlc-audit.txt --dry-run

# Run audit — all show commands, read-only
bash $(find data/d000/infra/scripts/cli -name 'net-push.sh') 9800-wlc-01 wlc-audit.txt

# Apply a patch — destructive command requires hostname confirmation
bash $(find data/d000/infra/scripts/cli -name 'net-push.sh') 9800-wlc-01 patches/write-memory.txt
Security Model
Layer Mechanism

Credential isolation

gopass → fd 3 → Python os.fdopen()del password. Never in env vars, argv, or disk.

Script integrity

SHA-256 hash verified before any credential fetch. Tampered script aborts with diff.

Destructive gate

Commands matching write memory|erase|reload require typing the device hostname. read -r < /dev/tty — can’t be piped or scripted.

Output permissions

umask 077 + chmod 600 — owner-only on all output files.

Output redaction

sed strips key 0/7, password 0, secret 0, ascii 0, server-key values post-capture.

fd cleanup

trap cleanup EXIT closes fd 3 on any exit — normal, error, or signal.

GPG boundary

gopass requires GPG unlock. GPG key protected by passphrase + YubiKey (if configured).

Disk encryption

LUKS full-disk. /tmp is on encrypted filesystem. Reboot scrubs tmpfs.

Destructive Command Flow
$ net-push 9800-wlc-01 patches/write-memory.txt

WARNING: Patch contains destructive commands for 9800-wlc-01 (ios-xe):
   2: write memory

Type the device name to confirm [9800-wlc-01]: _

Wrong input → aborted. Correct input → proceeds with credential fetch and execution.

Work Usage Considerations

For use at CHLA or any enterprise environment:

Consideration Status

Automated SSH to production devices

Verify with security team — some orgs require all changes through PAM/change tools

Credential storage

gopass + GPG is strong but may not satisfy CyberArk/PAM requirements

Audit trail

Local /tmp captures satisfy "who did what when" — org may need centralized logging

CAB process

Config changes must follow STD-021 (iTrack change records). net-push captures are evidence, not approval.

Network segmentation

Workstation must have SSH access to target devices — verify ACLs

Recommendation: use for audit/read-only commands at work without restriction. Use for config changes only within an approved change window with CAB approval (STD-021).

Evolution
Version Milestone

v1

ssh -tt host 2>&1 | tee /tmp/file.txt — manual interactive capture

v2

wlc-push.sh — pexpect automation, single device, env var credentials

v3

net-push.sh — multi-device YAML inventory, fd credential piping, integrity hash, destructive gate, output redaction, umask 077

ISE MnT — Session Report Script

ISE MnT — Session Report Script

Created session-report.sh — joins active sessions with per-session detail to produce enriched tables. Demonstrates jq join pattern: loop source → fetch detail → extract + flatten → tabulate.

Formats
# Full enriched table
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh') table

# JSON array — all fields, pipeable to more jq
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh') json

# Compact — MAC, IP, accounting status
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh') summary

# Split other_attr_string into key-value pairs for one MAC
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh') attrs E0:D5:5D:6C:E1:66
Sample Output — table
MAC                IP            USER                                       NAS         ACCT   PROFILE
64:32:A8:C4:C7:19  —             64:32:A8:C4:C7:19                          10.50.1.40  Start  Intel-Device
A0:59:50:C9:92:EE  —             A0:59:50:C9:92:EE                          10.50.1.40  Start  Windows10-Workstation
56:38:C0:55:C5:8F  —             56:38:C0:55:C5:8F                          10.50.1.40  Start  Unknown
24:FD:FA:04:A8:33  —             24:FD:FA:04:A8:33                          10.50.1.40  Start  Unknown

MAB sessions still show for IP — these are stale pre-fix sessions. New sessions after re-auth will have framed_ip_address populated.

jq Patterns Used
Pattern What it does

jq -r '…​ | @tsv'

Extract fields → tab-separated for column -t

jq -s '.'

Slurp individual JSON objects into array

jq --arg mac "$mac"

Inject shell variable into jq filter

jq 'capture("(?<key>[=]+)=(?<value>.*)$")'

Regex named capture — split key=value preserving = in values

(.field // "—")

Null coalescing — default for missing fields

P16g Session Detail — Post-Fix Verification
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-detail.sh') E0:D5:5D:6C:E1:66 | jq '{user: .sessionParameters.user_name, ip: .sessionParameters.framed_ip_address, nas: .sessionParameters.nas_ip_address, acct: .sessionParameters.acct_status_type, profile: .sessionParameters.endpoint_policy}'
Output — framed_ip_address populated after radius-profiling + dot11-tlv-accounting fix
{
  "user": "modestus-p16g.inside.domusdigitalis.dev",
  "ip": "10.50.1.204",
  "nas": "10.50.1.40",
  "acct": "Stop",
  "profile": "Intel-Device"
}

lsof Diagnostics — Process & File Handle Investigation

lsof Diagnostics — Process & File Handle Investigation

Created a set of diagnostic scripts using lsof for investigating open files, network connections, serial console sessions, and process file descriptors. Each script is a single concern — one tool, one question answered.

Scripts Created
Script Purpose Usage

lsof-serial.sh

Identify processes holding serial/console ports (picocom, minicom, screen)

bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-serial.sh')

lsof-network.sh

Network connections by state, port, or process name

bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-network.sh') established

lsof-by-pid.sh

All files open by a process — name or PID (command substitution: pgreplsof)

bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-by-pid.sh') picocom

lsof-files.sh

Who has a file/device/directory open

bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-files.sh') /dev/ttyUSB0

Picocom Console Session

Active serial console to LAB-3560CX-01 via USB-to-serial adapter:

pgrep -a picocom
Output
390232 picocom -b 9600 /dev/ttyUSB0

Investigating file descriptors held by picocom:

bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-by-pid.sh') picocom
Command Substitution Patterns

lsof-by-pid.sh demonstrates the key pattern — resolve a name to PID via command substitution, then feed to lsof:

# Name → PIDs → lsof (what the script does internally)
lsof -p $(pgrep -d, picocom)

# Multiple PIDs joined with comma
lsof -p $(pgrep -d, kitty)

# Inline PID resolution for network filtering
lsof -i -a -p $(pgrep ssh-agent)
lsof Network Filters
# ESTABLISHED connections only
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-network.sh') established

# What's listening on RADIUS port
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-network.sh') 1812

# All SSH connections
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-network.sh') ssh

# Connections by any process name
bash $(find data/d000/infra/scripts/diagnostics -name 'lsof-network.sh') claude
Directory Structure
data/d000/infra/scripts/diagnostics/
├── lsof-serial.sh      # Serial/console port ownership
├── lsof-network.sh     # Network connections by state/port/process
├── lsof-by-pid.sh      # All file handles by process (name or PID)
└── lsof-files.sh       # Who holds a file/device/directory

Ollama — Model Architecture & Shell Integration

Ollama — Model Architecture & Shell Integration

RTX 5090 (24GB VRAM) + i9-275HX (24 cores) + 62GB RAM. Previously: 4 base models, no Modelfiles, broken aliases (quick/coder/analyst didn’t exist).

Model Lineup
Alias Model Name Base Temp Purpose

ai

quick

qwen3:30b-a3b

0.7

Fast general Q&A, brainstorming

aic

coder

deepseek-r1:32b

0.3

Code review, debugging, scripts, regex

aia

analyst

deepseek-r1:32b

0.4

Log analysis, data investigation, incidents

ain

neteng

deepseek-r1:32b

0.2

ISE config review, Cisco CLI, dACL design

aid

docs

qwen3:30b-a3b

0.5

AsciiDoc, Antora standards, technical writing

Domain Functions
Network Engineering
  • ise-review <file> — review ISE config/policy export

  • ise-debug [file] — analyze RADIUS auth failures (file or stdin)

  • ise-api "description" — generate ISE ERS API curl commands

Documentation
  • adoc-review <file> — check AsciiDoc standards compliance

  • adoc-draft "description" — draft an AsciiDoc partial

Analysis
  • analyze <file> — auto-detect file type, analyze with analyst model

  • aisum — pipe long output, get concise summary

Git
  • aicommit — generate commit message from staged diff

  • aidiff [ref] — explain a diff

  • aifix "error" — suggest fix for error message

Heredoc (multi-line prompts)
  • aih, aich, aiah, ainh, aidh — heredoc to each model

Management
  • aigpu — GPU VRAM usage + loaded models

  • aikill — unload all models to free VRAM

  • airebuild — recreate all models from Modelfiles

Architecture
dots-quantum/ollama/.config/ollama/modelfiles/
├── quick.Modelfile      # qwen3:30b-a3b — general
├── coder.Modelfile      # deepseek-r1:32b — code
├── analyst.Modelfile    # deepseek-r1:32b — data/logs
├── neteng.Modelfile     # deepseek-r1:32b — ISE/network
├── docs.Modelfile       # qwen3:30b-a3b — AsciiDoc
└── setup.sh             # Creates all models from Modelfiles

Modelfiles are stowable via dq-stow ollama. System prompts embed real environment context (IPs, VLANs, domain, cert paths, API patterns).

Setup Commands
dq-stow ollama
bash ~/.config/ollama/modelfiles/setup.sh
source ~/.zshrc
ai
Test Checklist
  • dq-stow ollama links Modelfiles to ~/.config/ollama/modelfiles/

  • bash ~/.config/ollama/modelfiles/setup.sh creates all 5 models

  • ai — opens quick session

  • aic — opens coder session

  • ain — opens neteng session (ISE context)

  • aid — opens docs session (AsciiDoc rules)

  • ask "what is EAP-TLS" — one-shot, no session

  • ise-review <config-file> — ISE-specific review

  • adoc-review <file.adoc> — standards check

  • aigpu — shows VRAM usage

  • aikill — frees VRAM

SSH Config — Missing Host Mappings After Modular Migration

SSH Config — modestus-p50 Unreachable

ssh modestus-p50 fails — hostname echoes back unresolved. Not a missing SSH config entry — there is no DNS A record in BIND for modestus-p50.

Diagnosis
SSH echoes hostname back — no IP mapping
ssh -G modestus-p50 | awk '/^hostname/'
Key hierarchy assigns keys but 80-d000-hosts.conf has no entry
grep 'modestus-p50' ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/20-d000-key-hierarchy.conf
grep 'modestus-p50' ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf

The June 21 DNS inventory (see dns-inventory-loop.adoc) already confirmed:

modestus-p50         — | No A record (retired?)
modestus-p16g        — | No A record (DHCP — currently 10.50.1.204)
thinkpad-x1          — | No A record (retired?)
gabriel-workstation  — | No A record

The dig loop includes these hosts — they returned nothing because BIND has no record to resolve.

Step 1 — Find the P50’s current IP via ISE MnT

ISE session report shows A0:59:50:C9:92:EE profiled as Windows10-Workstation — likely candidate.

Session report — identify active MACs
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-report.sh')
Full session detail — includes framed_ip_address
bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'session-detail.sh') A0:59:50:C9:92:EE
Command substitution — capture IP directly into a variable
P50_IP=$(bash $(find data/d000/infra/ise-api-reference/scripts/mnt -name 'reshape-session.sh') \
  A0:59:50:C9:92:EE | jq -r '.identity.ip')
printf "modestus-p50 IP: %s\n" "$P50_IP"

reshape-session.sh returns .identity.ip.framed_ip_address from the MnT session. Command substitution $(…​) captures that output as a shell variable — no copy-paste, no intermediary. The nested $(find …​) resolves the script path, the outer $(…​) captures its output.

Step 2 — Snapshot zone state BEFORE edit
Process substitution — save a snapshot without a temp file
ZONE_BEFORE=$(dig +noall +answer axfr inside.domusdigitalis.dev @10.50.1.90 | \
  awk '$4=="A" {h=$1; sub(/\.$/,"",h); printf "%-45s %s\n", h, $5}' | sort -k2,2V)

This captures the full A-record inventory before the edit. After adding the record, diff via process substitution will show exactly what changed — no temp files on disk.

Step 3 — Create DNS A record in BIND
Check current zone syntax
ssh bind-01 sudo named-checkzone inside.domusdigitalis.dev /var/named/inside.domusdigitalis.dev.zone
Edit zone file — add A record, bump serial
ssh bind-01 sudo vim /var/named/inside.domusdigitalis.dev.zone

Add the record:

modestus-p50    IN  A  <IP-FROM-STEP-1>
Verify syntax after edit
ssh bind-01 sudo named-checkzone inside.domusdigitalis.dev /var/named/inside.domusdigitalis.dev.zone
Reload zone
ssh bind-01 sudo rndc reload inside.domusdigitalis.dev
Step 4 — Verify with process substitution
diff the zone before vs after — process substitution, no temp files
diff <(echo "$ZONE_BEFORE") \
     <(dig +noall +answer axfr inside.domusdigitalis.dev @10.50.1.90 | \
       awk '$4=="A" {h=$1; sub(/\.$/,"",h); printf "%-45s %s\n", h, $5}' | sort -k2,2V)

Only the new modestus-p50 record should appear as a > line. If anything else changed, investigate.

Verify primary-secondary agreement — process substitution with paste
paste <(dig +short modestus-p50.inside.domusdigitalis.dev @10.50.1.90) \
      <(dig +short modestus-p50.inside.domusdigitalis.dev @10.50.1.91) \
  | awk '{printf "primary=%-15s secondary=%s\n", $1, $2}'

Both columns must match. If secondary is empty, force zone transfer:

ssh bind-01 sudo rndc notify inside.domusdigitalis.dev
Step 5 — Regenerate SSH host mappings

The dig loop from dns-inventory-loop.adoc already includes modestus-p50 in its brace expansion. Now that DNS resolves, regenerate:

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-{p16g,aw,razer} thinkpad-x1 modestus-p50 gabriel-workstation; 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
Step 6 — Verify SSH resolution
ssh -G modestus-p50 | awk '/^hostname|^user|^port/'
Step 7 — Re-encrypt hosts file
age -e -R ~/.age/recipients/self.txt \
  -o ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf.age \
  ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.d/80-d000-hosts.conf
Concepts — Process Substitution vs Command Substitution
Mechanism Syntax What it does

Command substitution

VAR=$(command)

Captures stdout as a string — assigns to variable or embeds inline

Process substitution

<(command)

Presents stdout as a file descriptor — feeds to commands expecting file arguments

Command substitution answers: "what did this command say?" Process substitution answers: "treat this command’s output as if it were a file."

diff needs two files. paste needs two files. Process substitution gives them files without creating files. The shell opens /dev/fd/NN, pipes the command’s output through it, and passes the path to the consumer.

The nested pattern in step 1 — P50_IP=$(bash $(find …​) | jq -r …​) — is two command substitutions: inner $(find …​) resolves the script path, outer $(…​) captures the final output. Each $(…​) is an independent subshell; they compose because the inner one resolves first, left to right.

User override

The old monolithic config set User gabriel for modestus-p50 / gabriel-workstation. The key hierarchy sets User evanusmodestus. If Gabriel’s account is needed, add a User override in 80-d000-hosts.conf:

Host modestus-p50 gabriel-workstation
    HostName <IP>
    User gabriel

Ad-Hoc Requests

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/'