# Policy Alias Implementation Guide **Related:** [Concept Matching Analysis](./concept-matching-analysis.md) **Status:** Implementation Ready **Estimated Effort:** 2-3 days --- ## Implementation Phases ### Phase 1: Schema Extension (Day 1, Morning) **Goal:** Add `PolicyAlias` type and extend `TrustPack`. #### 1.1 Define PolicyAlias Type **File:** `applications/aphoria/src/policy.rs` ```rust /// Maps policy assertion paths to extractor output patterns. /// /// Enables enterprise security teams to define standards using logical hierarchies /// (e.g., "code://standards/tls/*") that match extractor output /// (e.g., "code://rust/myapp/tls/*"). #[derive(Archive, Deserialize, Serialize, Debug, Clone)] #[archive(check_bytes)] pub struct PolicyAlias { /// The policy path used in assertions (e.g., "code://standards/tls/cert_verification"). pub policy_path: String, /// Glob patterns that should resolve to this policy path. /// Supports '*' wildcard for single-segment match. /// /// Examples: /// - "code://rust/*/tls/cert_verification" (matches any project) /// - "code://*/myapp/tls/cert_verification" (matches any language) /// - "code://rust/myapp/*/cert_verification" (matches any module) pub target_patterns: Vec, } ``` #### 1.2 Extend TrustPack **File:** `applications/aphoria/src/policy.rs` ```rust #[derive(Archive, Deserialize, Serialize, Debug, Clone)] #[archive(check_bytes)] pub struct TrustPack { pub header: PackHeader, pub assertions: Vec, pub aliases: Vec, /// Policy-level aliases for matching extractor output to policy paths. /// Optional: Empty vec = no policy aliases (backward compatible). pub policy_aliases: Vec, pub signature: [u8; 64], } ``` #### 1.3 Update TrustPack Constructor ```rust impl TrustPack { pub fn new( name: String, version: String, assertions: Vec, aliases: Vec, policy_aliases: Vec, // NEW signing_key: &SigningKey, ) -> Result { // ... existing timestamp/issuer logic let temp_pack = TrustPack { header: header.clone(), assertions: assertions.clone(), aliases: aliases.clone(), policy_aliases: policy_aliases.clone(), // NEW signature: [0u8; 64], }; // ... existing signing logic Ok(TrustPack { header, assertions, aliases, policy_aliases, // NEW signature }) } } ``` **Testing:** ```bash cargo test -p aphoria policy::tests::trust_pack_with_policy_aliases ``` --- ### Phase 2: Pattern Matching (Day 1, Afternoon) **Goal:** Implement glob-based pattern matching for policy aliases. #### 2.1 Add Glob Matching Function **File:** `applications/aphoria/src/episteme/concept_index.rs` ```rust /// Check if a subject matches a glob pattern with '*' wildcard. /// /// Supports single-segment wildcards only (not recursive `**`). /// /// # Examples /// ``` /// assert!(glob_match("code://rust/*/tls/cert_verification", "code://rust/myapp/tls/cert_verification")); /// assert!(glob_match("code://*/myapp/tls/*", "code://python/myapp/tls/min_version")); /// assert!(!glob_match("code://rust/*/tls", "code://go/myapp/tls/cert_verification")); /// ``` fn glob_match(pattern: &str, subject: &str) -> bool { let pattern_parts: Vec<&str> = pattern.split('/').collect(); let subject_parts: Vec<&str> = subject.split('/').collect(); if pattern_parts.len() != subject_parts.len() { return false; } pattern_parts.iter().zip(subject_parts.iter()).all(|(p, s)| { *p == "*" || *p == *s }) } ``` #### 2.2 Extend ConceptIndex Lookup **File:** `applications/aphoria/src/episteme/concept_index.rs` ```rust use crate::policy::PolicyAlias; impl ConceptIndex { /// Look up assertions with policy alias fallback. /// /// Algorithm: /// 1. Try direct tail-path match (existing behavior) /// 2. If no match, try each policy alias pattern /// 3. If pattern matches subject, lookup using policy_path /// 4. Return first match (policy aliases processed in order) pub fn lookup_with_policy_aliases( &self, subject: &str, predicate: &str, policy_aliases: &[PolicyAlias], ) -> Option<&Vec> { // Try direct tail-path match first (fast path) if let Some(result) = self.lookup(subject, predicate) { return Some(result); } // Try policy alias patterns (fallback) for alias in policy_aliases { // Check if any pattern matches the subject let pattern_matches = alias.target_patterns.iter().any(|pattern| { glob_match(pattern, subject) }); if pattern_matches { // Look up using the policy path instead if let Some(result) = self.lookup(&alias.policy_path, predicate) { return Some(result); } } } None } } ``` **Testing:** ```bash cargo test -p aphoria episteme::concept_index::tests::policy_alias_matching ``` **Test Cases:** ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_glob_match_wildcard() { assert!(glob_match("code://rust/*/tls", "code://rust/myapp/tls")); assert!(glob_match("code://*/myapp/tls", "code://rust/myapp/tls")); assert!(!glob_match("code://rust/*/tls", "code://go/myapp/tls")); } #[test] fn test_policy_alias_lookup() { // Policy assertion: "code://standards/tls/cert_verification" let policy_assertion = Assertion { subject: "code://standards/tls/cert_verification".to_string(), predicate: "enabled".to_string(), object: ObjectValue::Boolean(true), // ... other fields }; let index = ConceptIndex::build(&[policy_assertion]); let alias = PolicyAlias { policy_path: "code://standards/tls/cert_verification".to_string(), target_patterns: vec![ "code://rust/*/tls/cert_verification".to_string(), ], }; // Should match via alias let result = index.lookup_with_policy_aliases( "code://rust/myapp/tls/cert_verification", "enabled", &[alias], ); assert!(result.is_some()); assert_eq!(result.unwrap().len(), 1); } } ``` --- ### Phase 3: Integration (Day 2, Morning) **Goal:** Wire policy aliases into scan flow. #### 3.1 Pass Policy Aliases to ConceptIndex **File:** `applications/aphoria/src/scan.rs` ```rust async fn check_conflicts_persistent( all_claims: &[Observation], project_root: &Path, config: &AphoriaConfig, sync: bool, ) -> Result { // ... existing setup // Load policies (Trust Packs) let policy_manager = PolicyManager::new(&config.corpus.cache_dir); let policies = policy_manager.load_policies(&config.policies)?; // Extract policy aliases from all Trust Packs let policy_aliases: Vec = policies .iter() .flat_map(|pack| &pack.policy_aliases) .cloned() .collect(); info!( policy_alias_count = policy_aliases.len(), "Loaded policy aliases from Trust Packs" ); // Build corpus and index let mut corpus = create_authoritative_corpus(&signing_key); let imported_assertions = episteme.fetch_authoritative_assertions().await?; corpus.extend(imported_assertions); let index = ConceptIndex::build(&corpus); // Pass aliases to conflict checker let conflicts = episteme .check_conflicts_with_aliases(all_claims, config, &index, &policy_aliases) .await?; // ... rest of function } ``` #### 3.2 Extend LocalEpisteme::check_conflicts **File:** `applications/aphoria/src/episteme/local.rs` ```rust use crate::policy::PolicyAlias; impl LocalEpisteme { /// Check conflicts with policy alias support. pub async fn check_conflicts_with_aliases( &self, claims: &[Observation], config: &AphoriaConfig, index: &ConceptIndex, policy_aliases: &[PolicyAlias], ) -> Result, AphoriaError> { // ... existing setup (fetch acks, etc.) for claim in claims { // Use extended lookup with policy aliases let auth_assertions = match index.lookup_with_policy_aliases( &claim.concept_path, &claim.predicate, policy_aliases, ) { Some(assertions) => assertions, None => continue, }; // ... rest of conflict detection logic (unchanged) } // ... return results } } ``` **Backward Compatibility:** ```rust // Keep existing method for ephemeral mode pub async fn check_conflicts( &self, claims: &[Observation], config: &AphoriaConfig, index: &ConceptIndex, ) -> Result, AphoriaError> { // Delegate to new method with empty aliases self.check_conflicts_with_aliases(claims, config, index, &[]).await } ``` #### 3.3 Update EphemeralDetector **File:** `applications/aphoria/src/episteme/ephemeral.rs` ```rust pub struct EphemeralDetector { // ... existing fields policy_aliases: Vec, // NEW } impl EphemeralDetector { pub fn ingest_policies(&mut self, policies: &[TrustPack]) { for pack in policies { // Ingest assertions (existing) self.ingest_authoritative(&pack.assertions); // Ingest policy aliases (NEW) self.policy_aliases.extend(pack.policy_aliases.clone()); } } pub fn check_conflicts( &self, claims: &[Observation], config: &AphoriaConfig, ) -> Vec { let index = ConceptIndex::build(&self.corpus); // Use policy aliases in lookup for claim in claims { let auth_assertions = index.lookup_with_policy_aliases( &claim.concept_path, &claim.predicate, &self.policy_aliases, ); // ... rest of conflict logic } } } ``` --- ### Phase 4: CLI Tooling (Day 2, Afternoon) **Goal:** Enable security teams to create policy aliases easily. #### 4.1 Add `policy add-alias` Command **File:** `applications/aphoria/src/types/command.rs` ```rust #[derive(Debug, Subcommand)] pub enum PolicyCommand { // ... existing commands (export, import, list) /// Add a policy alias to a Trust Pack. /// /// Allows mapping extractor output patterns to policy assertion paths. #[command(name = "add-alias")] AddAlias { /// Path to the Trust Pack file. #[arg(short, long)] pack: PathBuf, /// Policy path (e.g., "code://standards/tls/cert_verification"). #[arg(long)] policy_path: String, /// Target pattern (e.g., "code://rust/*/tls/cert_verification"). /// Can be specified multiple times. #[arg(long = "target")] target_patterns: Vec, /// Output path for updated pack (default: overwrite input). #[arg(short, long)] output: Option, }, } ``` #### 4.2 Implement Handler **File:** `applications/aphoria/src/policy_ops.rs` ```rust use crate::policy::{PolicyAlias, TrustPack}; pub fn handle_policy_add_alias( pack_path: &Path, policy_path: String, target_patterns: Vec, output_path: Option<&Path>, signing_key: &SigningKey, ) -> Result<(), AphoriaError> { // Load existing Trust Pack let mut pack = TrustPack::load(pack_path)?; info!( pack = %pack.header.name, version = %pack.header.version, "Loaded Trust Pack" ); // Validate patterns for pattern in &target_patterns { if !is_valid_glob_pattern(pattern) { return Err(AphoriaError::Config(format!( "Invalid glob pattern: {}", pattern ))); } } // Check if alias already exists (avoid duplicates) let exists = pack.policy_aliases.iter().any(|a| { a.policy_path == policy_path && a.target_patterns == target_patterns }); if exists { info!("Policy alias already exists, skipping"); return Ok(()); } // Add new policy alias let alias = PolicyAlias { policy_path: policy_path.clone(), target_patterns }; pack.policy_aliases.push(alias); // Re-sign the pack (required because we modified it) let new_pack = TrustPack::new( pack.header.name, pack.header.version, pack.assertions, pack.aliases, pack.policy_aliases, signing_key, )?; // Save to output path (or overwrite input) let save_path = output_path.unwrap_or(pack_path); new_pack.save(save_path)?; info!( policy_path, output = %save_path.display(), "Added policy alias to Trust Pack" ); Ok(()) } fn is_valid_glob_pattern(pattern: &str) -> bool { // Check for balanced segments (no double slashes, etc.) !pattern.is_empty() && !pattern.contains("//") && pattern.split('/').all(|seg| !seg.is_empty() || seg == "*") } ``` #### 4.3 Wire into CLI **File:** `applications/aphoria/src/handlers.rs` ```rust PolicyCommand::AddAlias { pack, policy_path, target_patterns, output } => { let signing_key = load_or_generate_key(&project_root)?; crate::policy_ops::handle_policy_add_alias( &pack, policy_path, target_patterns, output.as_deref(), &signing_key, )?; } ``` **Example Usage:** ```bash # Security team workflow aphoria policy export security-standards-v1.0.pack aphoria policy add-alias \ --pack security-standards-v1.0.pack \ --policy-path "code://standards/tls/cert_verification" \ --target "code://rust/*/tls/cert_verification" \ --target "code://go/*/tls/cert_verification" \ --target "code://python/*/tls/cert_verification" # Dev team imports and scans aphoria scan --mode persistent ``` --- ### Phase 5: Documentation & Testing (Day 3) #### 5.1 Update User Guide **File:** `applications/aphoria/docs/guides/federating-truth.md` Add section: ```markdown ### Policy Aliases When security teams create standards using logical hierarchies (e.g., `code://standards/*`), these may not match extractor output (e.g., `code://rust/myapp/*`). Policy aliases bridge this gap: ```bash # Add alias to Trust Pack aphoria policy add-alias \ --pack security.pack \ --policy-path "code://standards/tls/cert_verification" \ --target "code://rust/*/tls/cert_verification" ``` Now scans will match code extractors against the policy path. ``` #### 5.2 Write UAT Scenario **File:** `applications/aphoria/uat/2026-02-05-policy-alias-uat.md` ```markdown # UAT: Policy Alias Matching ## Scenario Security team creates standard at `code://standards/tls/cert_verification`. Dev team code has `code://rust/myapp/tls/cert_verification`. ## Setup 1. Create security-team project with blessed assertion 2. Export Trust Pack with policy alias 3. Create dev-team project with violating code 4. Import Trust Pack 5. Scan ## Expected Outcome - Scan detects conflict via policy alias - Report shows policy source - Exit code = 1 (BLOCK) ``` #### 5.3 Integration Tests **File:** `applications/aphoria/src/tests/policy_alias_integration.rs` ```rust #[tokio::test] async fn test_policy_alias_matching_integration() { // 1. Create policy assertion let policy_assertion = create_test_assertion( "code://standards/tls/cert_verification", "enabled", ObjectValue::Boolean(true), SourceClass::Expert, ); // 2. Create policy alias let alias = PolicyAlias { policy_path: "code://standards/tls/cert_verification".to_string(), target_patterns: vec![ "code://rust/*/tls/cert_verification".to_string(), ], }; // 3. Build Trust Pack let key = SigningKey::generate(&mut rand::thread_rng()); let pack = TrustPack::new( "Test Policy".to_string(), "1.0.0".to_string(), vec![policy_assertion], vec![], vec![alias], &key, ).unwrap(); // 4. Simulate scan with code claim let code_claim = Observation { concept_path: "code://rust/myapp/tls/cert_verification".to_string(), predicate: "enabled".to_string(), value: ObjectValue::Boolean(false), // CONFLICT // ... other fields }; // 5. Check conflicts let corpus = vec![pack.assertions[0].clone()]; let index = ConceptIndex::build(&corpus); let result = index.lookup_with_policy_aliases( &code_claim.concept_path, &code_claim.predicate, &pack.policy_aliases, ); // 6. Assert match assert!(result.is_some(), "Policy alias should match"); assert_eq!(result.unwrap()[0].object, ObjectValue::Boolean(true)); } ``` --- ## Migration & Rollout ### Backward Compatibility ✅ **Existing Trust Packs:** - `policy_aliases` field is optional (deserializes as empty vec) - No re-signing required unless adding aliases ✅ **Existing Scans:** - Empty aliases vec = current behavior - No performance impact (skips alias loop) ✅ **Existing CLI:** - All existing commands work unchanged - `policy add-alias` is additive ### Rollout Plan **Week 1 (Dev):** - [ ] Implement Phases 1-3 - [ ] Write unit tests - [ ] Manual testing with UAT scenario **Week 2 (Validation):** - [ ] Implement Phase 4 (CLI) - [ ] Write integration tests - [ ] Performance benchmarks **Week 3 (Docs & Release):** - [ ] Update user documentation - [ ] Write migration guide - [ ] Release 0.2.0 with feature flag **Week 4 (Enterprise Pilot):** - [ ] Deploy to 2-3 enterprise teams - [ ] Collect feedback - [ ] Iterate on pattern syntax if needed --- ## Performance Considerations ### Lookup Complexity **Direct tail-path:** O(1) hash lookup **Policy alias:** O(P * A) where: - P = patterns per alias - A = number of aliases **Mitigation:** - Try direct lookup first (fast path) - Only iterate aliases on miss - Most scans will have < 10 aliases - Pattern matching is simple string comparison **Benchmark Target:** < 5% scan time increase ### Memory Overhead **Per Trust Pack:** - PolicyAlias: ~100 bytes - 10 aliases: ~1 KB - Negligible compared to corpus (MBs) --- ## Future Enhancements ### 1. Recursive Wildcards **Current:** `code://rust/*/tls` (single segment) **Future:** `code://rust/**/tls` (any depth) **Implementation:** Use `globset` crate for full glob support. ### 2. Regex Patterns **Current:** Glob wildcards **Future:** Full regex support ```rust pub enum PatternSyntax { Glob(String), Regex(String), } ``` ### 3. Alias Auto-Discovery **During Scan:** Suggest aliases when tail-path matches but full path differs. ```rust // In conflict detection if tail_match && !full_match { warn!( "Potential alias needed: {} -> {}", claim.concept_path, assertion.subject ); } ``` ### 4. Trust Pack Composition **Idea:** Allow Trust Packs to "extend" other packs. ```rust pub struct TrustPack { pub header: PackHeader, pub extends: Vec, // URLs of parent packs // ... } ``` --- ## Success Criteria ### Functional - [ ] Security team can create policy at `code://standards/*` - [ ] Dev team code at `code://rust/myapp/*` matches - [ ] Conflicts detected and reported correctly - [ ] Trust Pack signature verifies with aliases ### Performance - [ ] Scan time increase < 5% - [ ] Memory overhead < 10 KB per pack ### Usability - [ ] `policy add-alias` command works intuitively - [ ] Trust Pack import is automatic (no manual config) - [ ] Error messages are clear (invalid patterns, etc.) ### Quality - [ ] 100% test coverage on pattern matching - [ ] Integration test covers full workflow - [ ] UAT scenario passes --- ## Questions for Review 1. **Glob Syntax:** Single wildcard (`*`) sufficient, or support recursive (`**`)? 2. **Alias Priority:** First match wins, or most specific match? 3. **Validation:** Fail Trust Pack creation if pattern is invalid? 4. **Caching:** Cache pattern match results, or recompute each time? --- **Ready to implement.** Feedback welcome before starting Phase 1.