stemedb/applications/aphoria/docs/architecture/policy-alias-implementation.md
jordan 41c676a78e feat: Aphoria enterprise features + ontology SDK + file length compliance
Enterprise Features:
- Hosted mode with remote sync for team pattern aggregation
- Community sharing with privacy-preserving anonymization
- LLM-based semantic claim extraction with Gemini integration
- Pattern learning with promotion to declarative extractors
- High-entropy secrets extractor with configurable thresholds
- Auth bypass and insecure cookies extractors

Module Refactoring:
- Split oversized files to comply with 500-line limit
- Config split: types/core.rs, types/extractors.rs, types/hosted.rs, etc.
- Handlers split: scan.rs, policy.rs, report.rs modules
- Extractors split: declarative/, high_entropy_secrets/, insecure_cookies/
- Learning split: store modules with metrics and persistence

SDK & Ontology:
- stemedb-ontology SDK with fluent builders and StemeDB client
- Pharma domain extractors for FDA Orange Book data
- Consumer health UAT test infrastructure

Code Quality:
- Fixed clippy warnings (needless_borrows_for_generic_args)
- Added KVStore trait imports where needed
- Fixed utoipa path re-exports for OpenAPI docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:55:29 -07:00

20 KiB

Policy Alias Implementation Guide

Related: Concept Matching Analysis 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

/// 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<String>,
}

1.2 Extend TrustPack

File: applications/aphoria/src/policy.rs

#[derive(Archive, Deserialize, Serialize, Debug, Clone)]
#[archive(check_bytes)]
pub struct TrustPack {
    pub header: PackHeader,
    pub assertions: Vec<Assertion>,
    pub aliases: Vec<ConceptAlias>,

    /// Policy-level aliases for matching extractor output to policy paths.
    /// Optional: Empty vec = no policy aliases (backward compatible).
    pub policy_aliases: Vec<PolicyAlias>,

    pub signature: [u8; 64],
}

1.3 Update TrustPack Constructor

impl TrustPack {
    pub fn new(
        name: String,
        version: String,
        assertions: Vec<Assertion>,
        aliases: Vec<ConceptAlias>,
        policy_aliases: Vec<PolicyAlias>,  // NEW
        signing_key: &SigningKey,
    ) -> Result<Self, AphoriaError> {
        // ... 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:

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

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

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<Assertion>> {
        // 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:

cargo test -p aphoria episteme::concept_index::tests::policy_alias_matching

Test Cases:

#[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

async fn check_conflicts_persistent(
    all_claims: &[ExtractedClaim],
    project_root: &Path,
    config: &AphoriaConfig,
    sync: bool,
) -> Result<ConflictCheckResult, AphoriaError> {
    // ... 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<PolicyAlias> = 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

use crate::policy::PolicyAlias;

impl LocalEpisteme {
    /// Check conflicts with policy alias support.
    pub async fn check_conflicts_with_aliases(
        &self,
        claims: &[ExtractedClaim],
        config: &AphoriaConfig,
        index: &ConceptIndex,
        policy_aliases: &[PolicyAlias],
    ) -> Result<Vec<ConflictResult>, 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:

// Keep existing method for ephemeral mode
pub async fn check_conflicts(
    &self,
    claims: &[ExtractedClaim],
    config: &AphoriaConfig,
    index: &ConceptIndex,
) -> Result<Vec<ConflictResult>, 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

pub struct EphemeralDetector {
    // ... existing fields
    policy_aliases: Vec<PolicyAlias>,  // 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: &[ExtractedClaim],
        config: &AphoriaConfig,
    ) -> Vec<ConflictResult> {
        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

#[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<String>,

        /// Output path for updated pack (default: overwrite input).
        #[arg(short, long)]
        output: Option<PathBuf>,
    },
}

4.2 Implement Handler

File: applications/aphoria/src/policy_ops.rs

use crate::policy::{PolicyAlias, TrustPack};

pub fn handle_policy_add_alias(
    pack_path: &Path,
    policy_path: String,
    target_patterns: Vec<String>,
    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

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:

# 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:

### 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

#[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 = ExtractedClaim {
        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

pub enum PatternSyntax {
    Glob(String),
    Regex(String),
}

3. Alias Auto-Discovery

During Scan: Suggest aliases when tail-path matches but full path differs.

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

pub struct TrustPack {
    pub header: PackHeader,
    pub extends: Vec<String>,  // 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.