Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
b0acde4
skip IP private address check in case the DC is theoretically in a pu…
jazofra Jan 23, 2026
9959752
Go port first try
jazofra Jan 31, 2026
d2b397c
Add feature parity with PowerShell for Go port
claude Jan 31, 2026
b4bf2f7
Merge pull request #1 from jazofra/claude/port-powershell-to-go-LneSn
jazofra Jan 31, 2026
0ec0622
Add unit tests for Go collector edge creation
claude Jan 31, 2026
105f199
Add remaining feature parity features for Go port
claude Jan 31, 2026
a594067
Add complete edge kind parity with PowerShell
claude Feb 1, 2026
8f4138e
Add comprehensive edge reference table to Go README
claude Feb 1, 2026
58b9ba8
Merge pull request #2 from jazofra/claude/port-powershell-to-go-LneSn
jazofra Feb 1, 2026
af0c2d3
go port refined
jazofra Feb 3, 2026
5217921
Fix LDAP authentication with improved fallback methods
claude Feb 3, 2026
719d115
Merge pull request #3 from jazofra/claude/fix-ldap-authentication-6I2FJ
jazofra Feb 3, 2026
7df2bd1
first complete version in Go
jazofra Feb 3, 2026
28edeef
Revise README.md for MSSQLHound Go
jazofra Feb 3, 2026
df2b900
fixed inconsistent naming
jazofra Feb 4, 2026
68de6f4
Fix Go edge generation to match PowerShell implementation
claude Feb 10, 2026
f026bc6
Fix remaining Go/PowerShell edge generation differences
claude Feb 10, 2026
8509dc1
Merge pull request #4 from jazofra/claude/debug-edge-generation-YXmc1
jazofra Feb 10, 2026
013b7fa
Add DNS server and domain controller options
Mayyhem Feb 10, 2026
c8ef034
Updated EPA checks, added options
Mayyhem Feb 10, 2026
b30d72a
Update EPA checks with Force Strict Encryption
Mayyhem Feb 10, 2026
2312862
Update missing edge classes
Mayyhem Feb 11, 2026
892d0c9
All unit tests passing after feature parity updates
Mayyhem Feb 17, 2026
c8d9f49
--scan-all-computer collection fixed
jazofra Feb 18, 2026
ff83904
Update edge properties for feature parity with .ps1
Mayyhem Feb 23, 2026
0a793a1
Collection against server with Force Encryption, Force Strict Encrypt…
Mayyhem Feb 25, 2026
70b35fd
Back off after invalid password attempt
Mayyhem Feb 25, 2026
eb4ed74
Save EPA data to nodes when connection fails
Mayyhem Feb 26, 2026
8ff2860
Added port scan before EPA check/auth attempts
Mayyhem Feb 26, 2026
0829301
Bump version
Mayyhem Feb 26, 2026
ce4f551
Redact cleartext passwords in connection strings
Mayyhem Feb 26, 2026
c06eea8
Remove older version
Mayyhem Feb 26, 2026
2118cab
Update all EPA checks and add testing matrix
Mayyhem Feb 27, 2026
9c90ebd
Add seed data to zip by default to allow querying when no edge instan…
Mayyhem Feb 27, 2026
c79e816
Port unit tests to Go
Mayyhem Mar 3, 2026
c512dcd
Remove unnecessary code, update edge comparison tool
Mayyhem Mar 4, 2026
bb6e307
Update READMEs
Mayyhem Mar 25, 2026
aa48c56
Merge branch 'main' into main
Mayyhem Mar 25, 2026
4323603
Merge pull request #12 from jazofra/main
Mayyhem Mar 26, 2026
f48694d
Remove binary
Mayyhem Mar 27, 2026
aaa08ab
Remove duplicate files, references to perspectives, and reorganize
Mayyhem Mar 30, 2026
b88106a
Add contributing guidance
Mayyhem Mar 30, 2026
2fe3f44
Moved go code to root, deprecated PowerShell, rewrote logger
Mayyhem Mar 30, 2026
8c5baaf
Add OpenGraph schema, OPSEC documentation, fix DNS, log enhancements,…
Mayyhem Mar 31, 2026
525b267
Add GitHub Action for integration tests
Mayyhem Mar 31, 2026
5b4eedd
Update trigger
Mayyhem Mar 31, 2026
51575c7
Update subnet mask
Mayyhem Mar 31, 2026
b2cb2e9
Update password complexity
Mayyhem Mar 31, 2026
bb2de41
Increase timeout
Mayyhem Mar 31, 2026
3829b55
Debug SQL Server
Mayyhem Mar 31, 2026
77883ab
Update IP addresses
Mayyhem Mar 31, 2026
620e05f
Update krb
Mayyhem Mar 31, 2026
dbfc8f4
Fix krb
Mayyhem Mar 31, 2026
e256bdc
Debug krb
Mayyhem Mar 31, 2026
e1db8e7
Change IPs
Mayyhem Mar 31, 2026
bfe5dae
Increase timeout
Mayyhem Mar 31, 2026
afa1808
Debug
Mayyhem Mar 31, 2026
8c81f14
More debugging
Mayyhem Mar 31, 2026
03ee061
Debug
Mayyhem Mar 31, 2026
e1ef374
NT hash and kerberos support, upload to BloodHound
Mayyhem Mar 31, 2026
b4b00c1
Group options
Mayyhem Mar 31, 2026
d79d155
Debug
Mayyhem Apr 1, 2026
c4699be
Update IP
Mayyhem Apr 1, 2026
4a99e44
Update working version
Mayyhem Apr 1, 2026
da5e30c
Try stopping resolved
Mayyhem Apr 1, 2026
2afb957
Try to fix networking
Mayyhem Apr 1, 2026
054e30b
Try to revert
Mayyhem Apr 1, 2026
f93d88a
Working til integration tests, adding second return
Mayyhem Apr 1, 2026
c90b1c6
Remove duplicate, wait for domain accounts in integration tests
Mayyhem Apr 1, 2026
ed63ea4
Domain join SQL runner
Mayyhem Apr 1, 2026
67ad4f1
Disable reverse DNS for Kerberos
Mayyhem Apr 2, 2026
04dc5bb
Add try/catch for local group
Mayyhem Apr 2, 2026
7e1add4
Update command line options, Kerberos for integration testing
Mayyhem Apr 2, 2026
ee03e28
Support for user:pass@<target> in comma-separated or one entry per li…
Mayyhem Apr 2, 2026
300c066
Update CLI options
Mayyhem Apr 2, 2026
8cec8a0
Fix Kerberos for CI
Mayyhem Apr 2, 2026
9134fd5
Fresh LDAP connection per bind attempt
Mayyhem Apr 2, 2026
ce798cf
Run MSSQL with domain service account
Mayyhem Apr 2, 2026
2a924dd
Debug service account issues
Mayyhem Apr 2, 2026
99a2d21
Lookup SPN when service account is empty after collection
Mayyhem Apr 2, 2026
66a6b38
Update OPSEC and log for service account SPN fallback
Mayyhem Apr 2, 2026
d5d10c8
Remove extra linked server expectation from SCCM lab, add log context
Mayyhem Apr 2, 2026
88b410d
Use BUILTIN\Administrators instead of RDP
Mayyhem Apr 2, 2026
bfd638d
Update unit test
Mayyhem Apr 2, 2026
441ce71
Update expected links in unit test
Mayyhem Apr 2, 2026
6b84ca6
Update schema upload functions
Mayyhem Apr 3, 2026
ce0cda8
Update schema with MSSQL_ prefix, Kerberos auth working
Mayyhem Apr 7, 2026
e355ee0
Invert options, update schema, remove traversable property
Mayyhem Apr 7, 2026
0ffd85e
Update ignored files
Mayyhem Apr 7, 2026
038324f
Add plans, update schema and ignored files
Mayyhem Apr 7, 2026
91f5c57
Update DNS resolution timeout, seed data, design docs
Mayyhem Apr 7, 2026
9ec4b3b
Autodiscover WinRM port for EPA matrix test
Mayyhem Apr 16, 2026
b995358
Update README and .gitignore
Mayyhem Apr 21, 2026
f8e568e
Fix /dev/tty failure
Mayyhem Apr 21, 2026
c0265be
Check before adding MS repo
Mayyhem Apr 21, 2026
70d9cad
Update license to Apache 2.0
Mayyhem Apr 21, 2026
2f99eb1
Merge branch 'main' into go
Mayyhem Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions .claude/plans/integration_test_ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# GitHub Actions CI: Integration Tests with Samba AD DC + SQL Server

