stemedb/applications/aphoria/src/extractors/unreal_cpp.rs
jml 6430ff0fd6 fix(aphoria): move claims.toml to project root and fix verify integration
## Root Cause
Claims file was in applications/aphoria/.aphoria/ but all commands looked
for .aphoria/claims.toml relative to project root. Additionally, .aphoria/
was fully gitignored, preventing version control of claims.

## Changes

### Path Fixes
- Move claims.toml from applications/aphoria/.aphoria/ to .aphoria/ at project root
- Update .gitignore: .aphoria/ → .aphoria/* with !.aphoria/claims.toml exception
- Now claims can be version controlled while keys remain secret

### Verify Integration (Scanner)
- scanner.rs: Load claims from ClaimsFile and call verify_claims()
- ScanResult: Add verify field with VerifyReport
- Report formatters: Add claim verification sections showing PASS/CONFLICT/MISSING

### Clippy Fix
- report/json.rs: Replace filter().map().expect() with filter_map()

## Verification
- aphoria scan . → Shows claim verification with verdicts
- aphoria verify run → Per-claim verification results
- aphoria verify map → Extractor coverage mapping (7/10 claims = 70%)
- aphoria claims list → Reads from project root
- aphoria claims create → Writes to project root
- All tests pass (1120+ aphoria tests)
- clippy --workspace passes

## Impact
Both primary use cases now work:
1. Day-to-day (commit-time): Skills can read/create claims via CLI
2. Audit (scan-time): Scanner verifies code against authored claims

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 11:09:57 +00:00

194 lines
5.5 KiB
Rust

//! Unreal Engine C++ extractor.
//!
//! Detects security and architecture issues in Unreal Engine C++ code, such as:
//! - Exposed Exec functions (can be called from console by users/cheaters)
//! - Unprotected replication variables
//! - Hardcoded asset paths (fragile and hard to maintain)
#![allow(clippy::too_many_arguments)]
use regex::Regex;
use stemedb_core::types::ObjectValue;
use super::Extractor;
use crate::types::{Language, Observation};
/// Extractor for Unreal Engine C++ patterns.
pub struct UnrealCppExtractor {
/// UFUNCTION(Exec) detection
exec_function: Regex,
/// UPROPERTY(Replicated) without condition
unconditional_replication: Regex,
/// Hardcoded asset paths (TEXT("/Game/...") or TEXT("/Engine/..."))
hardcoded_asset_path: Regex,
}
impl Default for UnrealCppExtractor {
fn default() -> Self {
Self::new()
}
}
impl UnrealCppExtractor {
/// Create a new Unreal C++ extractor with compiled regexes.
///
/// # Panics
/// Panics if regex patterns are invalid.
#[allow(clippy::expect_used)]
pub fn new() -> Self {
Self {
exec_function: Regex::new(r"UFUNCTION\s*\((?:[^)]*,\s*)?Exec(?:,\s*[^)]*)?\)")
.expect("valid regex"),
unconditional_replication: Regex::new(
r"UPROPERTY\s*\((?:[^)]*,\s*)?Replicated(?:\s*)\)",
)
.expect("valid regex"),
hardcoded_asset_path: Regex::new(r#"TEXT\s*\(\s*['"]/(Game|Engine)/[^'"]+['"]\s*\)"#)
.expect("valid regex"),
}
}
fn check_pattern(
&self,
content: &str,
pattern: &Regex,
path_segments: &[String],
file: &str,
category: &str,
leaf: &str,
desc_template: &str,
) -> Vec<Observation> {
let mut claims = Vec::new();
for (line_idx, line) in content.lines().enumerate() {
if let Some(matched) = pattern.find(line) {
let mut concept_path = path_segments.to_vec();
concept_path.push("unreal".to_string());
concept_path.push(category.to_string());
concept_path.push(leaf.to_string());
claims.push(Observation {
concept_path: format!("code://{}", concept_path.join("/")),
predicate: "exposed".to_string(), // Default predicate
value: ObjectValue::Boolean(true),
file: file.to_string(),
line: line_idx + 1,
matched_text: matched.as_str().to_string(),
confidence: 0.9,
description: desc_template.to_string(),
});
}
}
claims
}
}
impl Extractor for UnrealCppExtractor {
fn name(&self) -> &str {
"unreal_cpp"
}
fn languages(&self) -> &[Language] {
&[Language::Cpp]
}
fn extract(
&self,
path_segments: &[String],
content: &str,
language: Language,
file: &str,
) -> Vec<Observation> {
if language != Language::Cpp {
return vec![];
}
let mut claims = Vec::new();
// Check for Exec functions
claims.extend(self.check_pattern(
content,
&self.exec_function,
path_segments,
file,
"security",
"exec_function",
"UFUNCTION(Exec) exposes this function to the console",
));
// Check for Unconditional Replication
claims.extend(self.check_pattern(
content,
&self.unconditional_replication,
path_segments,
file,
"security",
"replication",
"UPROPERTY(Replicated) used without condition",
));
// Check for Hardcoded Asset Paths
claims.extend(self.check_pattern(
content,
&self.hardcoded_asset_path,
path_segments,
file,
"assets",
"hardcoded_path",
"Hardcoded asset path found in C++. Use SoftObjectPtr or UPROPERTY(Config) instead.",
));
claims
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exec_function_detection() {
let extractor = UnrealCppExtractor::new();
let content = r#"
UCLASS()
class AMyActor : public AActor {
GENERATED_BODY()
UFUNCTION(Exec)
void CheatGiveMoney();
};
"#;
let claims = extractor.extract(
&["cpp".to_string()],
content,
Language::Cpp,
"Source/MyGame/MyActor.h",
);
assert_eq!(claims.len(), 1);
assert_eq!(claims[0].concept_path, "code://cpp/unreal/security/exec_function");
}
#[test]
fn test_hardcoded_asset_path_detection() {
let extractor = UnrealCppExtractor::new();
let content = r#"
static ConstructorHelpers::FObjectFinder<UTexture2D> Logo(TEXT("/Game/UI/Logo"));
static ConstructorHelpers::FClassFinder<AActor> Pawn(TEXT("/Engine/BasicShapes/Cube"));
"#;
let claims = extractor.extract(
&["cpp".to_string()],
content,
Language::Cpp,
"Source/MyGame/MyActor.cpp",
);
assert_eq!(claims.len(), 2);
assert_eq!(claims[0].concept_path, "code://cpp/unreal/assets/hardcoded_path");
assert_eq!(claims[1].concept_path, "code://cpp/unreal/assets/hardcoded_path");
}
}