## 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>
194 lines
5.5 KiB
Rust
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");
|
|
}
|
|
}
|