## Context

MSSQLHound needs CI that validates all expected edges are created against a real AD environment. The integration test framework already handles AD object creation via LDAP, SQL setup/teardown, collector execution, and edge validation — we just need to provide the infrastructure.

**Architecture**: SQL Server installed on the runner host (via apt) + Samba AD DC in a Docker container. SQL Server is configured for AD auth via a keytab from `samba-tool`. The integration test framework (`TestIntegrationAll`) handles everything else.

## Network and domain config

| Setting | Value |
|---------|-------|
| Subnet | `10.2.0.0/20` |
| DC IP | `10.2.10.100` |
| SQL Server | On the runner host (localhost) |
| Domain FQDN | `MAYYHEM.COM` |
| NetBIOS | `MAYYHEM` |
| Admin account | `MAYYHEM\domainadmin` (sysadmin on SQL) |
| Password (all) | `password` |
| SQL auth mode | Mixed mode (Windows + SQL) |

## Files to create

- `.github/workflows/ci.yml` (new)

## Plan

### 1. Create `.github/workflows/ci.yml` with two jobs

**Job 1: `unit-tests`** — Fast validation of all edge creation logic
- `ubuntu-latest`, checkout, setup Go, `go test -v -count=1 ./...`

**Job 2: `integration-tests`** — Full pipeline against real AD + SQL Server on `ubuntu-22.04`

