# UAT Report: Citadel Scan

**Date:** 2026-02-03
**Target:** `/Users/jordanwashburn/Workspace/orchard9/citadel`
**Aphoria Version:** 0.1.0
**Status:** PASS

---

## Executive Summary

Ran Aphoria scan against the Citadel codebase (a production Rust monorepo with ~50 crates). Initial scan revealed critical precision issues with extractors producing 98% false positives. After bug fixes, precision improved to 100% with only 3 true positive findings remaining.

| Metric | Before Fixes | After Fixes |
|--------|--------------|-------------|
| Files scanned | 1,438 | 1,438 |
| Claims extracted | 1,259 | 1,259 |
| Conflicts reported | 168 | 3 |
| True positives | ~3 | 3 |
| False positives | ~165 | 0 |
| Precision | ~2% | **100%** |
| Scan time | 1.7s | 1.7s |

---

## Test Environment

### Target Codebase: Citadel

- **Type:** Rust workspace with ~50 crates
- **Domain:** Log aggregation and observability platform
- **Notable crates:**
  - `citadel-auth`, `citadel-auth-db`, `citadel-auth-oidc` (authentication)
  - `citadel-api` (HTTP API)
  - `citadel-agent` (log collection agent)
  - `ingestion-edge`, `tiered-storage` (data pipeline)
  - `citadel-community` (Next.js frontend)
- **Languages:** Rust, TypeScript, YAML, TOML, JSON

### Scan Configuration

```bash
cargo run --release -- scan /Users/jordanwashburn/Workspace/orchard9/citadel
```

- Mode: Ephemeral (no persistence)
- Format: Table (default), JSON for analysis

---

## Bugs Discovered and Fixed

### Bug #1: Rate Limit Regex Matches Embedded Zeros (HIGH)

**Symptom:** 150 of 168 conflicts were rate_limit false positives.

**Root Cause:** The regex pattern `r"(?i)(rate_?limit|ratelimit).*(?:disabled|off|false|0|none|skip)"` matched any line containing "rate limit" followed by any `0` digit anywhere.

**Example False Positive:**
```rust
// Line: RateLimiter::new(10); // 10 req/min
// Matched because: "RateLimiter" contains "rate...limit", and "10" contains "0"
```

**Fix:** Changed pattern to require assignment context:
```rust
// Before
r"(?i)(rate_?limit|ratelimit).*(?:disabled|off|false|0|none|skip)"

// After
r"(?i)(rate_?limit|ratelimit)(?:_enabled)?\s*[:=]\s*(?:disabled|off|false|\b0\b|none|skip)"
```

**Key changes:**
- Added `(?:_enabled)?` to match `ratelimit_enabled = false`
- Required `\s*[:=]\s*` for assignment context
- Changed `0` to `\b0\b` (word boundary) to avoid matching `10`, `100`, etc.

**File:** `src/extractors/rate_limit.rs:55-58`

---

### Bug #2: Rate Limit Numeric Pattern Doesn't Handle Underscores (MEDIUM)

**Symptom:** `rateLimitPerTenant: 100_000` not detected as high limit.

**Root Cause:** Regex `\d+` doesn't match underscore-separated numbers common in YAML/Rust.

**Fix:**
1. Changed pattern to `[\d_]+` to capture underscored numbers
2. Added underscore stripping before parsing: `value_str.chars().filter(|c| *c != '_').collect()`

**File:** `src/extractors/rate_limit.rs:59-70, 143-146`

---

### Bug #3: JWT Audience Pattern Matches "audit" (MEDIUM)

**Symptom:** `audit_log_path.is_none()` flagged as "JWT audience validation disabled".

**Root Cause:** Pattern `aud.*None` matched `audit_log_path...None` because `aud` is a prefix of `audit`.

**Example False Positive:**
```rust
// Line: assert!(config.audit_log_path.is_none());
// Matched because: "audit" starts with "aud", followed eventually by "None"
```

**Fix:** Made pattern specific to JWT audience contexts:
```rust
// Before
r"(?i)(set_audience.*\[\]|validate_aud.*false|aud.*None|ValidateAudience.*false)"

// After
r"(?i)(set_audience\s*\(\s*(?:vec!)?\s*\[\s*\]\s*\)|validate_aud\w*\s*[:=]\s*false|\baud(?:ience)?\s*[:=]\s*None|ValidateAudience\s*[:=]\s*false)"
```

**Key changes:**
- Used `\baud(?:ience)?` with word boundary
- Required assignment context `[:=]`

**File:** `src/extractors/jwt_config.rs:40-47`

---

### Bug #4: JWT Signature Verify Pattern Too Broad (MEDIUM)

**Symptom:** `insecure_skip_verify = false` flagged as "signature verification disabled" when it's actually the SECURE setting.