#### Step 1: Checkout + Setup Go

#### Step 2: Start Samba AD DC
```bash
docker network create --subnet=10.2.0.0/20 adnet

docker run -d --privileged \
--name dc --hostname DC \
--network adnet --ip 10.2.10.100 \
-e REALM='MAYYHEM.COM' \
-e DOMAIN='MAYYHEM' \
-e ADMIN_PASS='password' \
-e DNS_FORWARDER='8.8.8.8' \
-p 389:389/tcp -p 389:389/udp \
-p 636:636/tcp \
-p 88:88/tcp -p 88:88/udp \
-p 464:464/tcp -p 464:464/udp \
diegogslomp/samba-ad-dc
```

#### Step 3: Wait for Samba DC readiness
Poll `samba-tool domain info` up to 60 attempts / 2s.

#### Step 4: Create domainadmin user + SQL Server service account + keytab
```bash
# Create domainadmin user
docker exec dc samba-tool user create domainadmin 'password' --use-username-as-cn
docker exec dc samba-tool group addmembers "Domain Admins" domainadmin

# Create SQL Server service account
docker exec dc samba-tool user create sqlsvc 'password' --use-username-as-cn
HOSTNAME=$(hostname)
docker exec dc samba-tool spn add MSSQLSvc/${HOSTNAME}.mayyhem.com sqlsvc
docker exec dc samba-tool spn add MSSQLSvc/${HOSTNAME}.mayyhem.com:1433 sqlsvc

# Export keytab
docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab --principal=sqlsvc
docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab \
--principal=MSSQLSvc/${HOSTNAME}.mayyhem.com
docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab \
--principal=MSSQLSvc/${HOSTNAME}.mayyhem.com:1433
docker cp dc:/tmp/mssql.keytab /tmp/mssql.keytab
```

#### Step 5: Configure host DNS + Kerberos
```bash
# DNS
echo "10.2.10.100 dc.mayyhem.com dc mayyhem.com" | sudo tee -a /etc/hosts
sudo sed -i '1i nameserver 10.2.10.100' /etc/resolv.conf

# Kerberos
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y krb5-user
cat <<'EOF' | sudo tee /etc/krb5.conf
[libdefaults]
default_realm = MAYYHEM.COM
dns_lookup_realm = false
dns_lookup_kdc = false

[realms]
MAYYHEM.COM = {
kdc = 10.2.10.100
admin_server = 10.2.10.100
default_domain = mayyhem.com
}

[domain_realm]
.mayyhem.com = MAYYHEM.COM
mayyhem.com = MAYYHEM.COM
EOF
```

#### Step 6: Install SQL Server 2022
```bash
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | \
sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list | \
sudo tee /etc/apt/sources.list.d/mssql-server-2022.list
sudo apt-get update
sudo apt-get install -y mssql-server
sudo MSSQL_SA_PASSWORD='password' MSSQL_PID='Developer' \
/opt/mssql/bin/mssql-conf setup accept-eula
```

#### Step 7: Enable mixed mode auth
```bash
sudo /opt/mssql/bin/mssql-conf set sqlagent.enabled true
sudo /opt/mssql/bin/mssql-conf set network.kerberoskeytabfile /var/opt/mssql/secrets/mssql.keytab
sudo /opt/mssql/bin/mssql-conf set network.privilegedadaccount sqlsvc

# Enable mixed mode (SQL + Windows auth)
# SQL Server Linux uses the MSSQL_SA_PASSWORD being set during setup to enable mixed mode.
# To explicitly toggle it post-setup if needed:
sudo /opt/mssql/bin/mssql-conf set sqlagent.enabled true
```