**Root Cause:** Pattern `verify.*false` matched any line with "verify" followed by "false".

**Example False Positives:**
```rust
// Line: insecure_skip_verify = false  (SECURE - not skipping!)
// Line: const [isVerifying, setIsVerifying] = useState(false)  (React UI state)
```

**Fix:** Required specific `verify_signature` or `signature_verify` context:
```rust
// Before
r"(?i)(dangerous_insecure|skip_signature|verify.*false|RequireSignedTokens.*false)"

// After
r"(?i)(dangerous_insecure|skip_signature|(?:verify_signature|signature_verify)\s*[:=]\s*false|RequireSignedTokens\s*[:=]\s*false)"
```

**File:** `src/extractors/jwt_config.rs:60-69`

---

### Bug #5: JWT Algorithm None Matches Documentation (LOW)

**Symptom:** Documentation line `(no 'alg: none')` flagged when it's describing PREVENTION.

**Example False Positive:**
```rust
// Line: /// - Algorithm whitelist (no `alg: none`)
// This is documentation saying alg:none is NOT allowed (secure!)
```

**Fix:** Added post-processing filter to exclude prevention documentation:
```rust
let is_prevention_doc = lower_line.contains("no alg")
    || lower_line.contains("no `alg")
    || lower_line.contains("whitelist")
    || lower_line.contains("reject")
    // ... etc
```

**File:** `src/extractors/jwt_config.rs:170-183`

---

## Regression Tests Added

| Test | Purpose | File |
|------|---------|------|
| `test_rate_limiter_constructor_not_flagged` | `RateLimiter::new(10)` not flagged | rate_limit.rs |
| `test_rate_limit_constant_not_flagged` | `RATE_LIMIT: u64 = 100` not flagged | rate_limit.rs |
| `test_rate_limit_per_tenant_not_flagged` | Underscore numbers handled | rate_limit.rs |
| `test_rate_limit_explicitly_zero` | `rate_limit = 0` IS flagged | rate_limit.rs |
| `test_rate_limit_documentation_not_flagged` | Doc comments not flagged | rate_limit.rs |
| `test_skip_verify_false_not_flagged` | `skip_verify = false` not flagged | jwt_config.rs |
| `test_ui_state_not_flagged` | `isVerifying` state not flagged | jwt_config.rs |
| `test_verify_release_not_flagged` | Non-JWT verify functions | jwt_config.rs |
| `test_actual_signature_verify_disabled` | True positives still caught | jwt_config.rs |

---

## Final Scan Results

### Command
```bash
cargo run --release -- scan /Users/jordanwashburn/Workspace/orchard9/citadel
```

### Output Summary
```
3 BLOCK, 0 FLAG, 0 PASS
```

### Remaining True Positives

| File | Line | Finding |
|------|------|---------|
| `tools/citadel-cli/src/commands/agent.rs` | 903 | API key hardcoded in source |
| `crates/citadel-cli/src/commands/query.rs` | 736 | API key hardcoded in source |
| `crates/citadel-agent/src/config.rs` | 101 | API key hardcoded in source |

**Analysis:** All 3 are example API keys in test code or documentation. They follow the pattern `ck_live_*` or `ck_test_*` which correctly matches the hardcoded secrets extractor. These are true positives (low severity) that can be:
1. Fixed by moving examples to environment variables
2. Acknowledged with `aphoria ack` if intentional for documentation

---

## Quality Gates Passed

| Check | Status |
|-------|--------|
| `cargo test` | 158 passed, 0 failed |
| `cargo clippy -- -D warnings` | 0 warnings |
| `cargo fmt --check` | Formatted |

---

## Known Limitations

1. **No TLS conflicts found:** Citadel's TLS configuration appears compliant, or patterns aren't matching their specific setup.

2. **Secrets extractor catches test keys:** Low-severity true positives in test/doc code. May want to add confidence reduction for `test` or `example` path segments.

3. **Algorithm none filter is heuristic:** The documentation filter uses keyword matching which could miss edge cases or over-filter.

---

## Recommendations

1. **Add path-aware confidence:** Reduce confidence for findings in `/tests/`, `/examples/`, or files containing "test" in the name.

2. **Consider YAML-aware extraction:** The current line-by-line regex approach could be improved with proper YAML/TOML parsing for structured configs.

3. **Expand test corpus:** Use Citadel findings to build a larger regression test suite.

4. **Document patterns:** Add ai-lookup entries explaining the regex patterns for future maintainers.

---

## Appendix: Conflict Categories

### Before Fixes
| Category | Count | % |
|----------|-------|---|
| rate_limit | 150 | 89% |
| jwt | 15 | 9% |
| secrets | 3 | 2% |
| **Total** | **168** | |

### After Fixes
| Category | Count | % |
|----------|-------|---|
| secrets | 3 | 100% |
| **Total** | **3** | |