Note: SQL Server on Linux enables mixed mode auth when SA password is set during setup. The `MSSQL_SA_PASSWORD` in step 6 handles this.

#### Step 8: Configure SQL Server keytab + restart
```bash
sudo mkdir -p /var/opt/mssql/secrets
sudo cp /tmp/mssql.keytab /var/opt/mssql/secrets/mssql.keytab
sudo chown mssql:mssql /var/opt/mssql/secrets/mssql.keytab
sudo chmod 400 /var/opt/mssql/secrets/mssql.keytab
sudo systemctl restart mssql-server
sleep 5
```

#### Step 9: Create domainadmin as SQL sysadmin
```bash
# Install sqlcmd
curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/prod.list | \
sudo tee /etc/apt/sources.list.d/mssql-release.list
sudo apt-get update
sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18

export PATH="$PATH:/opt/mssql-tools18/bin"
sqlcmd -S localhost -U sa -P 'password' -C -Q "
CREATE LOGIN [MAYYHEM\domainadmin] FROM WINDOWS;
ALTER SERVER ROLE [sysadmin] ADD MEMBER [MAYYHEM\domainadmin];
"
```

#### Step 10: Verify AD auth
```bash
echo 'password' | kinit Administrator@MAYYHEM.COM
klist
```

#### Step 11: Run integration tests
```bash
MSSQL_SERVER=localhost \
MSSQL_USER=sa \
MSSQL_PASSWORD='password' \
MSSQL_DOMAIN=mayyhem.com \
MSSQL_DC=10.2.10.100 \
LDAP_USER='Administrator@mayyhem.com' \
LDAP_PASSWORD='password' \
MSSQL_SKIP_DOMAIN=false \
MSSQL_ACTION=all \
MSSQL_SKIP_HTML=true \
go test -v -count=1 -tags integration -timeout 30m \
-run TestIntegrationAll ./internal/collector/...
```

Since `MSSQL_DOMAIN=mayyhem.com` → `substituteDomain()` extracts `MAYYHEM` as NetBIOS, which matches the hardcoded `MAYYHEM\` references in the SQL scripts. No substitution gap.

## Critical files

| File | Role |
|------|------|
| [integration_setup_test.go](internal/collector/integration_setup_test.go) | Config loading, LDAP object creation, SQL setup orchestration |
| [integration_sql_test.go](internal/collector/integration_sql_test.go) | Embedded SQL scripts with `FROM WINDOWS` + `$Domain` references |
| [edge_integration_test.go](internal/collector/edge_integration_test.go) | `TestIntegrationAll` entry point, edge validation |
| [edge_test_data_test.go](internal/collector/edge_test_data_test.go) | 200+ edge test case definitions |

## Risks and mitigations

| Risk | Mitigation |
|------|------------|
| Port 53 conflict on runner | Don't publish port 53; use `/etc/hosts` + `/etc/resolv.conf` |
| SQL Server 2022 not on Ubuntu 24.04 | Pin `ubuntu-22.04` |
| Samba DC slow to start | 120s timeout with polling |
| `FROM WINDOWS` fails if DNS broken | Verify `kinit` works before running tests |
| `diegogslomp/samba-ad-dc` unavailable | Fall back to `craftdock/samba-ad-dc` |
| Mixed mode not enabled | SA password set during setup enables it automatically |

## Verification

1. Unit tests locally: `go test -v ./...`
2. Push to branch and verify both jobs pass
3. Integration test output includes edge coverage report from `TestIntegrationAll`
151 changes: 151 additions & 0 deletions .claude/plans/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Logging Overhaul Plan

## Context
MSSQLHound currently uses raw `fmt.Printf`/`fmt.Println` for all logging (~149 calls across 10 files). Messages have no timestamps, no log levels, and inconsistent formatting. The goal is to add UTC timestamps and log levels to every message using Go's stdlib `log/slog` (available since Go 1.21, project uses Go 1.24).

## Approach: Use `log/slog` from stdlib

No new packages or dependencies. Create a `*slog.Logger` in `main()`, propagate via struct fields.

### Output format
```
INFO 2026-03-30T14:22:01Z Processing 5 SQL Server(s)...
INFO 2026-03-30T14:22:01Z [corp.local] Enumerating MSSQL SPNs from Active Directory...
VERBOSE 2026-03-30T14:22:01Z [corp.local] Found SPNs count=12 host=sql01.corp.local
WARNING 2026-03-30T14:22:02Z [sql01.corp.local] SPN enumeration failed error="connection refused"
DEBUG 2026-03-30T14:22:02Z [sql01.corp.local] EPA TLS handshake complete cipher=0x1301
```

- Custom `slog.Handler` implementation in a new file `internal/logging/handler.go`
- Format: `LEVEL TIMESTAMP [target] message attrs...`
- Level name is left-aligned, right-padded to 7 chars (longest: `WARNING` and `VERBOSE`)
- Output goes to **stderr** (standard for logs; keeps stdout clean for data output like tables)
- Timestamps in UTC RFC3339 format
- **Target context**: when processing a specific server or domain, a `[target]` tag appears after the timestamp. Implemented via `logger.With("target", serverName)` to create sub-loggers. The custom handler renders the `target` attr specially in brackets, separate from other attrs.
- Messages without a target (startup, global config) omit the bracket section

### Colors (ANSI escape codes)
- Auto-detect TTY on stderr (`os.Stderr.Fd()` + `isatty` check or `golang.org/x/term.IsTerminal`). Disable colors when piped.
- **Level colors**:
- `ERROR` — red (ANSI 31)
- `WARNING` — yellow (ANSI 33)
- `INFO` — white/default (no color)
- `VERBOSE` — dim/gray (ANSI 90)
- `DEBUG` — magenta (ANSI 35)
- **Timestamp** — light blue (ANSI 94)
- **Target** `[brackets]` — deterministic color per unique target string. Hash the target name to pick from a palette of primary/secondary ANSI colors (red 31, green 32, yellow 33, blue 34, magenta 35, cyan 36). Same target always gets the same color. Use a simple hash (e.g., `fnv32(target) % len(palette)`).
- **Message text** — default/no color
- **Attrs** (`key=value`) — dim (ANSI 2) for the key, default for value

### Custom slog levels
| Name | slog.Level value | Meaning |
|---|---|---|
| `ERROR` | `slog.LevelError` (8) | Error conditions |
| `WARNING` | `slog.LevelWarn` (4) | Warnings |
| `INFO` | `slog.LevelInfo` (0) | Normal status/progress |
| `VERBOSE` | `slog.Level(-2)` | Detailed progress (was `logVerbose`) |
| `DEBUG` | `slog.LevelDebug` (-4) | EPA/TLS/NTLM diagnostics (was `logDebug`/`logf`) |

### Level mapping from current code
| Current pattern | New level |
|---|---|
| `fmt.Printf(...)` (status/progress) | `INFO` |
| `fmt.Printf("Warning: ...")` | `WARNING` |
| `fmt.Printf("ERROR: ...")` | `ERROR` |
| `logVerbose(...)` | `VERBOSE` |
| `logDebug(...)` / `logf(...)` (EPA diagnostics) | `DEBUG` |

### Flag behavior
- No flags: minimum level = INFO
- `--verbose`: minimum level = VERBOSE (shows VERBOSE + INFO + WARNING + ERROR)
- `--debug`: minimum level = DEBUG (shows everything)
- `--debug` additionally sets `debug=true` on subsystems (controls EPA test behavior beyond just logging)

## Implementation Phases

### Phase 0: Custom handler (`internal/logging/`)

**New file: [internal/logging/handler.go](go/internal/logging/handler.go)**
- Implement `slog.Handler` that formats: `LEVEL TIMESTAMP [target] message attrs...`
- Define custom level constants: `LevelVerbose = slog.Level(-2)`
- Level name mapping: `-2` → `VERBOSE`, `slog.LevelWarn` → `WARNING`
- Left-align level name, right-pad to 7 chars
- Special handling for `target` attr: rendered as `[value]` before the message, not as `key=value`
- Other attrs appended as `key=value` after the message
- Thread-safe writer (mutex around writes)
- `WithAttrs` / `WithGroup` support for creating sub-loggers (e.g., `logger.With("target", server)`)
- ANSI color support: detect TTY via `golang.org/x/term.IsTerminal(int(os.Stderr.Fd()))`
- Color each element per the palette defined above (level, timestamp, target, attrs)
- Target color: `fnv32a(targetString) % 6` maps to one of [blue 34, cyan 36, bright green 92, bright blue 94, bright cyan 96, bright white 97]. These avoid red (ERROR), yellow (WARNING), magenta (DEBUG), and gray (VERBOSE).
- Accept a `NoColor bool` option to force colors off (for tests or `--no-color` flag)

### Phase 1: Logger setup in main (`cmd/mssqlhound/`)

**[main.go](go/cmd/mssqlhound/main.go)**
- Create `slog.LevelVar` and `*slog.Logger` with custom `logging.NewHandler(os.Stderr, ...)` in `main()`
- Add `PersistentPreRunE` to set level from `--verbose`/`--debug` flags
- Pass logger to `run()` and subcommands
- Convert 11 `fmt.Printf`/`fmt.Println` calls to `logger.Info`/`logger.Warn`
- Keep `fmt.Fprintf(os.Stderr, ...)` for cobra error at line 105 (logger may not exist)

**[cmd_test_epa_matrix.go](go/cmd/mssqlhound/cmd_test_epa_matrix.go)**
- Accept logger parameter from main
- Convert 8 `fmt.Printf` calls

### Phase 2: Collector (`internal/collector/`)

**[collector.go](go/internal/collector/collector.go)**
- Add `Logger *slog.Logger` field to `Config` struct
- Convert 75 `fmt.Printf`/`fmt.Println` calls to appropriate slog levels
- Convert 49 `c.logVerbose(...)` calls to `c.config.Logger.Log(ctx, logging.LevelVerbose, ...)`
- Remove `logVerbose` method (line 6128)
- When processing a server, create a sub-logger: `serverLog := c.config.Logger.With("target", server.ConnectionString)` and use it for all per-server messages
- For domain-level operations: `domainLog := c.config.Logger.With("target", domain)`
- Pass logger to `mssql.Client` and `wmi` calls

### Phase 3: MSSQL client (`internal/mssql/`)

**[client.go](go/internal/mssql/client.go)** — 15 fmt calls + 7 logf calls
- Add `logger *slog.Logger` field, `SetLogger` method
- Default to `slog.Default()` in `NewClient`
- Replace `logVerbose`/`logDebug` methods with `c.logger.Debug()`
- Change `epaTLSDialer.logf` and `epaTDSDialer.logf` fields from `func(string, ...interface{})` to `*slog.Logger`
- Update dialer `d.logf(...)` calls to `d.logger.Debug(...)`

**[epa_tester.go](go/internal/mssql/epa_tester.go)** — 69 logf calls + 2 fmt calls
- Add `Logger *slog.Logger` to `EPATestConfig`
- Remove the `logf` closure (line 91-95)
- Convert all 69 `logf(...)` calls to `config.Logger.Debug(...)` with `"component", "epa"` attr
- Convert 2 direct `fmt.Printf` calls

**[epa_auth_provider.go](go/internal/mssql/epa_auth_provider.go)** — 2 fmt calls
- Add `logger *slog.Logger` field
- Convert 2 `fmt.Printf("[EPA-auth] ...")` calls

**[powershell_fallback.go](go/internal/mssql/powershell_fallback.go)** — 1 fmt call
- Add `logger *slog.Logger` field, `SetLogger` method
- Remove `logVerbose` method, convert call to `p.logger.Debug()`

### Phase 4: Supporting packages

**[epamatrix/epamatrix.go](go/internal/epamatrix/epamatrix.go)** — 24 fmt calls
- Add `Logger *slog.Logger` to `MatrixConfig`
- Convert all 24 calls

**[wmi/wmi_windows.go](go/internal/wmi/wmi_windows.go)** — 7 fmt calls
- Change `GetLocalGroupMembers` and `GetLocalGroupMembersWithFallback` signatures: replace `verbose bool` with `logger *slog.Logger`
- Update [wmi/wmi_stub.go](go/internal/wmi/wmi_stub.go) signatures to match
- Update caller in [collector.go:1854](go/internal/collector/collector.go#L1854)

### NOT changed
- **[epamatrix/table.go](go/internal/epamatrix/table.go)** — `PrintResultsTable`/`Summarize` write formatted table data to `io.Writer`. This is data output, not logging.
- All `fmt.Errorf(...)` calls (error construction, not logging)
- All `fmt.Sprintf(...)` calls (string building)

## Verification
1. `go build ./...` compiles cleanly
2. `go vet ./...` passes
3. Run with no flags — only INFO+ messages appear, each with UTC timestamp and level
4. Run with `--verbose` — DEBUG messages appear
5. Run with `--debug` — EPA diagnostic messages appear with `component=epa` attribute
6. Table output (EPA matrix) still renders correctly to stdout without log formatting
Loading
Loading